Vue3 和 Vue2 的区别

VueVue3MonorepoComposition APISource Code性能优化
  • 源码全部采用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)

2升级到3的大的变动,都是通过RFC的机制进行确认。

官方给出一些提案,然后收集社区的反馈并讨论,最终确认。

# 设计动机

Vue2.x在设计中小型项目的时候已经很好用,但是使用2.x在使用长期开发迭代和维护的大型项目的时候,也会有一些限制。在大型项目中可能会有一些复杂功能的组件,我们在看他人开发的组件的时候可能会很难看懂。原因是vue2.x开发组件的时候使用的是Options API

Options API

包含一个描述组件选项(data、methods、props等)的对象来创建组件的方式

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
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

我们看别人的组件,同一个功能可能会同时涉及data、methods、props、mounted等选项,其代码会被拆分到不同的选项,项目开发属性需要来回拖动,且增加一个小功能要同时修改多个选项

另外,使用这种API还难以提取组件中可重用的逻辑,虽然有mixin混入机制,可以把重复的代码提取并重用,但是mixin使用的过程也有问题,例如:命名冲突或者数据来源不清晰等。

Composition API

Vue3.0新增API,是一组基于函数的API,可以更灵活的组织组件的逻辑

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
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

在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中模板编译的结果:

<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>
1
2
3
4
5
6
7
8
9
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"])
  ]))
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Fragments

如果我们把根节点app删了,这里会有一个Fragment,代码片段,这里还维持了一个树形结构。

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 */))
}
1
2
3
4
5
6
7
8
9

# 静态提升

如果我们在options里面勾选静态提升

静态提升

那么我们可以看到静态节点都提升到了最上面




 
 
 
 
 
 













// 这些静态节点只有在初始化的时候才会被创建一次
// 当我们再次调用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 */))
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# patchFlag

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 */))
}
1
2
3
4
5
6
7
8
9
patchFlag 含义
1 文本内容是动态绑定的,这里只会比较文本内容是否会发生变化/* TEXT */
9 文本和props都是动态内容/* TEXT, PROPS */,还记录了绑定的动态属性是id,所以这里只会比较文本内容和id属性是否会发生变化。

这样就大大提升了虚拟DOM的diff的性能,diff性能是最耗时的,vue2中重新渲染的时候需要去重新创建新旧VNode,diff的时候会跳过静态根节点,对比剩下的每一个新旧VNode,哪怕这个节点什么都没有做。

Vue3中通过标记静态节点以及patchFlag标记动态节点,大大提升了diff的性能。

# 缓存事件处理函数

cacheHandlers —— 缓存事件处理函数

缓存事件处理函数






 



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 */))
}
1
2
3
4
5
6
7
8





 
 
 



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 */))
}
1
2
3
4
5
6
7
8
9
10

注意缓存的函数永远不会发生变化,即绑定不会发生变化,但是运行这个函数的时候,会获取最新的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版本默认只会加载核心运行时和响应式系统。

更新时间: 2022-03-25 17:04