专业编程基础技术教程

网站首页 > 基础教程 正文

Vue3模版解析优化和手写优质render函数

ccvgpt 2024-08-03 12:41:50 基础教程 8 ℃

概要#

本文将分析Vue3在模板解析的时候具体做了哪几方面的优化以及在手写render函数的时候怎么处理会优先采用优化模式

BlockTree#

Vue3的模板解析性能优化的最大机制就是引入了BlockTree的概念(类似React的effectList),我们先看下Vue2在解析以下模版的时候是怎么做的

Vue3模版解析优化和手写优质render函数

  <div>
    <p>martin cai</p>
    <p>martin cai</p>
    <p>{{msg}}</p>
    <p>martin cai</p>
    <p>martin cai</p>
  </div>
 
        复制成功!
    

Vue2在依赖收集后更改msg变量触发派发更新的时候会对以上标签进行全量遍历,然后进行diff算法去逐个更新标签,这里其实除了msg是动态节点,其余的都是静态节点,全量对比只会增加无效的对比时间(即使Vue3优化了传统的diff算法),基于以上的性能缺陷,Vue3引入BlockTree的概念,下面正式介绍下BlockTree的工作原理是怎么样的?它是如何去解决这个性能缺陷?

主要是做了什么?#

BlockTree是由BlockNode组合而成的一棵树(BlockNode是一种特殊的vnode,是vnode下的一个标记)每个BlockNode下都有一个动态节点集合(结构是稳定的,稳定指的是集合里的元素数量顺序要求保持一致, 后面会讲到为什么需要稳定),Vue3在模版解析的时候会把动态节点推入到BlockNode下,最后在diff的时候会直接遍历动态节点集合,这样就避免了全量遍历的性能问题。

怎么做?#

首先告知大家的是每个组件的根节点都是一个BlockNode(这里注意的是Vue3虽然在开发时允许多个根节点,但其实最终都会被放到一个内置节点Fragment下, Fragment作为组件的根节点),然后我们还是以上面的例子来说明: 以下是模板转换render函数后的代码:

  function render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("div", null, [
      _createElementVNode("p", null, "martin cai"),
      _createElementVNode("p", null, "martin cai"),
      _createElementVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
      _createElementVNode("p", null, "martin cai"),
      _createElementVNode("p", null, "martin cai")
    ]))
  }
 
        复制成功!
    

我们可以看到openBlock、createElementBlock两个方法,重点就在于这两个方法,下面看下源码:

  function openBlock(disableTracking = false) {
    blockStack.push((currentBlock = disableTracking ? null : []));
  }
  function createElementBlock(type, props, children, patchFlag, dynamicProps, shapeFlag) {
    return setupBlock(createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, true /* isBlock */));
  }
 
        复制成功!
    

首先openBlock会在blockStack上添加一个新的blcok,并且赋值给currentBlock,currentBlock其实就是一个数组, 随后createElementBlock会先调用createBaseVNode创建vnode,随后将结果给到setupBlock,我们继续看下createBaseVNode和setupBlock代码:

  function createBaseVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, shapeFlag = type === Fragment ? 0 : 1 /* ELEMENT */, isBlockNode = false, needFullChildrenNormalization = false) {
    const vnode = {
      ... // 省略
      patchFlag,
      dynamicProps,
      dynamicChildren: null,
    };
    ... // 省略
    // track vnode for block tree
    if (isBlockTreeEnabled > 0 &&
        // avoid a block node from tracking itself
        !isBlockNode &&
        // has current parent block
        currentBlock &&
        // presence of a patch flag indicates this node needs patching on updates.
        // component nodes also should always be patched, because even if the
        // component doesn't need to update, it needs to persist the instance on to
        // the next vnode so that it can be properly unmounted later.
        (vnode.patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) &&
        // the EVENTS flag is only for hydration and if it is the only flag, the
        // vnode should not be considered dynamic due to handler caching.
        vnode.patchFlag !== 32 /* HYDRATE_EVENTS */) {
        currentBlock.push(vnode);
    }
    return vnode;
}
 
        复制成功!
    

