> 웹 프론트엔드 > 프런트엔드 Q&A > vue3 컴파일에서는 어떤 최적화가 이루어졌나요?

vue3 컴파일에서는 어떤 최적화가 이루어졌나요?

青灯夜游
풀어 주다: 2022-12-19 18:05:58
원래의
2993명이 탐색했습니다.

vue3 컴파일 최적화에는 다음이 포함됩니다. 1. 컴파일 프로세스 중에 동적 콘텐츠를 표시하기 위해 patchFlag가 도입되었으며, 다양한 속성 유형에 따라 다양한 라벨이 표시되므로 빠른 비교 알고리즘이 실현됩니다. 2. 블록 트리. 3. 정적 승격은 정적 노드 또는 속성을 승격시키는 것입니다. 4. 문자열화를 사전 구문 분석하는 경우 연속 정적 노드가 10개보다 많으면 정적 노드가 문자열로 직렬화됩니다. 5. 함수 캐싱; 캐시핸들러 옵션을 켜면 함수가 캐시되어 나중에 직접 사용할 수 있습니다.

vue3 컴파일에서는 어떤 최적화가 이루어졌나요?

이 튜토리얼의 운영 환경: windows7 시스템, vue3 버전, DELL G3 컴퓨터.

이 글에서는 주로 Vue3.0 编译阶段做的优化,在 patch 단계에서 비교 횟수를 줄이기 위해 이러한 최적화 전략을 사용하는 방법을 분석합니다. 다음 템플릿과 같이 구성 요소가 업데이트될 때 구성 요소의 전체 vnode 트리를 계속 순회해야 하기 때문에: vnode 树,比如下面这个模板:

<template>
  <div id="container">
    <p class="text">static text</p>
    <p class="text">static text</p>
    <p class="text">{{ message }}</p>
    <p class="text">static text</p>
    <p class="text">static text</p>
  </div>
</template>
로그인 후 복사

整个 diff 过程如图所示:

vue3 컴파일에서는 어떤 최적화가 이루어졌나요?

可以看到,因为这段代码中只有一个动态节点,所以这里有很多 diff 和遍历其实都是不需要的,这就会导致 vnode 的性能跟模版大小正相关,跟动态节点的数量无关,当一些组件的整个模版内只有少量动态节点时,这些遍历都是性能的浪费。对于上述例子,理想状态只需要 diff 这个绑定 message 动态节点的 p 标签即可。

Vue.js 3.0 通过编译阶段对静态模板的分析,编译生成了 Block tree

Block tree 是一个将模板基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,而且每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block treeVue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,这是一个非常大的性能突破。

PatchFlag

由于 diff 算法无法避免新旧虚拟 DOM 中无用的比较操作,Vue.js 3.0 引入了 patchFlag,用来标记动态内容。在编译过程中会根据不同的属性类型打上不同的标识,从而实现了快速 diff 算法。PatchFlags 的所有枚举类型如下所示:

export const enum PatchFlags {
  TEXT = 1, // 动态文本节点
  CLASS = 1 << 1, // 动态class
  STYLE = 1 << 2, // 动态style
  PROPS = 1 << 3, // 除了class、style动态属性
  FULL_PROPS = 1 << 4, // 有key,需要完整diff
  HYDRATE_EVENTS = 1 << 5, // 挂载过事件的
  STABLE_FRAGMENT = 1 << 6, // 稳定序列,子节点顺序不会发生变化
  KEYED_FRAGMENT = 1 << 7, // 子节点有key的fragment
  UNKEYED_FRAGMENT = 1 << 8, // 子节点没有key的fragment
  NEED_PATCH = 1 << 9, // 进行非props比较, ref比较
  DYNAMIC_SLOTS = 1 << 10, // 动态插槽
  DEV_ROOT_FRAGMENT = 1 << 11, 
  HOISTED = -1, // 表示静态节点,内容变化,不比较儿子
  BAIL = -2 // 表示diff算法应该结束
}
로그인 후 복사

Block Tree

vue3 컴파일에서는 어떤 최적화가 이루어졌나요?

左侧的 template 经过编译后会生成右侧的 render 函数,里面有 _openBlock_createElementBlock_toDisplayString_createElementVNode(createVnode) 等辅助函数。

let currentBlock = null
function _openBlock() {
  currentBlock = [] // 用一个数组来收集多个动态节点
}
function _createElementBlock(type, props, children, patchFlag) {
  return setupBlock(createVnode(type, props, children, patchFlag));
}

