vue3.0进展相关资源链接
以下内容整理自尤大演讲视频,点击查看视频详情
vue3.0进展特性一栏
- 更快
- virtual DOM实现重构,优化编译减少运行时开销
- 优化slots的生成
- 静态内容的提取
- 数据监听系统
- 利用proxy减少组件实例初始化开销
- 更小
- vue本身的runtime可以变得更小
- 更易维护
- Flow -> TypeScript
- 内部模块解耦
- 模板编译器重构
- 更好的多端渲染支持
- 更好的警告信息
- 新特性
- 新功能之响应式的数据监听Api
- 轻松排查组件更新的触发原因
- 更好的ts支持包括原生的Calss Api和TSX
- Experimental Hooks api
- Experimental Time slicing Support
- 关于IE支持
- 最早也得2019年下半年发布
virtual DOM实现重构,优化编译减少运行时开销
virtual dom在运行时有很多潜在开销,比如说在模板里有很多不会变动的地方,但虚拟dom会重新生成这些节点,然后对这些节点进行比对,其实这些操作在很多情况下其实不必要,因此可以在编译时对模板进行分析,来减少这些运行时的开销。vue2.x有一定程度的优化,但3.0更彻底。
当将模板编译为虚拟dom的渲染函数时,在vue2.0里不管是原生组件还是浏览器自带的html的元素,都是统一作为一个字符串传递给h函数(就是创建一个虚拟node的一个函数)里,然后判断一个元素到底是组件还是原生元素的时候,其实是在运行时来做的。这里不可避免就有些开销,因为都需要先判断一下是组件还是原生元素。而3.0则在编译时进行判断,如果一定是一个html原生元素的话,那我们在运行时就直接生成原生元素对应的虚拟dom的代码。同样,如果是组件,就生成组件的代码。这就是所谓的Component fast Path
,还有就是尽可能生成所谓的叫Monomorphic calls
,即在生成这些虚拟node的时候,我们的函数调用要尽可能的形状一致,也就是它要有同样个数的参数。这样对于我们生成的代码,更易于被js引擎来优化,这些就是一些比较底层的优化技巧。
最后还可以做,在模板中直接静态的分析一个元素所包含的子元素的类型,比如下面代码,我们在div里有一个span标签,这时就可以在生成的代码里面,给运行时留下一些类似于hint
(其实就是一些提示),比如这里是一个元素,然后就可以在算法里,直接跳入对应只有一个子元素的这个分支。就可以跳过很多其他不必要的判断,这些积少成多的优化,就可以在整个应用产生很客观的收益。
<div>
<span></span>
</div>
优化slots的生成
之前我们使用slots时,一般先在父组件里定义一个slot,然后会自动传入到子组件。但当这个slot在父组件内变化后,会先在父组件里更新完毕,然后才会传入子组件并引发子组件更新,也就是每次都需要更新两个组件,也就是父子关联更新。在新的生成机制里,我们将所有的slot都跟scope slot
一样统一生成为一个函数,这个函数可以认为是一个lazy的函数,当你把函数传给子组件之后,由子组件来决定什么时候来调用这个函数,当子组件调用这个函数的时候,这个slot的依赖就成为了子组件而不是父组件的,这时当依赖变动的时候,只需要重新渲染子组件,然后父子的依赖就彻底的分开。这样在整个应用中就会得到一个非常精确的组件级的依赖收集,进而可以进一步的避免不必要的组件渲染。到这里,就基本可以说vue的更新检测是完全精确的了,也就是任何组件当其真正的依赖变化时才会更新,就不存在需要手动优化组件过度重绘这个问题了。
静态内容的提取
这个其实在2.0里已经做了,就是当我们检测到一部分的模板是不变的,直接可以把它提取出来,那么在之后的更新中,这一部分模板不仅可以直接复用之前的虚拟DOM,连比对过程都可以直接整个树直接跳过。但2.0里没有做的是,当一个元素它内部任意深度包含任意动态内容的时候,那整个元素就无法被静态化,那这就很可惜,但是还可以做一些优化,那就是如果这个元素本身上面所有的属性都是静态的,那就可以把这个属性对象给提取出来,提取之后,然后我们比对这个元素的时候,发现这个元素它的所有的data都是一样的,那这个元素本身就不需要比对了。然后就可以直接去比对他们的children就可以了。
还有内联事件函数提取,比如<Comp @event="count++">
其实就是一个内联的函数,也就是每次重新渲染的时候都会生成一个新的函数,由于这个新的函数和之前的函数是不一样的,虽然做的事情是一样的,但对于js来说无法区分,因此为了安全起见,会导致这个子组件每次都重新渲染,通过一定程度的优化,就是我们把它生成一次之后,就把它给cache,之后每次都复用同一个函数,这样就可以避免子组件无谓更新的一个效果。
以上就是关于模板编译以及虚拟dom运行时性能方面的优化
数据监听系统:
在2.0里面,用的是ES5的getter及setter,也就是Object.defineProperty
这个api,在3.0里我们会基于proxy来实现一个全新的数据监听系统。这个已经实现在我们的Prototype里面了,还实现了全语言特性支持,同时还有更好的特性。全语言特性支持意味着,对新属性的增加、删除,数组的index/length修改,Map,Set,WeakMap,WeakSet及Classes等都能完美支持。同时在应用初始化时候,侦听大规模数据的时候,性能也会得到提升。事实上基于proxy的数据侦听系统,是所谓的lazy by default,就是只有当一个数据被用到的时候,我们才会使用它,如果对于一个大数据,但是只使用了其中的一部分,那其实我们只会监听其中的一小部分,这也是另一方面的一个性能优化。
利用proxy还可以减少组件实例初始化开销:
我们知道每个vue组件都会代理它所包含的,包括所有的data,computed以及props,这些代理都是通过Object.defineProperty
来实现的。在实际的实例生成中,其实大量的Object.defineProperty
是一个相对昂贵的一个操作。在3.0里,我们直接暴露给用户的这个this,其实是一个真正的组件实例的一个proxy,然后当你在这个proxy上获取一些属性的时候,我们内部再做判断,这样就彻底的避免了Object.defineProperty
使用。然后实测下来也是对组件实例化、初始化,实例的初始化带来了很高的性能提升,所以实例及组件的初始化也快了近一倍。所有这些初始化最终达到的一个大概的效果就是,速度加倍,内存占用减半。这些改动都不涉及上层api的改动,也就是说使用3.0还是之前的api就可以达到这个性能。
vue本身的runtime可以变得更小:
现在vue代码运行时也就是20k左右,为了做到更小,就是让我们的整个代码结构可以和tree-shaking(可以在最后编译的时候将没有用到的代码给扔掉)配合起来。之前vue的代码,一个vue对象进来,然后所有的东西都在这个vue对象上,这样的话其实所有你没有用到的东西,也没有办法扔掉,因为他们都被增添到vue这个全局对象上了。但在3.0里面,一些不是每个应用都需要的功能,就做成了按需引入,用ES module import按需引入,比如内置组件keep-alive,transition等,以及指令的配合的运行时,比如v-model,v-for,需要的一些helper的函数,各种的工具函数,比如说创建一个async component、使用mixins,或者是memoize(这是一个新的内部工具函数),很多时候这些工具函数,都可以做成按需引入。
还有很多指令的运行时,在我们编译的时候,就可以生成对应的代码。也就是说当你使用了v-model的时候,我们才会去miport v-model相关的代码。这就保证了当你使用最基本的功能时,你就只会用到最核心的任何vue应用都会用到的那一部分代码。这样总的效果就是最终vue运行时的代码在10k左右,这样在之前速度加倍,内存减半后,这里还是可以尺寸减半(就是代码大小减半)。
更易于维护
其实是针对vue的开发团队而言的,这对于想要阅读源码及参与进来的开发人员来说,也是一件有意义的事情。
Flow -> TypeScript
代码从Flow迁徙到ts,我们是用ts完全重写了。flow这个项目,说实话facebook维护的不怎么样。ts则更加友好。所以以后全部改用ts,所以以后对ts的支持会越来越好。。。
内部模块解耦
对于2.0的源码其实是不太好理解的,但到了3.0,因为用到了ts,本身这些声明的类型就有助于理解源码。因此对于阅读源码的人来说更友好。
另外就是在内部的模块进行清晰的解耦,也就是说解耦后的模板是相对独立的,是可以单独使用的,因此只需要每个模块每个模块的阅读源码就好,每个模块内部还有文档及单元测试。然后就可以直接提出PR或改动,而不用担心影响到其他模块。这也是出于让更多的人参与到vue源码开发的目的的。
模板编译器重构
目标是插件化设计,其实在2.0里也有类似的,但是在内部有很多比如像v-if,v-for这样的编译的逻辑,是直接写死的代码。我们希望插件化以后,每一个对应的指令或者每一个之前提到的各种优化,都可以单独的做成一个一个的解耦的小插件。这样对于用户的阅读,维护及修bug都是可以起到一定程度的改善。
然后对于编译,我们希望能够提供一个带位置信息的Parser,这样的话就可以生成Source map。这样的话,如果生成的渲染函数里产生了runtime的错误,就会直接指向模板中出错的地方,同时这也是为更好的IDE工具链铺路。
在各种IDE中,有语法高亮及像vscode的vue插件vetur,他们需要用Vue的Parser去做一些事情。然后还有比如像ESLint的plugin,它也需要一个Vue的Parser,其实这几个东西他们都有各自的Vue Parser的实现,还有比如说pretty里面也用Vue Parser,但是我们希望有一个真正的基础的Parser,可以服务于所有这些用例。然后就是Vetur的作者,微软的vscode团队开发人员,正在投入Vetur相关的改善,接下来会有密切合作,同时vue3.0编译器重构他也会直接参与进来。
更好的多端渲染支持
weex, wepy(把vue编译成小程序)等,我们也希望之后vue可以作为一个运行时,去支持编译到尽可能多的端,最终实现learn once ,run anywhere
,2.0虽然能做到这一点,但是需要开发者fork vue的源码,来实现这些功能,这样对于维护成本就会提高很多。
然后在vue3.0里,我们会引入一个真正的Custom Render API,你要做的就是import 这个createRenderer,如下:
import { createRenderer } from '@vue/runtime-core'
const { render } = createRenderer({
nodeOps,
patchData
})
这里的runtime-core就是一个平台无关的runtime,它包含了vue的各种组件,虚拟dom的这些算法等等,但是它不包含任何跟dom直接相关的代码。跟dom直接相关的代码,就是在你createRenderer的时候,通过上面的nodeOps(这些就是节点操作)以及patchData(就是如何处理一个元素在被更新的时候,它上面的各种属性的操作),提供这些对应的函数之后,你就获得了一个render函数。这个render函数可以让你,把vue的组件及虚拟dom直接渲染到你的原生对象上面去。
还有源码里的runtime-dom, 就是以后跑在浏览器里面的这一块,也是用一模一样的Custom Render代码给它包出来的,所以以后vue本身的针对浏览器的这个,其实也是用一样的API写出来的。所以以后像weex及小程序的维护者,在用vue来做这些渲染到原生的实现的时候,其实就是完全只需要把runtime-core拉进来作为一个依赖就行了,那以后vue有更新,就只需更新依赖就行了,也不需要fork,然后去处理各种可能存在的各种merge conflict。
然后在源码里面还有runtime-test,现在vue3.0自己的测试都是用它写的,他可以跑在nodejs里面,它都不需要js dom就可以直接跑起来,然后它还给你一个功能就是,可以查看所做的每一次的这个操作,来确认它有没有真正做正确的操作。
更好的警告信息
- 组件堆栈包含函数式组件
- 可以直接在警告信息中查看组件的props
- 在更多的警告中提供组件堆栈信息
新功能之响应式的数据监听Api
vue之前的响应式功能并没有暴露出来,而3.0里暴露了出来,如下
import { observable, effect } from 'vue'
const state = observable({
count: 0
})
effect(() => {
console.log(`count is: ${state.count}`)
})// count is: 0
state.count++ // count is:1
现在的话就是直接作为可以import的两个函数,显示的创建一个用observable函数去创建一个显示的响应式的对象。这个就跟你把一个对象,传到vue的组件的data里面被转化之后,他就变为一个响应式的效果一样。所谓的响应式就是,在effect当中去依赖任何响应式的对象,就会注册依赖,那之后当这个对象被改动的时候,这个effect就会重新再执行一遍。
其实暴露这个api的目的是说,我们可以轻松的用observable轻松的实现跨组件的状态共享。在一些简单的应用里面就已经可以应付很多常见的用例了。
轻松排查组件更新的触发原因
我们希望轻松排查组件更新的触发原因,一些用户对vue的这种可变数据的开发模式,他们的一种担心就是说,我可以在任何的地方去改动数据,然后会触发一连串的反应,但是这可能最后让我没有办法去理解,为什么我的组件会更新。
然后在3.0里提供了renderTriggered
这个api,就是每一次组件触发更新的时候,就可以在这个地方放一个debugger,这样之后就可以在浏览器里面直接的看到究竟是哪一行触发的这个更新,同时event还可以提供一些更具体的信息。
const Comp = {
render(props) {
return h('div', props.count)
},
renderTriggered(event) {
debugger
}
}
更好的ts支持包括原生的Calss Api和TSX
我们会有一个原生的Class api,你就不要再依赖第三方同时也不需要依赖Vue Class Component这个库了。因为我们已经内置了原生支持,这样性能也会好很多。其实api不仅仅可以在ts里面用,如果你在原生es2015里面用,甚至不需要babel编译,就是原生的浏览器支持的Class,我们也支持,而且语法几乎是一样的。
interface HelloProps {
text: string
}
class Hello extends Component<HelloProps> {
count = 0
render() {
return <div>
{this.count}
{this.$props.text}
</div>
}
}
ts文件用TSX的时候最大的好处是,在编辑器里就可以及时给出错误提示。
Experimental Hooks api
- 作为一种逻辑复用机制,大概率取代mixins
最近大家对于React Hooks也已经听了很多了,React Hooks其实它背后的用处、意义及实现,其实和react本身并没有特别直接的关联,他是一个可以在任何框架内被使用的概念,事实上在vue中也是完全可以用的,而且vue之前也已经放了一个叫vue hooks的实验性质的库,在vue中实现也是完全没有问题的。
Hooks作为一种逻辑复用机制,可以说是完爆mixins的,我们也希望在vue中提供类似的机制,当然是否完全提供跟react完全一样的api,还有待商榷,我们希望是能够提供类似的功能,但是更贴合vue用户的习惯,所以目前还在研究,怎样以一种更合理的方式来引入这个东西。
我们确实也不希望以Hooks作为vue的一个主要使用的api,因为这和vue一直以来的设计是不太一致的,但是它本身的功能还是很有用的。所以我们希望能够把hooks以一种取代mixins的,这样的一种逻辑复用机制的形式引入进来。大家有兴趣可以去github上找vue hooks相关的代码。
Experimental Time slicing Support
Time slicing其实也是react前段时间提出的一个概念,叫做Concurrent React
,也就是说能够让我们的这个框架,在进行js计算的时候,把这些js计算切成一块一块的,一帧一帧的去做。因为作为框架最容易导致的问题就是我们在浏览器的主线程里面,进行大量的js操作,会使得整个主线程就被block,被block的时候浏览器其实是处在一个完全没有响应的状态。此时用户所有的事件(比如点击,输入等)全部都是无法被响应的。如果用户的输入导致大量的js计算,同时用户继续输入的话,就会导致大量计算被重复,被重复的推进来,那么性能就直线下降。要改进这个问题,可以通过每16毫秒也就是每一帧,我们做了这么多活以后,就把yeild给浏览器,让用户的事件重新进来,然后触发更新。这中间有机会说,可以让用户的这个新事件进来以后,可能会导致一些之前的需要做的更新被invalid,也就是可以不用去做。那么就可以省去一些不必要的活儿。
// 一个间隔一毫秒执行的函数
function block() {
const start = performance.now()
while (performance.now() - start < 1){
// block
}
}
比如用户每输入一个字母都会触发一个200个组件同时更新的场合,每个组件假设更新时间为1毫秒,则200个组件就需要200ms,这个时候用户不停的输入,可以看到输入框并不是立刻显示出来用户输入的信息,而是有卡滞的一下子出来很多用户输入的信息。。。这种体验就不太好,如果使用Time slicing之后(具体性能可以查看chrome的perfermance录制详情),大量的js计算被切成一帧一帧的,每一帧只做差不多16到17ms的工作量,然后就会交还给浏览器,那么浏览器就有机会去监听用户事件,同时再去做出响应,就使得整个应用,即使进行大量js计算的时候,依然保持着能够响应用户的操作。
如下图:
还可参考:
react的Fiber
完全理解React Fiber
关于IE 会有一个专门的版本,在IE11中自动降级为旧的getter/setter机制,并对ie中不支持的用法给出警告。当然在新的浏览器里会用proxy的版本来提高性能。
关于发布时间
最早也得明年下半年发布。。。