createBaseVNode作用是创建一个vnode(这里省略了部分不在本次讨论的范围的代码),vnode下有patchFlag(性能优化关键字段,后面会讲到)、dynmaicProps、dynamicChildren(动态子节点集合)几个属性,在最后的时候会进行判断 1.isBlockTreeEnabled是否大于0(v-once指令编译后会使用到, 默认是1), isBlockNode就是createElementBlock里createBaseVNnode传入的最后一个值(这里是因为BlockNode本身也是一个动态节点,不能添加自己到动态节点集合里,后面会被添加到父级block下) 2. currentBlock就是当前的block集合(openBlock最新添加到blockStack的数组) 3. vnode.patchFlag这个等下会讲,例子中msg对应的动态节点会传入1,目前只需要知道大于0的都是动态节点,shapeFlag是表示Vnode的类型(128: Suspense, 64: Teleport, 4: StateFul_Component, 2: Functional_Component, 1: 原生普通节点等),这里用& 6 来识别是否是组件类型的vnode 4. 最后一个条件就是不是SSR 满足上面四个条件的情况下当前Vnode会被添加到currentBlock里, 此时就会收集到动态内容是msg的p标签的vnode 然后我们看下setupBlock的代码:

 function setupBlock(vnode) {
    // save current block children on the block vnode
    vnode.dynamicChildren =
        isBlockTreeEnabled > 0 ? currentBlock || EMPTY_ARR : null;
    // close block
    closeBlock();
    // a block is always going to be patched, so track it as a child of its
    // parent block
    if (isBlockTreeEnabled > 0 && currentBlock) {
        currentBlock.push(vnode);
    }
    return vnode;
} 
 
        复制成功!
    

在setupBlock里会被当前的currentBlock收集到的动态节点集合赋值给当前BlockVnode.dynamicChildren,然后调用closeBlock将当前block弹出,并且赋值currentBlock为blockStack最底部(也就是最新的block), 因为BlockNode本身也是一个动态节点,所以随后会判断是否有更早的BlockNode包裹着, 也就是blockStack是否已经空了,如果还有的话会把当前BlockNode也放到父级Block的dynamicChildren,这样就形成了一个BlockTree,最后在diff的时候会判断当前的节点是否是BlockNode(通过dynamicChildren是否存在),如果是的话就不会走传统的diff,而是直接diff dynamicChildren

v-if#

上面其实说到了BlockNode要求dynamicChildren的结构是稳定的,那么当遇到v-if指令的时候会出现结构不稳定的情况怎么办? 看以下例子:

  <div>
    <main v-if="show">
      <p>{{msg}}</p>
    </main>
    <div v-else>
      <section>
        <p>{{msg}}</p>
      </section>
    </div>
  </div>
 
        复制成功!
    

按照我们上面解析的过程, 最后v-if的p标签会被收集到组件的动态节点集合里, 那么在show变更为false的后, 虽然p标签还是会被渲染出来,但是v-else的明显多了个section标签,该标签不是一个动态节点并且也没有在上一次渲染的时候添加到了动态节点集合里,显然这里就会出现结构上的问题,所以按照上面的解析是有问题,在这里需要把v-if/v-else-if/v-else当作一个BlockNode去处理,首先看下通过模版解析后的render函数的样子:

function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    (_ctx.show)
      ? (_openBlock(), _createElementBlock("main", { key: 0 }, [
          _createElementVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
        ]))
      : (_openBlock(), _createElementBlock("div", { key: 1 }, [
          _createElementVNode("section", null, [
            _createElementVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
          ])
        ]))
  ]))
}
 
        复制成功!
    

我们可以发现render函数里通过三目运算符将v-if和v-else都解析成为里BlockNode,所以当运行到这些节点的时候会执行openBlock往blockStack里推入一个新的block集合,然后遇到动态节点会被添加到当前的block下,这里v-if的div vnode就会收集到p标签 v-else也同样会收集到p标签,但是不要忘记了,blockNode本身也是一个动态节点,并且每个blockNode是不同的(v-if 和 v-else) 的两个不同,此时Fragment.dynamicChildren是以下:

  // show为true的情况下
  [vnode] // vnode是main标签
  // show为false的情况下
  [vnode] // vnode是div标签
 
        复制成功!
    

在组件根节点Fragment.dynamicChildren diff的时候会判断相同索引下两个vnode是否一样,不一样的话直接重新走新的vnode的渲染

v-for#

v-for和v-if有些类似,但是情况会更加复杂点,因为它分两种情况:

当循环遍历的是一个变量m时#

  <ul>
    <li v-for="n in m" :key="n">
      {{n}}
    </li>
  </ul>
 
        复制成功!
    

我们看下render函数:

  render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("ul", null, [
      (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.m, (n) => {
        return (_openBlock(), _createElementBlock("li", { key: n }, _toDisplayString(n), 1 /* TEXT */))
      }), 128 /* KEYED_FRAGMENT */))
    ]))
  }
 
        复制成功!
    

