Vue3进阶

Nevermore毓2024年2月26日
大约 9 分钟

Vue3 与 Vue2 的对比

Vue3进阶

template模板不如jsx灵活,但是template相比jsx的固定性,可以在编译时获取许多信息,编译出可以在运行时执行尽可能少,性能尽可能好的代码。Vue3性能优化的一个重要体现在编译优化,利用新的渲染器,编译出了相比vue2更小,更快的代码

Tree Shaking - 优化体积

Vue3 源码中采用函数编写API,更加有利于Tree Shaking,而Tree Shaking的原理是 利用ES6 Module的编译时加载,编译时就能确定模块的依赖关系,没有使用到的代码最终会被 webpack 或者 vite这样的构建工具删掉,js体积减小,网络传输就更快,js引擎解析也会更快,代码执行更快

vue2项目打包体积对比

// App.vue 1
<template>
  <div>test vue2 tree-shaking</div>
</template>

<script>export default {
  data() {
    return {
      name: "App",
    };
  },
};
</script>// App.vue 2 

<template>
  <div>test vue2 tree-shaking</div>
</template>

<script>export default {
  data() {
    return {
      name: "App",
    };
  },
  computed: {
    fullName() {
      return this.name + "vue2";
    },
  },
  watch: {
    name(newVal, oldVal) {
      console.log(newVal, oldVal);
    },
  },
};
</script>

打包后vue文件大小没有变化

Vue3项目打包体积对比

// App.vue 1
<template>
  <div>test vue3 tree-shaking</div>  {{ fullName }}
</template>

<script setup>
import { ref, computed, watch, nextTick, reactive } from "vue";
const name = ref("App");

const obj = reactive({
  item: "tree-shaking",
});

const fullName = computed(() => name.value + "vue3");

watch(
  () => name.value,
  async (newVal, oldVal) => {
    console.log(newVal, oldVal);
    await nextTick();
    obj.item = "vue3 tree-shaking";
  }
);
</script>

打包vue文件大小有变化

Poxy - 优化数据劫持

vue2的数据劫持使用的是 Object.defineProperty,它的缺点也是众所周知,只能监听对象中已有的属性,不能监听对象的增加删除,所以如果有一个嵌套层级很深的响应式对象数据,vue2无法知道代码运行时具体会访问哪个属性,所以在初始化这个对象的时候,vue2只能采取递归遍历的方式把对象的每一层每一个属性都变成响应式,这就会影响页面的初始化渲染速度;

而vue3就不一样了,它使用proxy进行数据劫持,对于多层嵌套的对象,由于proxy只能代理一层,所以vue3在真正访问到对象属性的时候,才去判断递归,而不是在初始化的时候就一股脑的递归。

下面看一下vue2和vue3在源码中的实现

vue2源码实现

在之前写的一篇关于vue2的文章中, vue2响应式原理(1)--初始化响应式对象dataopen in new window 比较详细的介绍了数据的初始化,这里简化一下源码

function initData(vm: Component) {
  let data: any = vm.$options.data  // 观测 data  observe(data)
}

export function observe(  value: any,
  shallow?: boolean,
): Observer | void {
   new Observer(value, shallow)
}

export class Observer {
  constructor(    public value: any,
    public shallow = false // 默认深层响应
      ) {
    const keys = Object.keys(value);
    // 遍历每一个属性变成响应式
        for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      defineReactive(value, key, NO_INIITIAL_VALUE, undefined, shallow);
    }
  }
}

export function defineReactive(  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
) {
  val = obj[key]
  // 递归遍历,嵌套过深,性能损失
    !shallow && observe(val, false, mock)
  //...
  }

vue3源码实现

// 简化版源码
// ref()  ref也是包装过后的reactive 
export function ref(value?: unknown) {
  return createRef(value)
}

function createRef(rawValue: unknown) {
  return new RefImpl(rawValue)
}

class RefImpl<T> {
  private _value: T

  constructor(value: T) {
    this._value = reactive(value)
  }
  get value() {
    return this._value  }
  set value(newVal) {
    this._value = reactive(newVal)
  }
}

// reactive()
export function reactive(target: object) {
  return createReactiveObject(target)
}

function createReactiveObject(target: Target) {
  const proxy = new Proxy(target, {
    get(target: Target, key: string | symbol) {
      const res = Reflect.get(target, key);
      if (isObject(res)) {
        // 对象属性被访问的时候才递归执行下一步 reactive,
        // 优化数据初始化时性能
        return reactive(res);
      }
      return res;
    },
  });
  return proxy;
}

