使用层面上来说

1. 选项式API 与组合式API

image.png
在 Vue 2 中 功能以及实现功能的数据其实是分开的。代码体量小还好,当代码的行数越来越大的时候,寻找定义的数据,以及修改数据的方法就变为了一件吃力的操作。尤其是想要复用一段代码的逻辑时,需要在不同的 option 找寻依赖关系
Vue 3 采用 Composition API 的方式,解决了复用困难的问题,将数据,修改数据的方法,数据改动时的监视器全部可以写在一起,开发真能更好的对代码进行维护。

2. 定义数据的方式不一样了

Vue 2 梭哈 data,直接递归整个 data 定义一个庞大的响应式系统,而 Vue 3 中采用 ref 和 reactive 来分别创建基本数据类型和引用数据类型的响应式数据。
这个也是更好的契合 API 风格的改变。

3. 生命周期的改变

删除了 beforeCreate、created 两个生命周期,将原来的生命周期命名改为了 on 开头当形式
image.png

4. watch 和 computed

watch

第一次进入到页面的时候不会执行回调,监视的内容发生改变时会执行回调函数。

  1. watch(message, (newValue, oldValue) => {
  2. console.log("新的值:", newValue);
  3. console.log("旧的值:", oldValue);
  4. });

监视多个值

  1. const x1 = ref(12);
  2. const number = reactive({ count: 0 });
  3. const countAdd = () => {
  4. number.count++;
  5. };
  6. watch([x1, () => number.count], (newValue, oldValue) => {
  7. console.log("新的值:", newValue);
  8. console.log("旧的值:", oldValue);
  9. });

深度监视

  1. watch(
  2. () => number,
  3. (newValue, oldValue) => {
  4. console.log("新的值:", newValue);
  5. console.log("旧的值:", oldValue);
  6. },
  7. { deep: true }
  8. );

watchEffect

自动追踪依赖,初始化立即执行一次,类似于 React 中的 useEffect
手动关闭监视器

  1. const unwatch = watchEffect(() => {})
  2. // ...当该侦听器不再需要时
  3. unwatch()

源码上来说

diff 优化