在经过render解析后,我们可以发现

  1. v-for的根节点也是Fragment
  2. Fragment在openBlock的时候传入了一个true 如果不把v-for当作一个BlockNode,那么组件的Fragment节点的动态节点集合的结构就会随着m的变化而变化,这显然是会出问题的,所以Vue会将v-for的根节点Fragment变成一个openBlock, 这样是为了保证v-for在父级Block下的结构是稳定的,组件根节点Fragment下的dynamincChildren始终是一个[FragmentVnode]的结构,变化的还是FramgentVnode里的结构,虽然这样能够保证组件根节点的稳定,但是同样的问题还是会留到v-for根节点,所以在创建根节点时openBlock传入了一个true值,然后我们回顾到openBlock里可以发现此时currentBlock其实是一个null值, 那么在createBaseVNode的时候所有动态节点都不会被添加进来,也就意味着v-for根节点Fragment不会走优化后的diff,而是走传统的diff全量遍历子节点,但是Vue同时又会把v-for子节点当作blockNode去收集动态节点,这样其实只是在v-for直接子节点这里会全量遍历,非直接子节点还是会走优化后的diff

当循环遍历的是一个常量时#

  <ul>
    <li v-for="n in 10" :key="n">
      {{n}}
    </li>
  </ul>
 
        复制成功!
    

那这里的话显然结构是非常稳定的,那么v-for的Fragment会直接走优化后的diff,直接子节点不会被变成blockNode

ref#

看以下例子:

  <div ref="martinRef"></div>
 
        复制成功!
    
  setup() {
    const ref1 = ref(null)
    const ref2 = ref(null)
    const show = ref(true)

    return {
      martinRef: show.value ? ref1 : ref2
    }
  }
 
        复制成功!
    

看似div标签是一个静态节点,因为它所有的属性都是不变的,但是ref是一个比较特殊的props,它会在每次渲染后都会执行setRef函数去赋值ref的值到setupState里,然后看例子,我们可以发现martinRef的值会随着show的值发生变化而变化,那么此时就必须要保证div标签必须在每次diff的时候都被当作动态节点去执行,否则setRef就执行不了,也就意味这ref1和ref2的值不会随着show的值变化而发生变化,所以只要是绑定了ref值的标签都会被当作动态节点去处理

成为动态节点的条件#

  1. 动态的属性props
  2. 动态的content
  3. ref属性绑定的节点
  4. svg 或者 foreignObject标签
  5. slot插槽
  6. BlockNode(组件、v-if/v-else-if/v-else, v-for, 动态key值)

为什么要求BlockNode数据结构是稳定的?#

上面一致强调稳定的结构,原因是因为在diff的过程中,会直接取对应索引下的值进行对比,如果数量不同或者顺序不一致会导致渲染发生问题

静态提升#

默认在单文件sfc情况下会开启静态提升 --- hoistStatic 我们先看render函数,它在渲染之前都会被重新执行一次生成新的vnode,但是有很多是静态节点,它其实是不会发生任何变化的,所以它可以不用去重新生成一次,并且数量越多效果越好,hoistStatic就是将静态节点提取到render函数之外用变量表示,这样的话就可以有效减少vnode的生成时间,还是这个例子:

  <div>
    <p>martin cai</p>
    <p>martin cai</p>
    <p>{{msg}}</p>
    <p>martin cai</p>
    <p>martin cai</p>
  </div>
 
        复制成功!
    

开启静态提升后

  const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, "martin cai", -1 /* HOISTED */)
  const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, "martin cai", -1 /* HOISTED */)
  const _hoisted_3 = /*#__PURE__*/_createElementVNode("p", null, "martin cai", -1 /* HOISTED */)
  const _hoisted_4 = /*#__PURE__*/_createElementVNode("p", null, "martin cai", -1 /* HOISTED */)

  function render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("div", null, [
      _hoisted_1,
      _hoisted_2,
      _createElementVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
      _hoisted_3,
      _hoisted_4
    ]))
  }
 
        复制成功!
    

我们可以非常清晰的看见静态节点会被全部提取到render函数的外层,这样的话每次diff的时候就不需要去消耗多余的vnode创建时间,但是这里要注意的是ref属性,上面其实也讲到了,ref属性即使是静态的它还是会被归类为动态节点,所以这里ref是不会被提升的