vue2静态标记

在vue2的编译中,有一个optimize过程,会对一些不会变化的DOM做静态标记,节点如果被标记为为静态根节点,会生成一个staticRenderFns来缓存它,而且它生成的vnode会带有isStatic:true的属性,以便在diff中跳过它们的对比。

// App.vue
<template>
  <div>
    <h1>2222</h1>
    <p>ddd</p>
  </div>
</template>

// \src\compiler\optimizer.ts
export function optimize(root,option) {
  // 标记静态节点
  markStatic(root)
  // 标记静态根节点
  markStaticRoots(root, false)
}

function markStaticRoots(node) {
// 标记为静态根节点的条件:
// 本身是一个静态节点,有children,children不能只是一个文本节点
// 避免过度优化,还不如重新渲染
 if (
      node.static &&
      node.children.length &&
      !(node.children.length === 1 && node.children[0].type === 3)
    ) {
      node.staticRoot = true
      return
    }
}

// \src\core\instance\render-helpers\render-static.ts
function markStaticNode(node) {
  node.isStatic = true
}

  // \src\core\vdom\patch.ts
  function patchVnode(oldVnode,vnode){
   // reuse element for static trees.
   // 复用静态节点,跳过详细对比
   if (
      isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }
  }

vue3编译优化

静态提升

vue3将模版中的静态节点和属性提取到render函数外面,在组件更新的时候,减少vnode的创建带来的性能损耗

// App.vue
<script>
import { ref } from "vue";
export default {
  setup() {
    const msg = ref("vue hosited");
    return { msg };
  },
};
</script>

<template>
  <div>
    <h1>静态提升测试</h1>
    <span>{{ msg }}</span>
  </div>
</template>

预字符串化

当有大量连续的静态节点时,通过转化为字符串,既减少vnode创建过程,也可以减少代码体积

// App.vue
<script>
import { ref } from "vue";
export default {
  setup() {
    const msg = ref("vue hosited");
    return { msg };
  },
};
</script>

<template>
  <div>
    <h1>静态提升测试</h1>
    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
      <li>6</li>
      <li>7</li>
      <li>8</li>
      <li>9</li>
      <li>10</li>
    </ul>
    <span>{{ msg }}</span>
  </div>
</template>

缓存事件处理函数

每次render函数执行过后,生成新的vnode,对vnode的props中事件属性进行patch的时候,就直接取上一次缓存的函数,如果没有缓存,每次函数都是新的,引用不一致,会造成组件的更新

<template>
  <div>
    <h1 @click="msg = 'cache'">静态提升测试</h1>
    <span @dblclick="msg = 'cache1'">{{ msg }}</span>
  </div>
</template>

Block Tree

Block是vue3在编译模板过程中做的优化,收集动态子节点,能够在diff过程中根据动态子节点数量更新。

<script setup>
import { ref } from "vue";
const msg = ref("vue");
</script>

<template>
  <div class="block">
    <h1>Block</h1>
    <span>{{ msg }}</span>
  </div>
</template>

在浏览器控制台Network中可以看到模板被编译后

import {
  createElementVNode as _createElementVNode,
  toDisplayString as _toDisplayString,
  openBlock as _openBlock,
  createElementBlock as _createElementBlock,
} from "/node_modules/.vite/deps/vue.js?v=6f26e7ed";

const _hoisted_1 = { class: "block" };
const _hoisted_2 = /*#__PURE__*/ _createElementVNode(
  "h1",
  null,
  "Block",
  -1 /* HOISTED */);

function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createElementBlock("div", _hoisted_1, [
      _hoisted_2,
      _createElementVNode(
        "span",
        null,
        _toDisplayString($setup.msg),
        1 /* TEXT */
      ),
    ])
  );
}

render函数中调用了3个函数,openBlock,createElementBlock,createElementVNode,通过这个三个函数收集动态子节点

// /packages/runtime-core/src/vnode.ts
// 存储currentBlock数组
export const blockStack: (VNode[] | null)[] = []
// 当前block
export let currentBlock: VNode[] | null = null
// 向blockStack推入currentBlock
export function openBlock(disableTracking = false) {
  blockStack.push((currentBlock = disableTracking ? null : []))
}