export function createVnode(type, props, children = null, patchFlag = 0) {
  const vnode = {
    type,
    props,
    children,
    el: null, // 虚拟节点上对应的真实节点,后续diff算法
    key: props?.["key"],
    __v_isVnode: true,
    shapeFlag,
    patchFlag 
  };
  ...

  if (currentBlock && vnode.patchFlag > 0) {
    currentBlock.push(vnode);
  }
  return vnode;
}

function setupBlock(vnode) {
  vnode.dynamicChildren = currentBlock;
  currentBlock = null;
  return vnode;
}

function _toDisplayString(val) {
  return isString(val)
    ? val
    : val == null
    ? ""
    : isObject(val)
    ? JSON.stringify(val)
    : String(val);
}
로그인 후 복사

此时生成的 vnode 如下:

vue3 컴파일에서는 어떤 최적화가 이루어졌나요?

此时生成的虚拟节点多出一个 dynamicChildren 属性,里面收集了动态节点 span

节点 diff 优化策略:

我们之前分析过,在 patch 阶段更新节点元素的时候,会执行 patchElement 函数,我们再来回顾一下它的实现:

const patchElement = (n1, n2) => { // 先复用节点、在比较属性、在比较儿子
  let el = n2.el = n1.el;
  let oldProps = n1.props || {}; // 对象
  let newProps = n2.props || {}; // 对象
  patchProps(oldProps, newProps, el);

  if (n2.dynamicChildren) { // 只比较动态元素
    patchBlockChildren(n1, n2);
  } else {
    patchChildren(n1, n2, el); // 全量 diff
  }
}
로그인 후 복사

我们在前面组件更新的章节分析过这个流程,在分析子节点更新的部分,当时并没有考虑到优化的场景,所以只分析了全量比对更新的场景。

而实际上,如果这个 vnode 是一个 Block vnode,那么我们不用去通过 patchChildren 全量比对,只需要通过 patchBlockChildren 去比对并更新 Block

const patchBlockChildren = (n1, n2) => {
  for (let i = 0; i < n2.dynamicChildren.length; i++) {
    patchElement(n1.dynamicChildren[i], n2.dynamicChildren[i])
  }
}
로그인 후 복사
tree 级别的比对,变成了线性结构比对。

我们来看一下它的实现:

const patchElement = (n1, n2) => { // 先复用节点、在比较属性、在比较儿子
  let el = n2.el = n1.el;
  let oldProps = n1.props || {}; // 对象
  let newProps = n2.props || {}; // 对象
  let { patchFlag, dynamicChildren } = n2
  
  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.FULL_PROPS) { // 对所 props 都进行比较更新
      patchProps(el, n2, oldProps, newProps, ...)
    } else {
      // 存在动态 class 属性时
      if (patchFlag & PatchFlags.CLASS) {
        if (oldProps.class !== newProps.class) {
          hostPatchProp(el, &#39;class&#39;, null, newProps.class, ...)
        }
      }
      // 存在动态 style 属性时
      if (patchFlag & PatchFlags.STYLE) {
        hostPatchProp(el, &#39;style&#39;, oldProps.style, newProps.style, ...)
      }
      
      // 针对除了 style、class 的 props
      if (patchFlag & PatchFlags.PROPS) {
        const propsToUpdate = n2.dynamicProps!
        for (let i = 0; i < propsToUpdate.length; i++) {
          const key = propsToUpdate[i]
          const prev = oldProps[key]
          const next = newProps[key]
          if (next !== prev) {
            hostPatchProp(el, key, prev, next, ...)
          }
        }
      }
      if (patchFlag & PatchFlags.TEXT) { // 存在动态文本
        if (n1.children !== n2.children) {
          hostSetElementText(el, n2.children as string)
        }
      } 
    } else if (dynamicChildren == null) {
      patchProps(el, n2, oldProps, newProps, ...)
    }
  }
}

function hostPatchProp(el, key, prevValue, nextValue) {
  if (key === &#39;class&#39;) { // 更新 class 
    patchClass(el, nextValue)
  } else if (key === &#39;style&#39;) { // 更新 style
    patchStyle(el, prevValue, nextValue)
  } else if (/^on[^a-z]/.test(key)) {  // events  addEventListener
    patchEvent(el, key, nextValue);
  } else { // 普通属性 el.setAttribute
    patchAttr(el, key, nextValue);
  }
}