属性提升#

  <div>
    <p>martin cai</p>
    <p>martin cai</p>
    <p martin="cai">{{msg}}</p>
    <p>martin cai</p>
    <p>martin cai</p>
  </div>
 
        复制成功!
    

我们可以发现第三个p标签虽然是动态节点,但是它的属性martin是一个静态属性,那么在转换的时候它也会被提升

  const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, "martin cai", -1 /* HOISTED */)
  const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, "martin cai", -1 /* HOISTED */)
  const _hoisted_3 = { martin: "cai" }
  const _hoisted_4 = /*#__PURE__*/_createElementVNode("p", null, "martin cai", -1 /* HOISTED */)
  const _hoisted_5 = /*#__PURE__*/_createElementVNode("p", null, "martin cai", -1 /* HOISTED */)

  function render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("div", null, [
      _hoisted_1,
      _hoisted_2,
      _createElementVNode("p", _hoisted_3, _toDisplayString(_ctx.msg), 1 /* TEXT */),
      _hoisted_4,
      _hoisted_5
    ]))
  }
 
        复制成功!
    

静态提升条件#

  1. 静态props(不包括ref)
  2. 静态节点
  3. v-pre和v-cloak指令的标签

预字符串化#

静态提升的作用已经在上面说过了,但其实在这其中还可以再优化下,我们知道vnode创建直到插入文档的效率是低于原生js方法的,毕竟任何封装框架都是基于原生语言基础实现的,所以当我们遇到一串连续的静态节点时,完全可以直接使用innerHTML方法直接插入到文档里,这样可以省去vnode带来的额外性能损耗

  <ul>
    <li></li> * 20
  </ul>
 
        复制成功!
    

假设我们拥有20个连续的静态li标签,它最终转换后的render函数:

  const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li><li></li>", 20)
  const _hoisted_21 = [
    _hoisted_1
  ]

  function render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("ul", null, _hoisted_21))
  }
 
        复制成功!
    

可以发现当有连续的静态节点的时候并不是一个一个去创建,而是直接使用createStaticNode的方式去创建,然后看下createStaticVNode的实现:

  function createStaticVNode(content, numberOfNodes) {
    // A static vnode can contain multiple stringified elements, and the number
    // of elements is necessary for hydration.
    const vnode = createVNode(Static, null, content);
    vnode.staticCount = numberOfNodes;
    return vnode;
  }
 
        复制成功!
    

可以发现内部其实还是调用createVNode,只是它的类型是Static, Static在开发环境下是Symbol('Static'), 生产环境下是Symbol(undefined),然后我们去看下StaticVnode的处理方式

  case Static:
    if (n1 == null) {
        mountStaticNode(n2, container, anchor, isSVG);
    }
    else if ((process.env.NODE_ENV !== 'production')) {
        patchStaticNode(n1, n2, container, isSVG);
    }
  // 我们纸刊mountStaticNode
  const mountStaticNode = (n2, container, anchor, isSVG) => {
    [n2.el, n2.anchor] = hostInsertStaticContent(n2.children, container, anchor, isSVG);
  };
  // hostInsertStaticContent就是insertStaticContent的别名
  function insertStaticContent(content, parent, anchor, isSVG) {
    // <parent> before | first ... last | anchor </parent>
    const before = anchor ? anchor.previousSibling : parent.lastChild;
    let template = staticTemplateCache.get(content);
    if (!template) {
        const t = doc.createElement('template');
        t.innerHTML = isSVG ? `<svg>${content}</svg>` : content;
        template = t.content;
        if (isSVG) {
            // remove outer svg wrapper
            const wrapper = template.firstChild;
            while (wrapper.firstChild) {
                template.appendChild(wrapper.firstChild);
            }
            template.removeChild(wrapper);
        }
        staticTemplateCache.set(content, template);
    }
    parent.insertBefore(template.cloneNode(true), anchor);
    return [
        // first
        before ? before.nextSibling : parent.firstChild,
        // last
        anchor ? anchor.previousSibling : parent.lastChild
    ];
  }
 
        复制成功!
    

我们直接看insertStaticContent方法,核心都在里面,可以发现静态节点模板也是被缓存在staticTemplateCache(Map)里,有缓存的话直接取缓存即可,没有的话,会在document下创建一个template标签,template标签是原生标签下一个不可视标签,它内部是一个文档碎片,但是它可以被js识别操作,所以这里会把静态模板直接通过innertHTML塞入到template标签下,然后获取到生成后的真实DOM塞回到对应的位置上 这里需要满足的条件是连续20个以上的静态节点

