Appearance
Vue3 和 Vue2 的区别
- 源码全部采用
typescript
重写,提升了可维护性。 - 项目采用
monorepo
的方式组织项目结构,把不同的模块单独提取到独立的包中,依赖关系明确,每个模块都可以单独使用、单独测试、单独发布、 90%
以上的API
兼容2.x
- 根据社区反馈,添加了
Composition API
(组合API
),是解决使用2.x
开发大型项目时遇到超大组件,使用options API
不好拆分和重用的问题。 - 性能大幅度的提升,使用
proxy
重写了响应式的代码,并且会编译器做了优化,重写了虚拟DOM
,从而让渲染和update
的性能有了大幅度的提升,官方说服务端渲染的性能也提升了2~3
倍 - 官方提供了开发工具
Vite
,在开发阶段我们测试项目不需要打包,可以直接运行项目,提升开发效率。
Vue.js 3.0 源码组织方式
- packages
- compiler-core:平台无关的编译器
- compiler-dom:浏览器平台下的编译器,依赖compiler-core
- compiler-sfc:编译单文件组件,依赖compiler-core和compiler-dom
- compile-ssr:服务端渲染的编译器,依赖compiler-dom
- reactivity:数据响应式系统,可以独立使用
- runtime-core:平台无关的运行时
- runtime-dom:浏览器平台下的运行时,处理原生dom的api和事件等
- runtime-test:为测试编写的轻量级的运行时,这个运行时渲染出来的dom树其实是js对象,所以这个运行时可以运行在所有js环境里。可以测试渲染是否正确,还可以用于序列化DOM,触发DOM事件以及记录某次更新中的DOM操作。
- server-renderer:用于服务端渲染
- shared:用户内部公共的API
- size-check:私有的包,不会发布到npm,作用是在tree-shaking之后检查包的大小
- template-explorer:在浏览器中运行的实时编译组件,可以输出render函数,这个包的README.md里面提供包访问的地址。
- vue:构建完整版的Vue,依赖于compiler和runtime
- global.d.ts
不同的构建版本
3和2类似,都构建了不同的版本。不同的是,不再构建UMD模块化的方式,因为UMD模块化的方式会让代码有更多的冗余,3中把cjs, es module和自执行函数的方式分别打包到了不同的文件中,
构建方式 | 构建版本 |
---|---|
cjs | - vue.cjs.js (开发版,未压缩) - vue.cjs.prod.js (生产版,已压缩) - 这两种方式都是完成版的vue,包含编译器 |
global | - vue.global.js (完整版、开发、未压缩) - vue.global.prod.js (完整版、生产,压缩) - vue.runtime.global.js (运行时,开发,未压缩) - vue.runtime.global.prod.js (运行时,生产,压缩) - 这四个文件都可以直接通过script标签导入,导入之后会增加一个迁居的vue对象 |
brower | - vue.esm-browser.js (完整版、开发、未压缩) - vue.esm-browser.prod.js (完整版、生产、压缩) - vue.runtime.esm-browser.js (运行时、开发、未压缩) - vue.runtime.esm-browser.prod.js (运行时、生产、压缩) - 这几个都是esmodule,原生模块化方式,在浏览器中可以直接通过script,type=module的方式导入模块 |
bundle | - vue.esm-bundler.js (完整版、开发、未压缩) - vue.runtime.esm-bundler.js (运行时、开发、未压缩是vue的一个最小版本,在项目开发完毕后,重新打包只会打包使用到的代码,体积更小。) - 这两个没有打包所有的代码,他们需要配合所有的工具使用。这两个都使用es module的方式,内部通过import导入了runtime core |
Composition API
Composition API学习的最好方式就是查看官方的RFC
RFC(Request For Comments)
- github-rfcs
- Composition API RFC 中介绍了Composition API的使用
2升级到3的大的变动,都是通过RFC的机制进行确认。
官方给出一些提案,然后收集社区的反馈并讨论,最终确认。
设计动机
Vue2.x在设计中小型项目的时候已经很好用,但是使用2.x在使用长期开发迭代和维护的大型项目的时候,也会有一些限制。在大型项目中可能会有一些复杂功能的组件,我们在看他人开发的组件的时候可能会很难看懂。原因是vue2.x开发组件的时候使用的是Options API
Options API
包含一个描述组件选项(data、methods、props等)的对象来创建组件的方式
js
export default {
data () {
return {
position: {
x: 0,
y: 0
}
}
},
created () {
window.addEventListener('mousemove', this.handle)
},
destroyed () {
window.addEventListener('mousemove', this.handle)
},
methods: {
handle (e) {
this.position.x = e.pageX
this.position.y = e.pageY
}
}
}
我们看别人的组件,同一个功能可能会同时涉及data、methods、props、mounted等选项,其代码会被拆分到不同的选项,项目开发属性需要来回拖动,且增加一个小功能要同时修改多个选项。
另外,使用这种API还难以提取组件中可重用的逻辑,虽然有mixin混入机制,可以把重复的代码提取并重用,但是mixin使用的过程也有问题,例如:命名冲突或者数据来源不清晰等。
Composition API
Vue3.0新增API,是一组基于函数的API,可以更灵活的组织组件的逻辑
js
import {reactive, onMounted, onUnmounted } from 'vue'
// 获取鼠标位置的功能
function useMousePosition () {
const position = reactive({
x: 0,
y: 0
})
const update = (e) => {
position.x = e.pageX
position.y = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.addEventListener('mousemove', update)
})
return position
}
export default {
setup () {
// 引用
const position = useMousePosition()
return {
position
}
}
}
在vue3.0中既可以使用options API也可以使用Composition API,这种方式可以更灵活的组织组件中的代码结构,还可以将功能提取,方便其他组件进行复用。
性能提升
- 响应式系统升级(通过proxy重写响应式系统)
- 编译优化
- 源码体积优化
响应式系统升级
在性能和功能上都做了优化。
vue.js 2.x | Vue.js 3.0 |
---|---|
vue.js2.x中响应式系统的核心defineProperty,在初始化的时候遍历data中的所有成员,通过defineProperty,把对象的属性转化成getter和setter。 | 使用了proxy对象重写响应式系统,proxy的性能本身比defineProperty要好,另外代理对象可以拦截属性的访问、赋值、删除等操作。 |
初始化的时候,如果data中的属性是对象的话,需要递归处理每一个子对象的属性。如果属性并没有使用,也做了响应式的处理。 | 不需要初始化的时候遍历所有的属性。如果有嵌套对象的时候,只有在访问某个属性的时候才会去递归处理下一级的属性。 |
动态添加属性需要调用Vue.set方法 | 使用proxy对象默认可以监听到动态添加的属性, |
监听不了删除的属性 | 可以监听删除的属性, |
监听不了数组的索引和length属性 | 可以监听数组的索引和length属性 |
编译优化
优化编译过程和重写虚拟DOM提升渲染的性能
Vue2.x
Vue2.x的时候首先要在构建过程中将模板编译成render函数,在编译的时候会编译静态根节点和静态节点,静态根节点要求静态节点中必须要有一个静态子节点。
当组件的状态发生变化时,会通知watcher,触发watcher的update,最终去执行虚拟DOM的patch操作,遍历所有的虚拟节点,找到差异然后更新到真实DOM上。
Diff的过程中,先去比较整个的虚拟DOM,先对比新旧的div以及其属性,然后再对比其内部的子节点。
Vue2中渲染的最小单位是组件,Diff的过程会跳过静态根节点,因为静态根节点的内容不会发生变化。Vue2中通过标记静态根节点,优化了diff的过程。但是在vue2的时候,静态节点还需要在进行diff,这个过程没有被优化。
Vue3.0
Vue3中标记和提升所有的静态节点,diff的时候值需要对比动态节点的内容。
- Vue3中新引入了Fragments —— 片段特性,模板中不需要再创建唯一的一个根节点,模板里面可以直接放文本内容或者很多同级的标签。在vsCode中需要升级vetur插件,否则模板中没有唯一的根节点,模板依旧会提示有错误。
- 静态提升
- Patch flag
- 缓存事件处理函数
下面看一下vue3中模板编译的结果:
html
<div id="app">
<div> static root
<div>static node</div>
</div>
<div>static node</div>
<div>static node</div>
<div :id="id">{{ count }} </div>
<button @click="handler">button</button>
</div>
js
import { ... } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
// 调用_createBlock给根创建了一个block,是一个树的结构,冲过createVNode创建了子节点。
return (_openBlock(), _createBlock("div", { id: "app" }, [
// _createVNode类似h函数
_createVNode("div", null, [
_createTextVNode(" static root "),
_createVNode("div", null, "static node")
]),
_createVNode("div", null, "static node"),
_createVNode("div", null, "static node"),
_createVNode("div", { id: _ctx.id }, _toDisplayString(_ctx.count), 9 /* TEXT, PROPS */, ["id"]),
_createVNode("button", { onClick: _ctx.handler }, "button", 8 /* PROPS */, ["onClick"])
]))
}
Fragments
如果我们把根节点app删了,这里会有一个Fragment,代码片段,这里还维持了一个树形结构。
js
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode("div", null, [
...
]),
_createVNode("div", null, "static node"),
...
], 64 /* STABLE_FRAGMENT */))
}
静态提升
如果我们在options里面勾选静态提升
那么我们可以看到静态节点都提升到了最上面
js
// 这些静态节点只有在初始化的时候才会被创建一次
// 当我们再次调用render的时候不需要再次创建这些节点
// 因为之前创建过,我们可以直接重用上一次创建静态节点对应的VNode
const _hoisted_1 = /*#__PURE__*/_createVNode("div", null, [
/*#__PURE__*/_createTextVNode(" static root "),
/*#__PURE__*/_createVNode("div", null, "static node")
], -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("div", null, "static node", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createVNode("div", null, "static node", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
// 这三个节点被提升到了render的外层
_hoisted_1,
_hoisted_2,
_hoisted_3,
_createVNode("div", { id: _ctx.id }, _toDisplayString(_ctx.count), 9 /* TEXT, PROPS */, ["id"]),
_createVNode("button", { onClick: _ctx.handler }, "button", 8 /* PROPS */, ["onClick"])
], 64 /* STABLE_FRAGMENT */))
}
patchFlag
js
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_hoisted_1,
_hoisted_2,
_hoisted_3,
_createVNode("div", { id: _ctx.id }, _toDisplayString(_ctx.count), 9 /* TEXT, PROPS */, ["id"]),
_createVNode("button", { onClick: _ctx.handler }, "button", 8 /* PROPS */, ["onClick"])
], 64 /* STABLE_FRAGMENT */))
}
patchFlag | 含义 |
---|---|
1 | 文本内容是动态绑定的,这里只会比较文本内容是否会发生变化/* TEXT */ |
9 | 文本和props都是动态内容/* TEXT, PROPS */ ,还记录了绑定的动态属性是id,所以这里只会比较文本内容和id属性是否会发生变化。 |
这样就大大提升了虚拟DOM的diff的性能,diff性能是最耗时的,vue2中重新渲染的时候需要去重新创建新旧VNode,diff的时候会跳过静态根节点,对比剩下的每一个新旧VNode,哪怕这个节点什么都没有做。
Vue3中通过标记静态节点以及patchFlag标记动态节点,大大提升了diff的性能。
缓存事件处理函数
cacheHandlers —— 缓存事件处理函数
js
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
...
// 给button注册了_ctx.handler事件处理函数,这个事件处理函数可能是由data中返回的。
// 那之后可能会把这个函数赋值成另一个函数,这个时候需要进行一次更新的操作,当数据变化的时候重新渲染视图。
_createVNode("button", { onClick: _ctx.handler }, "button", 8 /* PROPS */, ["onClick"])
], 64 /* STABLE_FRAGMENT */))
}
js
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
...
// 当首次渲染的时候会生成一个新的函数,函数返回的就是handler,并把这个新的函数赋值到cache[1]中
// 将来再次调用render的时候会从上一次缓存中调用函数
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.handler && _ctx.handler(...args)))
}, "button")
], 64 /* STABLE_FRAGMENT */))
}
注意缓存的函数永远不会发生变化,即绑定不会发生变化,但是运行这个函数的时候,会获取最新的handler,避免了不必要的更新。
按需引入
这里如果要添加一个transition,可以看到右边会按需引入进来,这样可以减少不必要的引用。
源码体积优化
通过优化源码体积和更好的tree shaking的知识减少打包体积
优化源码
Vue3.0中移除了一些不常用的API:inline-template、filter等,可以让最终的体积变小。移除的filter可以使用methods或者计算属性实现。
Tree-shaing
Vue3.0对Tree-shaking的支持更好,依赖ES Module,通过编译阶段的静态分析,找到没有引入的模块,在打包的时候直接过滤掉,让打包之后的体积更小。
vue3在设计之初就考虑到了Tree Shaking,内置的组件比如Transition,keep-alive和一些内置的指令,比如v-model都是按需引入的。
除此之外,Vue3中的很多API都是支持Tree-shaking的,所以如果这些API没有使用的话是不会打包的,但是默认的Vue的核心模块都会被打包。我们在介绍Vue3中不同的构建版本的时候曾经看过,使用runtime的bundler版本默认只会加载核心运行时和响应式系统。