function patchClass(el, nextValue) {
  if (nextValue == null) {
    el.removeAttribute(&#39;class&#39;); // 如果不需要class直接移除
  } else {
    el.className = nextValue
  }
}

function patchStyle(el, prevValue, nextValue = {}){
  ...
}

function patchAttr(el, key, nextValue){
  ...
}
로그인 후 복사

属性 diff 优化策略:

接下来我们看一下属性比对的优化策略:

<div>
  <span>hello</span> 
  <span a=1 b=2>{{name}}</span>
  <a><span>{{age}}</span></a>
</div>
로그인 후 복사
로그인 후 복사

总结: vue3 会充分利用 patchFlagdynamicChildren 做优化。如果确定只是某个局部的变动,比如 style 改变,那么只会调用 hostPatchProp 并传入对应的参数 style 做特定的更新(靶向更新);如果有 dynamicChildren,会执行 patchBlockChildren전체 diff 프로세스는 그림에 표시된 대로입니다. vue3 컴파일에서는 어떤 최적화가 이루어졌나요?

보실 수 있는 이유는 이 코드에는 동적 노드가 하나만 있으므로 여기에는 실제로 불필요한 많은 diff 및 순회가 있습니다. 이로 인해 vnode의 성능이 템플릿 크기와 직접 관련되며 동적 노드 수와는 아무런 관련이 없습니다. 노드 일부 구성 요소가 전체 템플릿에서 소수의 동적 노드만 포함하는 경우 이러한 순회는 성능 낭비입니다. 위 예의 경우 이상적으로는 바인딩된 메시지 동적 노드의 p 태그만 비교하면 됩니다. vue3 컴파일에서는 어떤 최적화가 이루어졌나요?

Vue.js 3.0 컴파일 단계에서 정적 템플릿 분석을 통해 블록 트리가 컴파일되고 생성됩니다. 🎜🎜블록 트리는 동적 노드 명령을 기반으로 템플릿을 자르는 중첩 블록입니다. 각 블록 내부의 노드 구조는 고정되어 있으며 각 블록에는 추적하는 데 하나의 배열만 필요합니다. 포함된 동적 노드. 블록 트리의 도움으로 🎜Vue.js는 템플릿의 전체 크기와 관련된 것에서 동적 콘텐츠의 양과 관련된 것으로 vnode 업데이트 성능을 향상시킵니다. 이는 매우 큰 성능 혁신입니다. . 🎜

🎜PatchFlag🎜

🎜diff 알고리즘은 이전 가상 DOM</code 및 새 가상 <code>DOM</code에서 쓸모없는 비교 작업을 피할 수 없기 때문에 >, < code>Vue.js 3.0에서는 동적 콘텐츠를 표시하기 위해 patchFlag를 도입했습니다. 컴파일 프로세스 중에 다양한 속성 유형에 따라 다양한 식별자가 표시되므로 빠른 diff 알고리즘이 실현됩니다. PatchFlags의 모든 열거형 유형은 다음과 같습니다: 🎜
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("span", null, "hello"),
    _createElementVNode("span", {
      a: "1",
      b: "2"
    }, _toDisplayString(_ctx.name), 1 /* TEXT */),
    _createElementVNode("a", null, [
      _createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
    ])
  ]))
}
로그인 후 복사
로그인 후 복사

🎜Block Tree🎜

🎜vue3 컴파일에서는 어떤 최적화가 이루어졌나요?🎜🎜왼쪽의 템플릿이 컴파일된 것입니다 그러면 오른쪽에 _openBlock, _createElementBlock, _toDisplayString, render 함수가 생성됩니다. >_createElementVNode< /code>(createVnode) 및 기타 보조 기능. 🎜
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "hello", -1 /* HOISTED */)
const _hoisted_2 = {
  a: "1",
  b: "2"
}

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _hoisted_1,
    _createElementVNode("span", _hoisted_2, _toDisplayString(_ctx.name), 1 /* TEXT */),
    _createElementVNode("a", null, [
      _createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
    ])
  ]))
}
로그인 후 복사
로그인 후 복사
🎜이번에 생성된 vnode는 다음과 같습니다.🎜🎜vue3 컴파일에서는 어떤 최적화가 이루어졌나요?🎜🎜이번에 생성된 가상 노드에는 동적 노드 span을 수집하는 추가 dynamicChildren 속성이 있습니다. 🎜

🎜노드 차이점 최적화 전략:🎜