export function createElementBlock(  type: string | typeof Fragment,
  props?: Record<string, any> | null,
  children?: any,
  patchFlag?: number,
  dynamicProps?: string[],
  shapeFlag?: number
) {
  return setupBlock(
    createBaseVNode(
      type,
      props,
      children,
      patchFlag,
      dynamicProps,
      shapeFlag,
      true /* isBlock */    )
  )
}

function createBaseVNode(type, props = null, children = null,patchFlag = 0) {
  const vnode = {
    type,
    props,
    children,
    patchFlag,
    // ...  };
  return vnode;
}

function setupBlock(vnode: VNode) {
  // 在vnode上保留当前Block收集的动态子节点
  vnode.dynamicChildren =
    isBlockTreeEnabled > 0
    ? currentBlock || (EMPTY_ARR as any) : null
  return vnode
}

例子中的render函数执行后返回一个vnode对象,如下,有type,children,dynamicChildren,props等属性

将图中的vnode对象简化一下,

{
  type: "div",
  props: {
    class: "block",
  },
  children: [
    {
      type: "h1",
      children: "Block",
    },
    {
      type: "span",
      children: "vue",
    },
  ],
  dynamicChildren: [
    {
      type: "span",
      children: "vue",
    },
  ],
};

更新的时候,就会根据vnode中的数据进行diff, 在 vue3组件更新open in new window 这篇文章中,在组件更新逻辑中,组件的更新最终还是会走到对普通 DOM 元素的更新,

// /packages/runtime-core/src/renderer.ts
const patch = (n1, n2, container, anchor = null, parentComponent = null) => {
  const { type, ref, shapeFlag } = n2;
  switch (type) {
      if (shapeFlag & ShapeFlags.ELEMENT) {
        // 更新普通 DOM 元素
        processElement(n1, n2, container, anchor, parentComponent);
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        // 更新组件
        processComponent(n1, n2, container, anchor, parentComponent);
      }
  }
};

const processElement = (n1, n2, container, anchor, parentComponent) => {
  if (n1 == null) {
    // 挂载
  } else {
   // 更新
    patchElement(n1,n2,parentComponent,parentSuspense,isSVG,slotScopeIds,optimized);
  }
};

组件是抽象的普通Dom元素的集合,更新最终都会走到 patchElement 这个函数,

// /packages/runtime-core/src/renderer.ts
const patchElement = (  n1: VNode,
  n2: VNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  const el = (n2.el = n1.el!);
  let { patchFlag, dynamicChildren, dirs } = n2;

  if (dynamicChildren) {
    // 如果有dynamicChildren,只更新动态子节点
      } else if (!optimized) {
    // 全量更新所有子节点
    }

PatchFlag

vue2 对比节点时,不知道这个节点哪些信息发生了变化,只能依次对比这些信息,vue3中,收集了dynamicChildren,已经减少对比静态子节点了,但是,动态子节点有许多属性,配合使用patchFlag,就可以知道哪些属性需要更新,就可以实现靶向更新

vue3中patchFlag是包含一系列二进制操作值的枚举类型,

// /packages/shared/src/patchFlags.ts
export const enum PatchFlags {
  // 动态文本的元素
  TEXT = 1,             //0b0000001  1
  // 动态 class 的元素
  CLASS = 1 << 1,       //0b0000010  2
  // 动态 style 的元素
  STYLE = 1 << 2,       //0b0000100  4
  // 动态 props 的元素
  PROPS = 1 << 3,       //0b0001000  8
  // 动态props和有key值绑定的元素
  FULL_PROPS = 1 << 4,  //0b0010000  16
  // 静态节点
   HOISTED = -1,
  //...}

认识一下跟二进制相关的几个操作符:

左移操作符 (<<)open in new window,是将第一个操作数向左移动指定位数,左边超出的位数将会被清除,右边将会补零

按位与( &)open in new window运算符在两个操作数对应的二进位都为 1 时,该位的结果值才为 1

按位或(| )open in new window运算符在其中一个或两个操作数对应的二进制位为 1 时,该位的结果值为 1

patchFlag是在创建vnode的时候作为第四个参数传入,如下图

<template>
  <div class="block">
    <h1>Block</h1>
    <span>{{ msg }}</span>
  </div>
</template>

在 patchElement 对普通Dom元素进行更新的时候,就可以做到只对动态有变化的属性更新

Loading...