- A+
vue作者尤雨溪在开发 vue3.0 的时候开发的一个基于浏览器原生 ES imports 的开发服务器(开发构建工具)。那么我们先来了解一下vite
Vite
Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 rollup 打。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,做得好可以彻底解决改一行代码等半天热更新的问题。它做到了本地快速开发启动, 用 vite 文档上的介绍,它具有以下特点:
- 快速的冷启动,不需要等待打包操作;
- 即时的热模块更新,替换性能和模块数量的解耦让更新飞起;
- 真正的按需编译,不再等待整个应用编译完成;
使用 npm:
# npm 7+,需要加上额外的双短横线 $ npm init vite@latest <project-name> -- --template vue $ cd <project-name> $ npm install $ npm run dev
或者 yarn:
$ yarn create vite <project-name> --template vue $ cd <project-name> $ yarn $ yarn dev
概览
- 速度更快
- 体积减少
- 更易维护
- 更接近原生
- 更易使用
- 重写了虚拟Dom实现
diff算法优化
<div> <span/> <span>{{ msg }}</span> </div>
被编译成:
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue" export function render(_ctx, _cache) { return (_openBlock(), _createBlock("div", null, [ _createVNode("span", null, "static"), _createVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) ])) }
首先静态节点进行提升,会提升到 render 函数外面,这样一来,这个静态节点永远只被创建一次,之后直接在 render 函数中使用就行了。
Vue在运行时会生成number(大于0)值的PatchFlag,用作标记,仅带有PatchFlag标记的节点会被真正追踪,无论层级嵌套多深,它的动态节点都直接与Block根节点绑定,无需再去遍历静态节点,所以处理的数据量减少,性能得到很大的提升。
- 事件监听缓存:cacheHandlers
<div> <span @click="onClick"> {{msg}} </span> </div>
优化前:
import { toDisplayString as _toDisplayString, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue" export function render(_ctx, _cache) { return (_openBlock(), _createBlock("div", null, [ _createVNode("span", { onClick: _ctx.onClick }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["onClick"]) ])) }
onClick会被视为PROPS动态绑定,后续替换点击事件时需要进行更新。
优化后:
import { toDisplayString as _toDisplayString, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue" export function render(_ctx, _cache) { return (_openBlock(), _createBlock("div", null, [ _createVNode("span", { onClick: _cache[1] || (_cache[1] = $event => (_ctx.onClick($event))) }, _toDisplayString(_ctx.msg), 1 /* TEXT */) ])) }
会自动生成一个内联函数,这个内联函数里面再去引用当前组件最新的onclick,然后把这个内联函数cache起来,第一次渲染的时候会创建内联函数并且缓存,后续的更新就直接从缓存里面读同一个函数,既然是同一个函数就没有再更新的必要,就变成了一个静态节点
3. SSR速度提高
当有大量静态的内容时,这些内容会被当做纯字符串推进一个buffer里面,即使存在动态的绑定,会通过模板 插值嵌入进去,这样会比通过虚拟dom来渲染的快很多。vue3.0 当静态文件大到一定量的时候,会用_ceratStaticVNode方法在客户端去生成一个static node, 这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染
- tree-shaking
tree-shakinng 原理
主要依赖es6的模块化的语法,es6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,
分析程序流,判断哪些变量未被使用、引用,进而删除对应代码
前提是所有的东西都必须用ES6 module的import来写
按照作者的原话解释,Tree-shaking其实就是:把无用的模块进行“剪枝”,很多没有用到的API就不会打包到最后的包里
在Vue2中,全局 API 如 Vue.nextTick() 是不支持 tree-shake 的,不管它们实际是否被使用,都会被包含在最终的打包产物中。
而Vue3源码引入tree shaking特性,将全局 API 进行分块。如果你不使用其某些功能,它们将不会包含在你的基础包中
5. compositon Api
没有Composition API之前vue相关业务的代码需要配置到option的特定的区域,中小型项目是没有问题的,但是在大型项目中会导致后期的维护性比较复杂,同时代码可复用性不高
compositon api提供了以下几个函数:
-
setup (入口函数,接收两个参数(props,context))
-
ref (将一个原始数据类型转换成一个带有响应式特性)
-
reactive (reactive 用来定义响应式的对象)
-
watchEffect
-
watch
-
computed
-
toRefs (解构响应式对象数据)
-
生命周期的hooks
如果用ref处理对象或数组,内部会自动将对象/数组转换为reactive的代理对象
ref内部:通过给value属性添加getter/setter来实现对数据的劫持
reactive内部:通过使用proxy来实现对对象内部所有数据的劫持,并通过Reflect反射操作对象内部数据
ref的数据操作:在js中使用ref对象.value获取数据,在模板中可直接使用
import { useRouter } from 'vue-router' import { reactive, onMounted, toRefs } from 'vue' // setup在beforeCreate 钩子之前被调用 // setup() 内部,this是undefined,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这在和其它选项式 API 一起使用 setup() 时可能会导致混淆 // props 是响应式的,当传入新的 prop 时,它将被更新(因为props是响应式的,所以不能使用 ES6 解构,因为它会消除 prop 的响应性。) // props参数:包含组件props配置声明且传入了的所有props的对象 // attrs参数:包含没有在props配置中声明的属性对象,相当于this.$attrs // slots参数:包含所有传入的插槽内容的对象,相当于this.$slots // emit参数:可以用来分发一个自定义事件,相当于this.$emit setup (props, {attrs, slots, emit}) { const state = reactive({ userInfo: {} }) const getUserInfo = async () => { state.userInfo = await GET_USER_INFO(props.id) } onMounted(getUserInfo) // 在 `mounted` 时调用 `getUserInfo` // setup的返回值 // 一般都是返回一个对象,为模板提供数据,就是模板中可以直接使用此对象中所有属性/方法 // 返回对象中的属性会与data函数返回对象的属性合并成为组件对象的属性 // 返回对象中的方法会与methods中的方法合并成组件对象的方法 // 若有重名,setup优先 return { ...toRefs(state), getUserInfo } }
灵活的逻辑组合与复用
可与现有的Options API一起使用
与选项API最大的区别的是逻辑的关注点
选项API这种碎片化使得理解和维护复杂组件变得困难,在处理单个逻辑关注点时,我们必须不断地上下翻找相关代码的选项块。
compositon API将同一个逻辑关注点相关代码收集在一起
6. Fragment(碎片)
<template> <header>...</header> <main v-bind="$attrs">...</main> <footer>...</footer> </template>
Vue 3不再限于模板中的单个根节点,它正式支持了多根节点的组件,可纯文字,多节点,v-for等
render 函数也可以返回数组
7. Teleport(传送门)
这个组件的作用主要用来将模板内的 DOM 元素移动到其他位置。
允许我们控制在 DOM 中哪个父节点下渲染了 HTML
<teleport to="body"> <div v-if="modalOpen" class="modal"> <div> I'm a teleported modal! (My parent is "body") <button @click="modalOpen = false"> Close </button> </div> </div> </teleport>
-
更好的Typescript支持
vue3是基于typescipt编写的,可以享受到自动的类型定义提示 -
自定义渲染 API
vue官方实现的 createApp 会给我们的 template 映射生成 html 代码,但是要是你不想渲染生成到 html ,而是要渲染生成到 canvas 之类的不是html的代码的时候,那就需要用到 Custom Renderer API 来定义自己的 render 渲染生成函数了。
意味着以后可以通过 vue, Dom 编程的方式来进行canvas、webgl 编程
默认的目标渲染平台
自定义目标渲染平台
-
响应原理的变化
vue2对象响应化:遍历每个key,通过 Object.defineProperty API定义getter,setter 进而触发一些视图更新
数组响应化:覆盖数组的原型方法,增加通知变更的逻辑
vue2响应式痛点
递归,消耗大
新增/删除属性,需要额外实现单独的API
数组,需要额外实现
Map Set Class等数据类型,无法响应式
修改语法有限制
vue3响应式方案: 使用ES6的Proxy进行数据响应化,解决上述vue2所有痛点,Proxy可以在目标对象上加一层拦截/代理,外界对目标对象的操作,都会经过这层拦截。Proxy可以在目标对象上加一层拦截/代理,外界对目标对象的操作,都会经过这层拦截,相比 Object.defineProperty ,Proxy支持的对象操作十分全面
一, 全局api
1. 全局 Vue API 已更改为使用应用程序实例
vue2使用全局api 如 Vue.component, Vue.mixin, Vue.use等,缺点是会导致所创建的根实例将共享相同的全局配置(从相同的 Vue 构造函数创建的每个根实例都共享同一套全局环境。这样就导致一个问题,只要某一个根实例对 全局 API 和 全局配置做了变动,就会影响由相同 Vue 构造函数创建的其他根实例。)
vue3 新增了createApp,调用createApp返回一个应用实例,拥有全局API的一个子集,任何全局改变 Vue 行为的 API 现在都会移动到应用实例上
2. 组件挂载
import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.mount('#app')
createApp初始化后会返回一个app对象,里面包含一个mount函数
mount函数是被重写过的
- 处理传入的容器并生成节点;
- 判断传入的组件是不是函数组件,组件里有没有render函数,template属性,没有就用容器的innerHTML作为组件的template;
- 清空容器内容
- 运行缓存的mount函数实现挂载组件;
二, 模板指令
- 组件上 v-model 用法更改,替换 v-bind.sync
vue2默认会利用名为 value 的 prop 和名为 input 的事件
// ParentComponent <ChildComponent v-model="pageTitle" /> <!-- 是以下的简写: --> <ChildComponent :value="pageTitle" @input="pageTitle = $event" /> // ChildComponent <input type="text" :value="value" @input="$emit('input', $event.target.value)">
如果想要更改 prop 或事件名称,则需要在组件中添加 model 选项:
model选项,允许组件自定义用于 v-model 的 prop 和事件
// ChildComponent <input type="text" :value="title" @input="$emit('change', $event.target.value)"> export default { model: { prop: 'title', event: 'change' }, props: { title: String } }
使用 title
代替 value
作为 model 的 prop
vue2.3 新增.sync (对某一个 prop 进行“双向绑定”,是update:title 事件的简写)
// ParentComponent <ChildComponent :title.sync="name" /> <!-- 是以下的简写 --> <ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
// ChildComponent <input type="text" :value="title" @input="$emit('update:title', $event.target.value)">
在 3.x 中,自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件
prop:value -> modelValue;
event:input -> update:modelValue
v-bind 的 .sync 修饰符和组件的 model 选项已移除,可用 v-model加参数 作为代替
vue3 可以将一个 argument 传递给 v-model:
<ChildComponent v-model:title="pageTitle" />
等价于
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
可使用多个model
- 可以在template元素上添加 key
<template v-for="item in list" :key="item.id"> <div>...</div> </template>
- 同一节点v-if 比 v-for 优先级更高
- v-bind="object" 现在排序敏感(绑定相同property,vue2单独的 property 总是会覆盖 object 中的绑定。vue3按顺序决定如何合并)
<div id="red" v-bind="{ id: 'blue' }" ></div> // vue2 id="red" // vue3 id="blue"
- 移除 v-on.native 修饰符
Vue 2 如果想要在一个组件的根元素上直接监听一个原生事件,需要使用v-on 的 .native 修饰符
Vue3 现在将所有未在组件emits 选项中定义的事件作为原生事件添加到子组件的根元素中(除非子组件选项中设置了 inheritAttrs: false)。
(强烈建议组件中使用的所有通过emit触发的event都在emits中声明)
<my-component @close="handleComponentEvent" @click="handleNativeClickEvent"/>
// mycomponent <template> <div> <button @click="$emit('click')">click</button> <button @click="$emit('close')">close</button> </div> </template> <script> export default { emits: ['close'] } </script>
- v-for 中的 ref 不再注册 ref 数组
vue2在 v-for 语句中使用ref属性时,会生成refs数组插入$refs属性中。由于当存在嵌套的v-for时,这种处理方式会变得复杂且低效。
vue3在 v-for 语句中使用ref属性 将不再会自动在$refs中创建数组。而是,将 ref 绑定到一个 function 中,在 function 中可以灵活处理ref。
<div v-for="item in list" :ref="setItemRef"></div> export default { setup() { let itemRefs = [] const setItemRef = el => { itemRefs.push(el) } return { setItemRef } } }
三, 组件
-
函数式组件
在 Vue 2 中,函数式组件有两个主要应用场景:
作为性能优化,因为它们的初始化速度比有状态组件快得多
返回多个根节点
然而Vue 3对有状态组件的性能进行了提升,与函数式组件的性能相差无几。此外,有状态组件现在还包括返回多个根节点的能力。所以,建议只使用有状态组件。结合<template>的函数式组件:
- functional 移除
- 将 props 的所有引用重命名为 $props,attrs 重命名为 $attrs。
<template> <component :is=`h${$props.level}` v-bind='$attrs' /> </template> <script> export default { props: ['level'] } </script>
函数写法:
相较于 Vue 2.x 有三点变化:
- 所有的函数式组件都是用普通函数创建的,换句话说,不需要定义 { functional: true } 组件选项。
- export default导出的是一个函数,函数有两个参数:
props
context(上下文):context是一个对象,包含attrs、slot、emit属性 - h函数需要全局导入
import { h } from 'vue' const DynamicHeading = (props, context) => { return h(`h${props.level}`, context.attrs, context.slots) } DynamicHeading.props = ['level'] export default DynamicHeading
- 异步组件需要 defineAsyncComponent 方法来创建
异步组件的导入需要使用辅助函数defineAsyncComponent来进行显式声明
import { defineAsyncComponent } from 'vue' const child = defineAsyncComponent(() => import('@/components/async-component-child.vue'))
带选项异步组件,component 选项重命名为 loader
const asyncPageWithOptions = defineAsyncComponent({ loader: () => import('./NextPage.vue'), delay: 200, timeout: 3000, error: ErrorComponent, loading: LoadingComponent })
- (新增)组件事件需要在 emits 选项中声明()
强烈建议使用 emits 记录每个组件所触发的所有事件。
因为移除了 v-on.native 修饰符。任何未声明 emits 的事件监听器都会被算入组件的 $attrs 并绑定在组件的根节点上。
如果emit的是原生的事件(如,click),就会存在两次触发。
一次来自于$emit的触发;
一次来自于根元素原生事件监听器的触发;
(emits 1.更好的记录已发出的事件,2.验证抛出的事件)
export default { props: ['text'], emits: ['accepted'] }
emits: { click: null, submit: payload => { if (payload.email && payload.password) { return true } else { console.warn(`Invalid submit event payload!`) return false } } }
四, 渲染函数
- 渲染函数API
h是全局导入,而不是作为参数传递给渲染函数
在 2.x 中,render 函数会自动接收 h 函数作为参数
在 3.x 中,h 函数需要全局导入。由于 render 函数不再接收任何参数,它将主要在 setup() 函数内部使用。可以访问在作用域中声明的响应式状态和函数,以及传递给 setup() 的参数
import { h, reactive } from 'vue' export default { setup(props, { slots, attrs, emit }) { const state = reactive({ count: 0 }) function increment() { state.count++ } // 返回render函数 return () => h( 'div', { onClick: increment }, state.count ) } }
- 移除$listeners整合到 $attrs
包含了父作用域中的(不含emits的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
{{$attrs}} <grand-son v-bind="$attrs"></grand-son>
- $attrs包含class&style
在vue2中,关于父组件使用子组件有这样一个原则:
默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上
这句话的意思是,父组件调用子组件时,给子组件锚点标签添加的属性中,除了在子组件的props中声明的属性,其他属性会自动添加到子组件根元素上。
为此,vue添加了inheritAttrs = false,这些默认行为将会被去掉,通过实例 property $attrs 可以让这些 attribute 生效,且可以通过 v-bind 显性的绑定到非根元素上。
五, 自定义元素
- 自定义元素检测在编译时执行
自定义元素交互
Vue 2中,通过 Vue.config.ignoredElements 配置自定义元素
Vue.config.ignoredElements = ['plastic-button']
Vue 3 通过app.config.isCustomElement
const app = Vue.createApp({}) app.config.isCustomElement = tag => tag === 'plastic-button'
- Vue 3.x 对 is做了新的限制
当在 Vue 保留的 component标签上使用is时,它的行为将与 Vue 2.x 中的一致
当在不同组件标签上使用is时,is会被当做一个不同的prop;
当在普通的 HTML 元素上使用is,is将会被当做元素的属性。
新增了v-is,专门来实现在普通的 HTML 元素渲染组件。
六, 其他
- destroyed 生命周期选项被重命名为 unmounted
- beforeDestroy 生命周期选项被重命名为 beforeUnmount
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-awWIzouv-1637307558259)(assets/vue3/img.png)]
整体来看其实变化不大,使用setup代替了之前的beforeCreate和created,其他生命周期名字有些变化,功能都是没有变化的 - Props 的默认值函数不能访问this
替代方案:
把组件接收到的原始 prop 作为参数传递给默认函数;
inject API 可以在默认函数中使用。
import { inject } from 'vue' export default { props: { theme: { default (props) { // `props` 是传递给组件的原始值。 // 也可以使用 `inject` 来访问注入的属性 return inject('theme', 'default-theme') } } } }
- 自定义指令 API 与组件生命周期一致
const MyDirective = { created(el, binding, vnode, prevVnode) {}, // 新增 beforeMount() {}, mounted() {}, beforeUpdate() {}, // 新增 updated() {}, beforeUnmount() {}, // 新增 unmounted() {} }
绑定组件的实例从 Vue 2.x 的vnode.context移到了binding.instance中
- data 选项应始终被声明为一个函数
data 组件选项声明不再接收 js 对象,只接受函数形式的声明。
<script> import { createApp } from 'vue' createApp({ data() { return { apiKey: 'a1b2c3' } } }).mount('#app') </script>
当合并来自 mixin 或 extend 的多个 data 返回值时,data现在变为浅拷贝形式(只合并根级属性)。
const Mixin = { data() { return { user: { name: 'Jack', id: 1 } } } } const CompA = { mixins: [Mixin], data() { return { user: { id: 2 } } } }
vue2
{ "user": { "id": 2, "name": "Jack" } }
vue3
{ "user": { "id": 2 } }
- 过渡的 class 名更改(过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from。)
- transition-group 不再需要设置根元素(
不再默认渲染根元素,但仍可以使用 tag prop创建一个根元素。) - 侦听数组(当侦听一个数组时,只有当数组被替换时才会触发回调。如果你需要在数组改变时触发回调,必须指定 deep 选项。)
- 已挂载的应用不会取代它所挂载的元素(在vue2中,当挂载一个具有 template 的应用时,被渲染的内容会替换我们要挂载的目标元素。在 Vue 3.x 中,被渲染的应用会作为子元素插入,从而替换目标元素的 innerHTML)
- 生命周期 hook: 事件前缀改为 vnode-(监听子组件和第三方组件的生命周期)
移除API
- 不再支持使用数字 (即键码) 作为 v-on 修饰符,vue3建议使用按键alias(别名)作为v-on的修饰符。
<input v-on:keyup.delete="confirmDelete" />
- vue3将移除且不再支持 filters,如果需要实现过滤功能,建议通过method或computed属性来实现(如果需要使用全局过滤器vue3提供了globalProperties。我们可以借助globalProperties来注册全局过滤, 全局过滤器里面定义的只能是method。)
const app = createApp(App) app.config.globalProperties.$filters = { currencyUSD(value) { return '$' + value } }
<template> <p>{{ $filters.currencyUSD(accountBalance) }}</p> </template>
- 内联模板 (inline-template attribute移除)
- $children(如果需要访问子组件实例,建议使用 $refs)
- propsData 选项之前用于在创建 Vue 实例的过程中传入 prop,现在它被移除了。如果想为 Vue 3 应用的根组件传入 prop,使用 createApp 的第二个参数。
- 全局函数 set 和 delete 以及实例方法 $set 和 $delete。基于代理的变化检测不再需要它们了。
用于迁移的构建版本
@vue/compat (即“迁移构建版本”) 是一个 Vue 3 的构建版本,提供了可配置的兼容 Vue 2 的行为。
该构建版本默认运行在 Vue 2 的模式下——大部分公有 API 的行为和 Vue 2 一致,仅有一小部分例外。使用在 Vue 3 中发生改变或被废弃的特性时会抛出运行时警告。一个特性的兼容性也可以基于单个组件进行开启或禁用。
已知的限制:
-
基于vue2内部API或文档中未记载行为的依赖。最常见的情况就是使用 VNodes 上的私有 property。如果你的项目依赖诸如 Vuetify、Quasar 或 Element UI 等组件库,那么最好等待一下它们的 Vue 3 兼容版本。
-
对IE11的支持:Vue 3 已经官方放弃对 IE11 的支持。如果仍然需要支持 IE11 或更低版本,那你仍需继续使用 Vue 2。
-
服务端渲染:该迁移构建版本可以被用于服务端渲染,但是迁移一个自定义的服务端渲染设置有更多工作要做。大致的思路是将 vue-server-renderer 替换为 @vue/server-renderer。Vue 3 不再提供一个包渲染器,推荐使用 Vite 以支持 Vue 3 服务端渲染。