符合DOM attr规范的连续五个静态节点#

  <div class="martincai"></div>
  <div class="martincai"></div>
  <div class="martincai"></div>
  <div class="martincai"></div>
  <div class="martincai"></div>
 
        复制成功!
    
  const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<div class=\"martincai\"></div><div class=\"martincai\"></div><div class=\"martincai\"></div><div class=\"martincai\"></div><div class=\"martincai\"></div>", 5)

  function render(_ctx, _cache, $props, $setup, $data, $options) {
    return _hoisted_1
  }
 
        复制成功!
    

可以看到符合标题条件的也会被提升为静态节点集合

CacheEventHandler#

Vue3在创建Vnode的时候更改了事件传入的写法 Vue2:

  h(tag, {
    on: {
      eventName: handler
    },
    nativeOn: {
      eventName: handler
    }
  })
 
        复制成功!
    

Vue3:

  h(tag, {
    onEventName: handler
  })
 
        复制成功!
    

看过两版差异后,我们先来看下Vue在解析事件时会根据模版解析出不同的事件绑定

场景1#

  <div @click="handler">click me</div>
  <div @click="function handler() {}">click me</div>
  <div @click="() => handler() {}">click me</div>
 
        复制成功!
    
  render() {
    return createElementBlock('div', {
      onClick: 'handler | function handler(){} | () => handler() {}'
    }, 'click me')
  }
 
        复制成功!
    

类似以上写法的会被原样解析出来

场景2#

  <div @click="handler()">click me</div>
  <div @click="handler($event)">click me</div>
  <div @click="count++">click me</div>
 
        复制成功!
    
  render() {
    return createElementBlock('div', {
      onClick: ($event) => handler()
    }, 'click me')
  }
 
        复制成功!
    

类似以上写法会在外面覆盖一层函数用于代理$event

重复生成函数#

在上面场景2的情况下,我们可以发现每次渲染的时候都会将函数重新生成一遍,这就意味着平白无故的多了好几个性能消耗:

  1. 函数的创建时需要时间(比如复制当前作用域链到[[Scope]]下等等)
  2. GC回收原来的旧函数所占用的内存需要时间
  3. 最主要的是组件会因为Props的更新而更新 这几个会带来不必要的性能消耗问题,Vue3特意提供了CacheEventHandler功能来解决这个问题
  <div @click="handler()">click me</div>
 
        复制成功!
    
  render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("div", {
      onClick: _cache[0] || (_cache[0] = $event => (_ctx.handler()))
    }, "click me"))
  }
 
        复制成功!
    

我们可以看到Vue会将其缓存到render传入的cache里(这里的cache就是instance.renderCache,很多渲染上的缓存都会存储到这里,例如v-once指令),这样就可以避免上面提到的几个问题

PatchFlags#

以上都是Vue在模版解析的时候运用到的各个优化手段,但是由于模版语言本身的灵活性偏低,很多场景下需要手写render函数去做渲染,那么这里就需要去了解PatchFlags这个不对外暴露的性能指数API,首先看下它的枚举值有哪些:

  const PatchFlagNames = {
    [1 /* TEXT */]: `TEXT`, // text文字内容 diff
    [2 /* CLASS */]: `CLASS`, // class属性 diff
    [4 /* STYLE */]: `STYLE`, // style属性 diff
    [8 /* PROPS */]: `PROPS`, // 结合第五个参数dynamicProps做diff
    [16 /* FULL_PROPS */]: `FULL_PROPS`, // 全量diff props
    [32 /* HYDRATE_EVENTS */]: `HYDRATE_EVENTS`,
    [64 /* STABLE_FRAGMENT */]: `STABLE_FRAGMENT`, // 标志Fragment的结构是稳定的
    [128 /* KEYED_FRAGMENT */]: `KEYED_FRAGMENT`,
    [256 /* UNKEYED_FRAGMENT */]: `UNKEYED_FRAGMENT`,
    [512 /* NEED_PATCH */]: `NEED_PATCH`, // 标记patchFlag,进入到dynamicChildren下
    [1024 /* DYNAMIC_SLOTS */]: `DYNAMIC_SLOTS`, // 动态slots的更新需要用这个标志
    [2048 /* DEV_ROOT_FRAGMENT */]: `DEV_ROOT_FRAGMENT`,
    [-1 /* HOISTED */]: `HOISTED`,
    [-2 /* BAIL */]: `BAIL`
  };
 
        复制成功!
    

