Vue3.0
Vue 3.0 已经在原型设计阶段了,而且我们已经实现了一个与 2.0 的特性近乎相等的运行时了。下文中列出的许多条目,要么已经实现了,要么已经确认可实现。那些还未实现或者仍在探索阶段的条目会用一个“*”标记
性能提升
一句话简介:更小巧,更快速;支持摇树优化;支持 Fragments 和跨组件渲染;支持自定义渲染器。
- 更小巧:这份新的代码库在设计之初就考虑到了对“摇树优化 (tree-shaking)”的友好。那些如内置组件 (、) 、运行时工具性指令(v-model)等特性将变为按需导入,所以也是“可摇树的”。对于这个新的运行时,它的大小将永远保持在 10kb 之下。另外,使这些特性变为“可摇树的”后,我们就可以提供更多的内置特性,同时还不会增加网络负载——如果没使用到这些特性的话。
摇树优化,是一种在打包时去除没用到的代码的优化手段,谷歌有一篇教程可以了解下: Reduce JavaScript Payloads with Tree Shaking
- 更快:在前期的基准测试中,我们看到整体性能有了一倍的提升,包括虚拟 DOM 的挂载和打补丁(patching,指更新——译注) 的速度(我们从 Inferno 那里学了好些个技巧过来——Inferno 是目前速度最快的虚拟 DOM 实现),以及组件实例化速度和数据监测的性能。在 3.0 中,你应用的启动时间将缩减一半。
- 支持 Fragments 和 Portal:虽然体积更小了,但 3.0 还将内置对 Fragments (即允许组件拥有多个根节点) 和 Portal (即允许在 DOM 的其他位置进行渲染,而不是组件内部) 的支持。
关于 Portal ,你可以将其理解为跨组件渲染或者异地渲染,vue-portal 是一个第三方实现,可以了解一下;Fragments 特性也有一个第三方库,但译者认为这个库的内部实现不够完善,叫做 vue-fragments,感兴趣可以了解一下。
- 增强的 slot 机制:所有由编译器生成的 slot 都将是函数形式,并且在子组件的 render 函数被调用过程中才被调用 (译注:现在只有 scoped slot 才是函数形式,其渲染的时机也是在父组件的渲染进行时)。这使得 slot 中的依赖项 (即数据——译注) 将被作为子组件的依赖项,而不是现在的父组件;从而意味着:1)当 slot 的内容发生变动时,只有子组件会被重新渲染;2)当父组件重新渲染时,如果子组件的内容未发生变动,子组件就没必要重新渲染。这种机制的改变,可以提供更精确的变动探测,也就可以消除没必要的重渲染。
- 支持自定义渲染器 (Renderer):这个 API 可以用来创建自定义的渲染器,它将作为“一等公民”出现,到时不再需要 fork 一份 Vue 的代码来通过修改实现自定义。这个 API 的到来,将使得那些如 Weex 和 NativeScript 的“渲染为原生应用”的项目保持与 Vue 的同步更新变得更加容易。除此之外,还将使得那些为了各种用途而创建自定义渲染器变得极其容易。
编译器相关的提升
- 如果采用的是支持“摇树优化”的打包器,模板中使用到的那些可选特性,在生成的代码中将通过 ES 的模块语法导入;而在打包后的文件中,那些没用到的可选特性就会被“摇掉”。
- 由于新的虚拟 DOM 实现所带来的提升,我们可以执行一些更加高效的编译耗时优化,如静态树提升(static tree hoisting)、静态属性提升(static props hoisting);以及为运行时提供一些来自编译器的提示,以此避开子组件的规范过程 (children normalization);提供 VNode 快速创建路径; 等等。
- 我们计划对解析器进行重写,以便在对模板进行编译发生错误时,可以提供错误发生的位置信息;除此之外还可以带来对模板的 source map 支持;还可以支持第三方工具如 eslint-plugin-vue 和 IDE 的语言服务 (language services) 特性。
API 变动
一句话介绍:除渲染函数 API 和 scoped-slot 语法之外,其余均保持不变或者将通过另外构建一个兼容包来兼容 2.x。
- 模板语法的 99% 将保持不变。除了 scoped slot 语法可能会有一些微调之外,我们还没有任何其他针对模板的变动计划。
- 3.0 版本将原生地支持基于 class 的组件,而且无需借助任何编译及各种 stage 阶段的特性,以此提供良好的编写体验。许多现有的 (组件) 配置项将有对应的合理的 class 版本的 API。各种 stage 阶段的特性,如 class 的静态字段和装饰器 (decorator) 等仍然可以选择性地使用,以此增强编写体验。另外,整体的 API 在设计时也将考虑 TypeScript 的类型推断特性。3.x 的代码库本身将用 TypeScript 来编写,并提供增强的 TypeScript 支持。(就是说,TypeScript 的使用与否仍然是整体可选的)
- 2.x 系列的基于对象的组件格式仍将受支持,不过会在内部将其转换为一个相应的 class。
- 仍然支持 Mixins。*
- 为了避免在安装插件时造成对 Vue 的运行时的修改,顶层 API 可能会做一个大的翻修。到时,插件的作用域将只局限到具体的一个组件树,使得对那些依赖于某些具体插件的组件的测试变得容易,也会使得在同一个页面中挂载多个使用不同插件——但使用同一个 Vue 运行时——的 Vue 应用变为可能。*
- 函数式组件将支持纯函数的书写形式——不过,这样的话异步组件就需要通过一个辅助性函数来显式地创建了。
- 变动最大的部分将是渲染函数 (render) 中的虚拟 DOM 的格式。我们现在正在收集主流的第三方库的作者们的反馈,在对这些变动有了更多的信心之后,我们还会将更多的细节曝光;虽然变动较大,但是只要你没在你的应用中重度使用手写的渲染函数 (不是指 JSX),那么变动之后的升级应该会比较容易。
代码重构
一句话介绍:更优良的内部模块解耦;TypeScript;更易于贡献的代码库。
在从零开始编写 3.0 之初,“达到更加清晰和更易维护的架构,特别是为了让代码的贡献变得容易”就是我们的目标。为了对复杂性进行隔离,我们将一些内部功能拆分为了多个单独的包。例如,observer 模块将成为一个单独的包,拥有自己对外的 API 和自己的测试用例;不过请注意,这不会对框架级的 API 造成影响——你不需要另外手动从多个包里导入许多小件小件的模块就可以使用 Vue,相反 Vue 的最终包会事先装配好这些内部包。
另外,代码库现在改为了用 TypeScript 编写;虽然这会使得“熟练 TypeScript”成为对新代码库进行贡献的一个前置要求,不过我们相信有类型信息配合 IDE 的支持,对于一个新的贡献者来说,要做出有意义的贡献,实际上反而会更加容易。
将 observer 和 scheduler 解耦为分开的两个包后,我们还可以拿一些替代的实现对这两个包进行置换试验。例如,我们可以实现一个兼容 IE11、API 也相同的 observer;或者实现另外一种利用 requestIdleCallback 来在长耗时的更新中产出工作成果到浏览器的 scheduler。
重写虚拟 DOM (Virtual DOM Rewrite)
随着虚拟 DOM 重写,我们可以期待更多的 编译时(compile-time)提示来减少 运行时(runtime)开销。重写将包括更有效的代码来创建虚拟节点。
优化插槽生成(Optimized Slots Generation)
在当前的 Vue 版本中,当父组件重新渲染时,其子组件也必须重新渲染(11 月 20 日更新:这句话是不严谨的,非常容易产生误导,我觉得有必要说明一下: 2.0 组件的重新渲染就是组件粒度的,除非修改的数据是子组件的 props,才会触发子组件的重新渲染。引用自:(https://juejin.im/pin/5bf28ddd6fb9a056783705fc)。 使用 Vue 3 ,可以单独重新渲染父组件和子组件。
静态树提升(Static Tree Hoisting)
使用静态树提升,这意味着 Vue 3 的编译器将能够检测到什么是静态组件,然后将其提升,从而降低了渲染成本。它将能够跳过未整个树结构打补丁的过程。
静态属性提升(Static Props Hoisting)
此外,我们可以期待静态属性提升,其中 Vue 3 将跳过不会改变节点的打补丁过程。
基于 Proxy 的观察者机制
目前,Vue 的反应系统是使用 Object.defineProperty 的 getter 和 setter。 但是,Vue 3 将使用 ES2015 Proxy 作为其观察者机制。 这消除了以前存在的警告,使速度加倍,并节省了一半的内存开销。
为了继续支持 IE11,Vue 3 将发布一个支持旧观察者机制和新 Proxy 版本的构建。
兼容 IE 11
一句话介绍:IE 11 将受到支持,但将会是另外构建一个版本 (build) 的形式支持,不过这个版本会存在与 Vue 2.x 响应式机制所存在的同样的局限。
新的代码库目前只针对主流浏览器,而且我们假定他们都支持 ES2015。但是,哎,我们也知道在可预见的未来还有很多用户仍然需要支持 IE11。除了 Proxy 外,大多数 ES2015 的特性都可以用转译或者垫片的方式在 IE11 中使用。我们的计划是另外单独实现一个具有同样 API 的替代性 observer,不过是基于老式的 Object.defineProperty API;然后再通过单独构建一个使用这个实现的 Vue 3.x 版本 (build) 进行发布,不过这个单独的版本还是会有 Vue 2.x 在变动探测方面所存在的问题,所以它其实并不是一个完全兼容 3.x 的一个版本。我们也意识到这会给第三方库的作者们带来某些不便,因为他们需要考虑两个不同版本之间的兼容性问题,不过当我们真的推进到那个阶段时,那时我们肯定会确保提供一份清晰的指导。
监测机制
一句话介绍:更加全面、精准、高效;更具可调试性的响应跟踪;以及可用来创建响应式对象的 API。
3.0 将带来一个基于 Proxy 的 observer 实现,它可以提供覆盖语言 (JavaScript——译注) 全范围的响应式能力,消除了当前 Vue 2 系列中基于 Object.defineProperty 所存在的一些局限,如:
- 对属性的添加、删除动作的监测
- 对数组基于下标的修改、对于 .length 修改的监测
- 对 Map、Set、WeakMap 和 WeakSet 的支持
另外这个新的 observer 还有以下特性:
- 公开的用于创建 observable (即响应式对象——译注) 的 API。这为小型到中型的应用提供了一种轻量级的、极其简单的跨组件状态管理解决方案。(译注:在这之前我们可以通过另外 new Vue({data : {…}}) 来创建这里所谓的 observable;另外,其实 vuex 内部也是用这种方式来实现的)
- 默认为惰性监测(Lazy Observation)。在 2.x 版本中,任何响应式数据,不管它的大小如何,都会在启动的时候被监测。如果你的数据量很大的话,在应用启动的时候,这就可能造成可观的性能消耗。而在 3.x 版本中,只有应用的初始可见部分所用到的数据会被监测,更不用说这种监测方案本身其实也是更加快的。
- 更精准的变动通知。举个例子:在 2.x 系列中,通过 Vue.set 强制添加一个新的属性,将导致所有依赖于这个对象的 watch 函数都会被执行一次;而在 3.x 中,只有依赖于这个具体属性的 watch 函数会被通知到。
- 不可变监测对象(Immutable observable):我们可以创建一个对象的“不可变”版本,以此来阻止对他的修改——包括他的嵌套属性,除非系统内部临时解除了这个限制。这种机制可以用来冻结传递到组件属性上的对象和处在 mutation 范围外的 Vuex 状态树。
- 更良好的可调试能力:通过使用新增的 renderTracked 和 renderTriggered 钩子,我们可以精确地追踪到一个组件发生重渲染的触发时机和完成时机,及其原因。
如果你想在v2.x中使用3.0的内容,可通过以下方式
npm install '@vue/composition-api'
在main.js中引入
import VueCompositionApi from '@vue/composition-api';
Vue.use(VueCompositionApi);
数据的双向绑定
V2.x中的数据双向绑定是通过object的defineProperty方法实现的,通过属性中的get和set方法中进行拦截操作。但是它无法监听数组的改变,除了使用以下几种方法可以:push()、pop()、shift()、unshift()、splice()、sort()、reverse(),如果直接使用index设置数组的某一项时是无法改变的。
V3.x中改用了proxy和Reflect实现双向绑定,它可以从更多方面对对象的变化进行监听,除了set 和 get 外,还提供了apply、setPrototypeOf、getPrototypeOf、deletePrototype、isExtensible、preventExtensions、getOwnPropertyDescriptor 、defineProperty、has、ownKeys、construct方法的监听。
对于proxy的实际应用我将在后期专门来讲解,本期以VU3.0的实践为主
直接上一斤的例子
<template>
<div>
<h3>数据绑定</h3>
<div>
<el-button @click="clicksingleVal">
单向绑定 {{singleVal}}
</el-button>
<el-button @click="clickdoubleVal">
双向绑定 {{doubleVal}}
</el-button>
<el-button @click="clickstateVal">
状态绑定 {{stateVal}}
</el-button>
</div>
</div>
</template>
<script>
import {ref, reactive, toRefs} from '@vue/composition-api'
export default {
setup(){
let singleVal = 1
const doubleVal = ref(1)
const state = reactive({
stateVal: 1
})
const methods = {
clicksingleVal(){
singleVal++
console.log(singleVal);
},
clickdoubleVal(){
doubleVal.value++
},
clickstateVal(){
state.stateVal++
},
}
return{
singleVal,
doubleVal,
...toRefs(state),
...methods
}
}
}
</script>
<style>
</style>
如果你是第一次看到上面的写法可能会有点陌生,这就是在vue3.x中的写法。在v2.x中的data、methods、computed、watch等内容,在v3.x中全部都写在一个叫 setup 的函数中。在里面我们可以任意的定义变量、函数等,最后通过return返回,返回的内容可以在模板中进行使用。
对于绑定的数据在v3.x中进行了更加详细的分类,可以分为以下三种:
- 单向绑定:var singleVal = 1,数据会变化,但是视图不会更新
- 单个双向绑定:const doubleVal = ref(1),使用 doubleVal.value 改变
- 双向绑定:需要使用 reactive ,使用 state.stateVal 改变值
我们来逐个的进行讲解:
- 单向绑定,听名字就可以知道它并不具有双向绑定的特性或功能,它只会在视图初始化时绑定一次,这个变量即使后面发生了改变,视图也不会更新。可以从控制台看到singleVal 的值的确发生了变化,但界面中始终显示的为1。变量声明的方法和我们平时声明一个变量一样,如:let singleVal = 1,最后在return中返回。
- 单个双向绑定,每次只能声明一个双向绑定的变量,通过ref函数创建一个包装对象,使它包含一个响应式的属性value。例如上面的const doubleVal = ref(1)。如果要改变它的值,需要改变的是它的属性value上的值,像这样一样doubleVal.value++。
- 双向绑定,通过reactive创建一个响应式的对象,这样创建的对象并不是一个包装对象,因此不需要使用.value来取值,它等价于 Vue 2.x 的Vue.observable。
const state = reactive({
stateVal: 1
})
return {
...state
}
对reactive的内容直接进行解构后返回,会导致响应式丢失,需要使用toRefs将reactive对象转为普通对象,这样结果对象上的每个属性都指向原始对象中对应属性的ref引用对象,保证了在使用对象解构或拓展运算符时响应式不会丢失。
对于事件方法,就和声明一个变量一样,在setup中声明,在return返回即可。
计算属性
引入computed方法,返回计算后的值,这里接着使用上面的例子,用total计算上面3个数的总和。
import {computed, ref, reactive, toRefs} from '@vue/composition-api'
export default {
setup(){
...
const total = computed(() =>{
return singleVal + doubleVal.value + state.stateVal
})
...
return{
...,
total
}
}
}
从演示效果中我们还可以看出一点,单向绑定的数据改变不会触发计算属性方法。
数据监听
同样还是写在setup中,也比较简单,没有什么可讲解的
import {computed, ref, reactive, toRefs, watch} from '@vue/composition-api'
...
watch(() => state.stateVal, (newVal, oldVal ) =>{
console.log(newVal, oldVal);
})
...
生命周期
vue3.0中取消了 beforeCreate 和 created 两个周期,因为setup会在这个这个周期前执行,因此你可以在setup中进行你需要的处理。其他的生命周期全部以on开头。
import {
onBeforeMount,
onMounted,
onBeforeUnmount,
onBeforeUpdate,
onDeactivated,
onUnmounted,
onUpdated
} from '@vue/composition-api'
export default {
setup(){
onBeforeMount(() =>{
console.log('onBeforeMount');
})
onMounted(() =>{
console.log('onMounted');
})
onBeforeUnmount(() =>{
console.log('onBeforeUnmount');
})
onBeforeUpdate(() =>{
console.log('onBeforeUpdate');
})
onDeactivated(() =>{
console.log('onDeactivated');
})
onUnmounted(() =>{
console.log('onUnmounted');
})
onUpdated(() =>{
console.log('onUpdated');
})
}
}
Mixin
vue3.0中使用函数式的API代替原有的mixin,mixin很容易引起命名重合和覆盖引入mixin的页面属性。
在vue3.0中以一个API的形式存在,当你需要时,将其引入。直接看例子
mixin.vue
<template>
<div>
<h3>mixins</h3>
<div>
<el-input v-model="searchValue" type="text" @change="onSearch"/>
<div>
<ul>
<li v-for="name in names" :key="name.id" v-show="name.show">
{{name.value}}
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import searchName from './searchName.js'
export default {
setup(){
let list = [
{id: 1, value: 'vue', show: true},
{id: 2, value: 'react', show: true},
{id: 3, value: 'angular', show: true},
{id: 4, value: 'elementui', show: true},
{id: 5, value: 'ant', show: true},
{id: 6, value: 'javascript', show: true},
{id: 7, value: 'css', show: true},
]
var {onSearch, names, searchValue} = searchName(list)
return {
onSearch,
names,
searchValue
}
}
}
</script>
<style>
</style>
searchName.js
import {reactive, toRefs} from '@vue/composition-api'
export default function searchName(names){
const state = reactive({
names: names,
searchValue: ''
})
const onSearch = () => {
state.names.forEach(name => {
name.show = name.value.includes(state.searchValue)
})
}
return {
...toRefs(state),
onSearch
}
}
上面我们将搜索功能独立到一个js文件中。在 searchName.js 中定义了一些属性和方法,这里的属性也是具有响应式的,最后返回这些内容。在组件中,先引入这个js文件,调用searchName方法,传入需要的参数。在该组件中的searchValue和names两个响应式数据并非自身的所有,而是来自searchName.js中,通过下面演示可以看到,他们的确也具有响应式的特性。
EventBus
- setup接收两个参数
- props:等同于V2.x中的 props:{},用于接收父组件传递的参数
ctx:上下文环境。在2.x中的this指向的是全局VUE实例,可以使用this.router、this.router、this.router、this.commit、this.emit等方法进行路由的跳转等操作。而在3.0中不能直接访问到this,如果你尝试输出this,你回看到它的值是undefined。但可以通过ctx.root.emit等方法进行路由的跳转等操作。而在3.0中不能直接访问到this,如果你尝试输出this,你回看到它的值是undefined。但可以通过ctx.root.emit等方法进行路由的跳转等操作。而在3.0中不能直接访问到this,如果你尝试输出this,你回看到它的值是undefined。但可以通过ctx.root.root.$emit将内容挂在到 $root 上,以使得任何地方都可以访问到。
setup(props, ctx){
...
const total = computed(() =>{
let total = singleVal + doubleVal.value + state.stateVal
ctx.root.$root.$emit('handleClick', {number: total })
return total
})
...
}
...
ctx.root.$root.$on('handleClick', (val) =>{
console.log('我是通过ctx.root.$root.$on触发的,接收的值为:' + val.number);
state.otherTotal = val.number
})
...