尤大公布的数据就是 update 性能提升了 1.3~2 倍

  • 事件缓存:将事件缓存,可以理解为变成静态的了
  • 添加静态标记:Vue2 是全量 Diff,Vue3 是静态标记 + 非全量 Diff
  • 静态提升:创建静态节点时保存,后续直接复用
  • 使用最长递增子序列优化了对比流程:Vue2 里在 updateChildren() 函数里对比变更,在 Vue3 里这一块的逻辑主要在 patchKeyedChildren() 函数里,具体看下面
    事件缓存
    比如这样一个有点击事件的按钮
    1. <button @click="handleClick">按钮</button>
    来看下在 Vue3 被编译后的结果
    1. export function render(_ctx, _cache, $props, $setup, $data, $options) {
    2. return (_openBlock(), _createElementBlock("button", {
    3. onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
    4. }, "按钮"))
    5. }
    注意看,onClick 会先读取缓存,如果缓存没有的话,就把传入的事件存到缓存里,都可以理解为变成静态节点了,优秀吧,而在 Vue2 中就没有缓存,就是动态的

    静态标记

    源码地址:packages/shared/src/patchFlags.ts
    1. export const enum PatchFlags {
    2. TEXT = 1 , // 动态文本节点
    3. CLASS = 1 << 1, // 2 动态class
    4. STYLE = 1 << 2, // 4 动态style
    5. PROPS = 1 << 3, // 8 除去class/style以外的动态属性
    6. FULL_PROPS = 1 << 4, // 16 有动态key属性的节点,当key改变时,需进行完整的diff比较
    7. HYDRATE_EVENTS = 1 << 5, // 32 有监听事件的节点
    8. STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序的fragment (一个组件内多个根元素就会用fragment包裹)
    9. KEYED_FRAGMENT = 1 << 7, // 128 带有key属性的fragment或部分子节点有key
    10. UNKEYEN_FRAGMENT = 1 << 8, // 256 子节点没有key的fragment
    11. NEED_PATCH = 1 << 9, // 512 一个节点只会进行非props比较
    12. DYNAMIC_SLOTS = 1 << 10, // 1024 动态slot
    13. HOISTED = -1, // 静态节点
    14. BAIL = -2 // 表示 Diff 过程中不需要优化
    15. }
    先了解一下静态标记有什么用?看个图
    在什么地方用到的呢?比如下面这样的代码
    1. <div id="app">
    2. <div>沐华</div>
    3. <p>{{ age }}</p>
    4. </div>
    在 Vue2 中编译的结果是,有兴趣的可以自行安装 vue-template-compiler 自行测试
    1. with(this){
    2. return _c(
    3. 'div',
    4. {attrs:{"id":"app"}},
    5. [
    6. _c('div',[_v("沐华")]),
    7. _c('p',[_v(_s(age))])
    8. ]
    9. )
    10. }
    在 Vue3 中编译的结果是这样的,有兴趣的可以点击这里自行测试 ```javascript const hoisted1 = { id: “app” } const _hoisted_2 = /*#__PURE/_createElementVNode(“div”, null, “沐华”, -1 / HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock(“div”, _hoisted_1, [ _hoisted_2, _createElementVNode(“p”, null, _toDisplayString(_ctx.age), 1 / TEXT /) ])) }

  1. 看到上面编译结果中的 -1 1 了吗,这就是静态标记,这是在 Vue2 中没有的,patch 过程中就会判断这个标记来 Diff 优化流程,跳过一些静态节点对比
  2. <a name="LG221"></a>
  3. #### 静态提升
  4. 简单理解:不参与更新的元素保存起来,render返回这个元素的引用。<br />在 Vue2 里每当触发更新的时候,不管元素是否参与更新,每次都会全部重新创建
  5. ```javascript
  6. with(this){
  7. return _c(
  8. 'div',
  9. {attrs:{"id":"app"}},
  10. [
  11. _c('div',[_v("沐华")]),
  12. _c('p',[_v(_s(age))])
  13. ]
  14. )
  15. }

而在 Vue3 中会把这个不参与更新的元素保存起来,只创建一次,之后在每次渲染的时候不停地复用,比如上面例子中的这个,静态的创建一次保存起来

  1. const _hoisted_1 = { id: "app" }
  2. const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, "沐华", -1 /* HOISTED */)

然后每次更新 age 的时候,就只创建这个动态的内容,复用上面保存的静态内容

  1. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  2. return (_openBlock(), _createElementBlock("div", _hoisted_1, [
  3. _hoisted_2,
  4. _createElementVNode("p", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
  5. ]))
  6. }

patchKeyedChildren

在 Vue2 里 updateChildren 会进行

  • 头和头比
  • 尾和尾比
  • 头和尾比
  • 尾和头比
  • 都没有命中的对比

在 Vue3 里 patchKeyedChildren 为

  • 头和头比
  • 尾和尾比
  • 基于最长递增子序列进行移动/添加/删除

看个例子,比如

  • 老的 children:[ a, b, c, d, e, f, g ]
  • 新的 children:[ a, b, f, c, d, e, h, g ]
  1. 先进行头和头比,发现不同就结束循环,得到 [ a, b ]
  2. 再进行尾和尾比,发现不同就结束循环,得到 [ g ]
  3. 再保存没有比较过的节点 [ f, c, d, e, h ],并通过 newIndexToOldIndexMap 拿到在数组里对应的下标,生成数组 [ 5, 2, 3, 4, -1 ],-1 是老数组里没有的就说明是新增
  4. 然后再拿取出数组里的最长递增子序列,也就是 [ 2, 3, 4 ] 对应的节点 [ c, d, e ]
  5. 然后只需要把其他剩余的节点,基于 [ c, d, e ] 的位置进行移动/新增/删除就可以了

使用最长递增子序列可以最大程度的减少 DOM 的移动,达到最少的 DOM 操作
力扣 300 最长递增子序列问题;https://leetcode.cn/submissions/detail/349683059/