还记得上面说到的吗?patchFlag大于0的都是动态节点,动态节点是在每一次更新时都需要重新渲染一遍,这里的PatchFlags枚举其实是告诉Vue究竟是哪方面是需要重新渲染的,举个例子

  <div :class="className"></div>
 
        复制成功!
    
  function render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("div", {
      class: _normalizeClass(_ctx.className)
    }, null, 2 /* CLASS */))
  }
 
        复制成功!
    

这里我们可以发现class是一个动态props,它对应的patchFlag的值是2,那么我们可以发现它是作为createVNode的第四个参数传入,然后我们看下渲染时它是怎么去处理patchFlag的

  const patchElement = (...) => {
    ...
    if (patchFlag > 0) {
      // the presence of a patchFlag means this element's render code was
      // generated by the compiler and can take the fast path.
      // in this path old node and new node are guaranteed to have the same shape
      // (i.e. at the exact same position in the source template)
      if (patchFlag & 16 /* FULL_PROPS */) {
          // element props contain dynamic keys, full diff needed
          patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG);
      }
      else {
          // class
          // this flag is matched when the element has dynamic class bindings.
          if (patchFlag & 2 /* CLASS */) {
              if (oldProps.class !== newProps.class) {
                  hostPatchProp(el, 'class', null, newProps.class, isSVG);
              }
          }
          // style
          // this flag is matched when the element has dynamic style bindings
          if (patchFlag & 4 /* STYLE */) {
              hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG);
          }
          // props
          // This flag is matched when the element has dynamic prop/attr bindings
          // other than class and style. The keys of dynamic prop/attrs are saved for
          // faster iteration.
          // Note dynamic keys like :[foo]="bar" will cause this optimization to
          // bail out and go through a full diff because we need to unset the old key
          if (patchFlag & 8 /* PROPS */) {
              // if the flag is present then dynamicProps must be non-null
              const propsToUpdate = n2.dynamicProps;
              for (let i = 0; i < propsToUpdate.length; i++) {
                  const key = propsToUpdate[i];
                  const prev = oldProps[key];
                  const next = newProps[key];
                  // #1471 force patch value
                  if (next !== prev || key === 'value') {
                      hostPatchProp(el, key, prev, next, isSVG, n1.children, parentComponent, parentSuspense, unmountChildren);
                  }
              }
          }
      }
      // text
      // This flag is matched when the element has only dynamic text children.
      if (patchFlag & 1 /* TEXT */) {
          if (n1.children !== n2.children) {
              hostSetElementText(el, n2.children);
          }
      }
    }
    else if (!optimized && dynamicChildren == null) {
        // unoptimized, full diff
        patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG);
    }
    ...
  }
 
        复制成功!
    

可以看到在patchElement的时候,它会利用&去匹配对应的patchFlag对应的更改值,随后调用对应的处理函数去更新值,这里我们用的是class,所以会调用hostPatchProp(el, 'class', null, newProps.class, isSVG)函数,具体操作其实就是删除原来的class属性,然后重新赋值, 同理style属性也类似。

FullProps和Props#

这里再说下Full_Props和Props两个值,上面说到的Class和Style属性都属于Props的一种,如果抛开这两者我们需要重新更新其他的属性的时候就需要使用到Full_Props和Props两个值,首先说下Props, Props需要结合第五个参数(dynamicProps)来告诉Vue究竟是哪些值发生了变化,dynamicProps是propsKey的集合,例如

  <div @click="handler" :id="id"></div>
 
        复制成功!
    
  const _hoisted_1 = ["onClick", "id"]

  function render(_ctx, _cache, $props, $setup, $data, $options) {
    return (_openBlock(), _createElementBlock("div", {
      onClick: _ctx.handler,
      id: _ctx.id
    }, null, 8 /* PROPS */, _hoisted_1))
  }
 
        复制成功!
    

可以看到hoisted_1收集了两个属性onClick和id,它们作为第五个参数dynamicProps传入,在diff时会作为动态属性进行更新 然后再看Full_Props值,这个值其实就是将所有的Props全部作为动态属性进行更新,这些配置虽然会增加开发者的心智负担,但是它们共有一个好处就是可以成为动态节点来使用BlockTree的diff功能

NEED_Patch#

该属性会在ref属性里使用,前面我们也有提到ref属性无论是否是动态的,它都需要被作为动态节点,但是它又没有需要什么去更新的,所以设置一个NEED_Patch作为动态节点收集即可



Tags:

最近发表
标签列表