关于Vue进阶问题的总结

一、为什么Vue不再使用 mixin 了

1.命名冲突

当mixin和引入组件中命名相同时

命名内容是数据对象,mixin被覆盖;

命名内容是methods、计算属性,mixin被覆盖;

命名内容是生命周期,会合并为一个数组,mixin先被执行。

以上是大白话,专业一点,这是Vue组件的合并策略在发挥作用。合并策略指本地选项将覆盖 mixin 选项。生命周期是个例外,若存在多个生命周期钩子,将被添加成数组,依次调用。

若第三方的 mixin 作为带有自己命名属性的 npm 包被添加到项目中,可能会导致冲突。

2. 依赖不透明

mixin 与 使用它的组件之间没有层次关系。这意味着组件可以使用 mixin 中的属性,mixin 也可以使用组件中的数据属性。

3. 对后续开发者不友好

除了以上两点,在 Vue2 源码分析中,发现 Vue2 大量使用了 mixin 的实现思路,导致源码溯源很难定位,即灵活度太高导致代码约束性太少。对后续开发者增加了很多上手成本。

二、Vue2 Diff和Vue3 Diff的不同

  • diff算法open in new window 的目的都是尽可能减少 dom 变更引起的 reflow/repaint,找到可以复用的节点。
  • Vue2 Diff采用双端diff,新旧列表的头尾互相对比,对比过程中指针会逐渐向内靠拢,缺点在于使全量 Diff,如果层级很高非常消耗内存。
  • Vue3 Diff有两个理念,一是相同的前置和后置元素的预处理,二是最长递增子序列。相比Vue2 Diff 增加了静态标记,作用是在发生变化的地方添加一个flag,下次变化时直接找到这个标记点进行比较,提高性能。
  • 具体详细点见本博客文章《Vue2 Diff原理》《Vue3 Diff原理》

三、Vue3.0 性能提升主要通过哪几个方面体现?

1.编译阶段

回顾 Vue2,每个组件实例都对应一个 watcher实例,它会在组件渲染的过程中把用到的数据property记录为依赖,当依赖发生改变,触发setter,则会通知watcher,从而使关联的组件重新渲染。

✅如下代码:组件内只有一个动态节点,其余都是静态节点,这里很多 diff 和遍历都是不需要的。

<template>
  <div id="content">
    <p class="text">静态文本</p>
    <p class="text">静态文本</p>
    <p class="text">{{ message }}</p>
    <p class="text">静态文本</p>
    ...
    <p class="text">静态文本</p>
</div>
</template>

✅因此在 Vue3 编译阶段,做了进一步优化:

diff 算法优化、静态提升、事件监听缓存、SSR 优化

1️⃣diff 算法优化:

Vue3 在 diff 算法中相比 Vue2 增加了静态标记

这个静态标记是为了在发生变化的地方添加一个 flag 标记,下次发生变化时直接找到这个地方进行比较。

//一些静态枚举类型💌
export const enum PatchFlags {
  TEXT = 1,// 动态的文本节点
  CLASS = 1 << 1,  // 2 动态的 class
  STYLE = 1 << 2,  // 4 动态的 style
  PROPS = 1 << 3,  // 8 动态属性,不包括类名和样式
  FULL_PROPS = 1 << 4,  // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
  HYDRATE_EVENTS = 1 << 5,  // 32 表示带有事件监听器的节点
  STABLE_FRAGMENT = 1 << 6,   // 64 一个不会改变子节点顺序的 Fragment
  KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
  UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
  NEED_PATCH = 1 << 9,   // 512
  DYNAMIC_SLOTS = 1 << 10,  // 动态 solt
  HOISTED = -1,  // 特殊标志是负整数表示永远不会用作 diff
  BAIL = -2 // 一个特殊的标志,指代差异算法
}

2️⃣静态提升:

Vue3 中对比不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用;

此举避免了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时占用的内存。

<span>你好</span>
<div>{{ message }}</div>

以下是根据此节点对有无静态提升的比对:

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _createVNode("span", null, "你好"),
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ], 64 /* STABLE_FRAGMENT */))
}
//代码块2;
const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "你好", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock(_Fragment, null, [
    _hoisted_1,
    _createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
  ], 64 /* STABLE_FRAGMENT */))
}
// Check the console for the AST

🚩做了静态提升后,静态内容_hoisted_1被放置在 render 函数之外,每次渲染只要取_hoisted_1即可。同时_hoisted_1被打上了PatchFlag,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff.

3️⃣事件监听缓存

默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化

<button @click = 'onClick'>点我</button>

以下是根据此节点对有无事件侦听器缓存的比对:

export const render = /*#__PURE__*/
_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
    // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
  ]))
})
export function render(_ctx, _cache, $props, $setup, $data, $options){
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", {
      onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
    }, "点我")
  ]))
}

🚩 开启缓存后,没有了静态标记,下次 diff算法时直接使用

4️⃣SSR 优化:

🚩当静态内容大到一定量级时,用createStaticVNode方法在客户端生成 static node,这些静态节点会直接 innerHtml,就不需要创建对象,然后根据对象渲染。

<div>
  <div>
    <span>你好</span>
  </div>
  ... <!--很多个静态属性-->
  <div>
    <span>{{ message }}</span>
  </div>
</div>

编译后:

import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as 
  _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate 
} from "@vue/server-renderer"
export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  const _cssVars = { style: { color: _ctx.color }}
  _push(`<div${
    _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
  }><div><span>你好</span>...<div><span>你好</span><div><span>${
    _ssrInterpolate(_ctx.message)
  }</span></div></div>`)
}

2.源码体积

相比 Vue2,Vue3 整体体积变小了,除了移除不常用的 API,重要的是 Tree Shaking。任何函数,如ref、reative、computed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小。

3.响应式系统

  • Vue2 中采用 defineProperty来劫持整个对象,然后深度遍历所有属性,给每个属性添加gettersetter,实现响应式;

  • Vue3 采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历:

    • 可以监听动态属性的添加
    • 可以监听到数组的索引和数组的length属性
    • 可以监听删除属性