🎜그 전에 패치 단계에서 노드 요소를 업데이트할 때 patchElement 함수를 실행한 후 구현을 검토해 보겠습니다. 🎜
<div>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
</div>
로그인 후 복사
로그인 후 복사
🎜 이 프로세스는 컴포넌트 업데이트의 이전 장에서 분석했는데, 당시에는 하위 노드 업데이트 부분을 분석할 때 최적화 시나리오를 고려하지 않았습니다. 우리는 전체 비교 및 ​​업데이트 시나리오를 분석했습니다. 🎜🎜실제로 이 vnodeBlock vnode라면 전체 비교를 위해 patchChildren을 거칠 필요는 없습니다. patchBlockChildren을 거쳐야 Block의 동적 하위 노드를 비교하고 업데이트할 수 있습니다. 트리 수준 비교에서 선형 구조 비교까지 성능이 크게 향상되었음을 확인할 수 있습니다. 🎜🎜구현을 살펴보겠습니다.🎜
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span>", 10)
const _hoisted_11 = [  _hoisted_1]

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, _hoisted_11))
}
로그인 후 복사
로그인 후 복사

🎜속성 차이 최적화 전략:🎜

🎜다음으로 속성 비교를 위한 최적화 전략을 살펴보겠습니다.🎜
<div @click="event => v = event.target.value"></div>
로그인 후 복사
로그인 후 복사
🎜요약: vue3는 최적화를 위해 patchFlagdynamicChildren을 최대한 활용합니다. style 변경과 같은 로컬 변경만 있는 것으로 판단되면 hostPatchProp만 호출되고 해당 매개변수 style는 특정 업데이트를 수행하기 위해 전달됩니다(🎜Targeted update🎜). dynamicChildren이 있으면 비교 및 ​​업데이트를 위해 patchBlockChildren이 실행됩니다. 매번 완전히 비교되고 업데이트되지는 않습니다🎜. 다이어그램은 다음과 같습니다: 🎜🎜🎜🎜

静态提升

静态提升是将静态的节点或者属性提升出去,假设有以下模板:

<div>
  <span>hello</span> 
  <span a=1 b=2>{{name}}</span>
  <a><span>{{age}}</span></a>
</div>
로그인 후 복사
로그인 후 복사

编译生成的 render 函数如下:

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("span", null, "hello"),
    _createElementVNode("span", {
      a: "1",
      b: "2"
    }, _toDisplayString(_ctx.name), 1 /* TEXT */),
    _createElementVNode("a", null, [
      _createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
    ])
  ]))
}
로그인 후 복사
로그인 후 복사

我们把模板编译成 render 函数是这个酱紫的,那么问题就是每次调用 render 函数都要重新创建虚拟节点。

开启静态提升 hoistStatic 选项后

const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "hello", -1 /* HOISTED */)
const _hoisted_2 = {
  a: "1",
  b: "2"
}

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _hoisted_1,
    _createElementVNode("span", _hoisted_2, _toDisplayString(_ctx.name), 1 /* TEXT */),
    _createElementVNode("a", null, [
      _createElementVNode("span", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
    ])
  ]))
}
로그인 후 복사
로그인 후 복사

预解析字符串化

静态提升的节点都是静态的,我们可以将提升出来的节点字符串化。 当连续静态节点超过 10 个时,会将静态节点序列化为字符串。

假如有如下模板:

<div>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
  <span>static</span>
</div>
로그인 후 복사
로그인 후 복사

开启静态提升 hoistStatic 选项后

const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span><span>static</span>", 10)
const _hoisted_11 = [  _hoisted_1]

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, _hoisted_11))
}
로그인 후 복사
로그인 후 복사

函数缓存

假如有如下模板:

<div @click="event => v = event.target.value"></div>
로그인 후 복사
로그인 후 복사

编译后:

const _hoisted_1 = ["onClick"]

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", {
    onClick: event => _ctx.v = event.target.value
  }, null, 8 /* PROPS */, _hoisted_1))
}
로그인 후 복사

每次调用 render 的时候要创建新函数,开启函数缓存 cacheHandlers 选项后,函数会被缓存起来,后续可以直接使用

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", {
    onClick: _cache[0] || (_cache[0] = event => _ctx.v = event.target.value)
  }))
}
로그인 후 복사

总结

以上几点即为 Vuejs 在编译阶段做的优化,基于上面几点,Vuejspatch 过程中极大地提高了性能。

【相关推荐:vuejs视频教程web前端开发

위 내용은 vue3 컴파일에서는 어떤 최적화가 이루어졌나요?의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