在上一篇文章中,咱们分析了在编译过程静态节点的提高。而且,在文章的结尾也说了,下一篇文章将会介绍 patch
过程。javascript
提及「Vue3」的 patch
过程,其中最为津津乐道的就是靶向更新。靶向更新,顾名思义,即更新的过程是带有目标性的、直接性的。而,这也是和静态节点提高同样,是「Vue3」针对 VNode
更新性能问题的一大优化。vue
那么,今天,咱们就来揭秘「Vue3」compile 和 runtime 结合的 patch
过程 到底是如何实现的!java
提及「Vue3」的 patch
,老生常谈的就是 patchFlag
。因此,对于 shapeFlag
我想你们可能有点蒙,这是啥?node
ShapeFlag
顾名思义,是对具备形状的元素进行标记,例如普通元素、函数组件、插槽、keep alive
组件等等。它的做用是帮助 Rutime 时的 render
的处理,能够根据不一样 ShapeFlag
的枚举值来进行不一样的 patch
操做。数组
在「Vue3」源码中 ShapeFlag
和 patchFlag
同样被定义为枚举类型,每个枚举值以及意义会是这样:缓存
了解过「Vue2.x」源码的同窗应该知道第一次 patch
的触发,就是在组件建立的过程。只不过此时,oldVNode
为 null
,因此会表现为挂载的行为。所以,在认知 pathc
的过程以前,不可或缺地是咱们须要知道组件是怎么建立的?微信
既然说 patch
的第一次触发会是组件的建立过程,那么在「Vue3」中组件的建立过程会是怎么样的?它会经历这么三个过程:svg
在以前,咱们讲过 compile
编译过程会将咱们的 template
转化为可执行代码,即 render
函数。而,compiler
生成的 render
函数会绑定在当前组件实例的 render
属性上。例如,此时有这样的 template
模板:函数
<div><div>hi vue3</div><div>{{msg}}</div></div>
它通过 compile
编译处理后生成的 render
函数会是这样:源码分析
const _Vue = Vue const _hoisted_1 = _createVNode("div", null, "hi vue3", -1 /* HOISTED */) function render(_ctx, _cache) { with (_ctx) { const { createVNode: _createVNode, toDisplayString: _toDisplayString, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue return (_openBlock(), _createBlock(_Fragment, null, [ _createVNode("div", null, [ _hoisted_1, _createVNode("div", null, _toDisplayString(msg), 1 /* TEXT */) ]) ])) } }
这个 render
函数真正执行的时机是在安装全局的渲染函数对应 effect
的时候,即 setupRenderEffect
。而渲染 effect
会在组件建立时和更新时触发。
这个时候,可能又有同窗会问什么是 effect
?effect
并非「Vue3」的新概念,它的本质是「Vue2.x」源码中的 watcher
,一样地,effect
也会负责依赖收集和派发更新。
有兴趣了解「Vue3」依赖收集和派发更新过程的同窗能够看一下这篇文章 4k+ 字分析 Vue 3.0 响应式原理(依赖收集和派发更新)
而 setupRenderEffect
函数对应的伪代码会是这样:
function setupRenderEffect() { instance.update = effect(function componentEffect() { // 组件未挂载 if (!instance.isMounted) { // 建立组件对应的 VNode tree const subTree = (instance.subTree = renderComponentRoot(instance)) ... instance.isMounted = true } else { // 更新组件 ... } }
能够看到,组件的建立会命中 renderComponentRoot(instance)
的逻辑,此时 renderComponentRoot(instance)
会调用 instance
上的 render
函数,而后为当前组件实例构造整个 VNode Tree
,即这里的 subTree
。renderComponentRoot
函数对应的伪代码会是这样:
function renderComponentRoot(instance) { const { ... render, ShapeFlags, ... } = instance if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { ... result = normalizeVNode( render!.call( proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx ) ) ... } }
能够看到,在 renderComponentRoot
中,若是当前 ShapeFlags
为 STATEFUL_COMPONENT
时会命中调用 render
的逻辑。这里的 render
函数,就是上面咱们所说的 compile
编译后生成的可执行代码。它最终会返回一个 VNode Tree
,它看起来会是这样:
{ ... children: (2) [{…}, {…}], ... dynamicChildren: (2) [{…}, {…}], ... el: null, key: null, patchFlag: 64, ... shapeFlag: 16, ... type: Symbol(Fragment), ... }
以靶向更新为例,了解过的同窗应该知道,它的实现离不开 VNode Tree
上的 dynamicChildren
属性,dynamicChildren
则是用来承接整个 VNode Tree
中的全部动态节点, 而标记动态节点的过程又是在 compile
编译的 transform
阶段,能够说是环环相扣,因此,这也是咱们常说的「Vue3」Runtime
和 Compile
的巧妙结合。
显然在「Vue2.x」是不具有构建 VNode
的 dynamicChildren
属性的条件。那么,「Vue3」又是如何生成的 dynamicChildren
?
Block VNode
是「Vue3」针对靶向更新而提出的概念,它的本质是动态节点对应的 VNode
。而,VNode
上的 dynamicChildren
属性则是衍生于 Block VNode
,所以,它也就是充当着靶向更新中的靶的角色。
这里,咱们再回到前面所提到的 compiler
编译时生成 render
函数,它返回的结果:
(_openBlock(), _createBlock(_Fragment, null, [ _createVNode("div", null, [ _hoisted_1, _createVNode("div", null, _toDisplayString(msg), 1 /* TEXT */) ]) ]))
须要注意的是openBlock
必须写在createBlock
以前,由于在Block Tree
中的Children
老是会在createBlock
以前执行。
能够看到有两个和 Block
相关的函数:_openBlock()
和 _createBlock()
。实际上,它们分别对应着源码中的 openBlock()
和 createBlock()
函数。那么,咱们分别来认识一下这二者:
openBlock
会为当前 Vnode
初始化一个数组 currentBlock
来存放 Block
。openBlock
函数的定义十分简单,会是这样:
function openBlock(disableTracking = false) { blockStack.push((currentBlock = disableTracking ? null : [])); }
openBlock
函数会有一个形参 disableTracking
,它是用来判断是否初始化 currentBlock
。那么,在什么状况下不须要建立 currentBlock
?
当存在 v-for
造成的 VNode
时,它的 render
函数中的 openBlock()
函数形参 disableTracking
就是 true
。由于,它不须要靶向更新,来优化更新过程,即它在 patch
时会经历完整的 diff
过程。
换个角理解,为何这么设计?靶向更新的本质是为了从一颗存在动态、静态节点的 VNode Tree
中筛选出动态的节点造成 Block Tree
,即 dynamicChildren
,而后在 patch
时实现精准、快速的更新。因此,显然 v-for
造成的 VNode Tree
它不须要靶向更新。
这里,你们可能还会有一个疑问,为何建立好的Block VNode
又被push
到了blockStack
中?它又有什么做用?有兴趣的同窗能够去试一下v-if
场景,它最终会构造一个Block Tree
,有兴趣的同窗能够看一下这篇文章 Vue3 Compiler 优化细节,如何手写高性能渲染函数
createBlock
则负责建立 Block VNode
,它会调用 createVNode
方法来依次建立 Block VNode
。createBlock
函数的定义:
function createBlock(type, props, children, patchFlag, dynamicProps) { const vnode = createVNode(type, props, children, patchFlag, dynamicProps, true); // 构造 `Block Tree` vnode.dynamicChildren = currentBlock || EMPTY_ARR; closeBlock(); if (shouldTrack > 0 && currentBlock) { currentBlock.push(vnode); } return vnode; }
能够看到在 createBlock
中仍然会调用 createVNode
建立 VNode
。而 createVNode
函数本质上调用的是源码中的 _createVNode
函数,它的类型定义看起来会是这样:
function _createVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props: (Data & VNodeProps) | null = null, children: unknown = null, patchFlag: number = 0, dynamicProps: string[] | null = null, isBlockNode = false ): VNode {}
当咱们调用 _createVNode()
建立 Block VNode
时,须要传入的 isBlockNode
为 true
,它用来标识当前 VNode
是否为 Block VNode
,从而避免 Block VNode
依赖本身的状况发生,即就不会将当前 VNode
加入到 currentBlock
中。其对应的伪代码会是这样:
function _createVNode() { ... if ( shouldTrack > 0 && !isBlockNode && currentBlock && patchFlag !== PatchFlags.HYDRATE_EVENTS && (patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) ) { currentBlock.push(vnode) } ... }
因此,只有知足上面的 if
语句中的全部条件的 VNode
,才能做为 Block Node
,它们对应的具体含义会是这样:
v-once
指令下的 VNode
。Block Node
。Block Node
,对于 v-for
场景下,curretBlock
为 null
,它不须要靶向更新。32
事件监听,只有事件监听状况时事件监听会被缓存。Block Node
,这是为了保证下一个 VNode
的正常卸载。至于,再深一层次探索为何?有兴趣的同窗能够自行去了解。
讲完 VNode
的建立过程,我想你们都会意识到一点,若是使用手写 render
函数的形式开发,咱们就须要对 createBlock
、openBlock
等函数的概念有必定的认知。由于,只有这样,咱们写出的 render
函数才能充分利用好靶向更新过程,实现的应用更新性能也是最好的。
前面,咱们也说起了 patch
是组件建立和更新的最后一步,有时候它也会被称为 diff
。在
「Vue2.x」中它的 patch
过程会是这样:
VNode
间的比较,判断这两个新旧 VNode
是否属于同一个引用,是则不进行后续比较,不是则对比每一级的 VNode
。VNode
的首尾,循环条件为头指针索引小于尾指针索引。VNode
的当前匹配成功的真实 DOM
移动到对应新 VNode
匹配成功的位置。VNode
中的真实 DOM
节点插入到旧 VNode
的对应位置中,即,此时是建立旧 VNode
中不存在的 DOM
节点。VNode
的 children
不存在为止。粗略一看,就能明白「Vue2.x」patch
是一个硬比较的过程。因此,这也是它的缺陷所在,没法合理地处理大型应用状况下的 VNode
更新。
虽然「Vue3」的 patch
没有像 compile
同样会从新命名一些例如 baseCompile
、transform
阶段性的函数。可是,其内部的处理相对于「Vue2.x」变得更为智能。
它会利用 compile
阶段的 type
和 patchFlag
来处理不一样状况下的更新,这也能够理解为是一种分而治之的策略。其对应的伪代码会是这样:
function patch(...) { if (n1 && !isSameVNodeType(n1, n2)) { ... } if (n2.patchFlag === PatchFlags.BAIL) { ... } const { type, ref, shapeFlag } = n2 switch (type) { case Text: processText(n1, n2, container, anchor) break case Comment: processCommentNode(n1, n2, container, anchor) break case Static: if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG) } else if (__DEV__) { patchStaticNode(n1, n2, container, isSVG) } break case Fragment: processFragment(...) break default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement(...) } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent(...) }else if (shapeFlag & ShapeFlags.TELEPORT) { ;(type as typeof TeleportImpl).process(...) } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ;(type as typeof SuspenseImpl).process(...) } else if (__DEV__) { warn('Invalid VNode type:', type, `(${typeof type})`) } }
能够看到,除开文本、静态、文档碎片、注释等 VNode
会根据 type
处理。默认状况下,都是根据 shapeFlag
来处理诸如组件、普通元素、Teleport
、Suspense
组件等。因此,这也是为何文章开头会介绍 shapeFlag
的缘由。
而且,从 render
阶段建立 Block VNode
到 patch
阶段根据特定 shapeFlag
的不一样处理,在必定程度上,shapeFlag
具备和 patchFlag
同样的价值!
这里取其中一种状况,当 ShapeFlag
为 ELEMENT
时,咱们来分析一下 processElement
是如何处理 VNode
的 patch
的。
一样地 processElement
会处理挂载的状况,即 oldVNode
为 null
的时候。processElement
函数的定义:
const processElement = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) => { isSVG = isSVG || (n2.type as string) === 'svg' if (n1 == null) { mountElement( n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else { patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized) } }
其实,我的认为oldVNode
改成n1
、newVNode
改成n2
,这命名是否有点仓促?
能够看到,processElement
在处理更新的状况时,实际上会调用 patchElement
函数。
patchElement
会处理咱们所熟悉的 props
、生命周期、自定义事件指令等。这里,咱们不会一一分析每一种状况会发生什么。咱们就以文章开头提的靶向更新为例,它是如何处理的?
其实,对于靶向更新的处理非常简单,即若是此时 n2
(newVNode
) 的 dynamicChildren
存在时,直接"梭哈",一把更新 dynamicChildren
,不须要处理其余 VNode
。它对应的伪代码会是这样:
function patchElement(...) { ... if (dynamicChildren) { patchBlockChildren( n1.dynamicChildren!, dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG ) ... } ... }
因此,若是 n2
的 dynamicChildren
存在时,则会调用 patchBlockChildren
方法。而,patchBlockChildren
方法其实是基于 patch
方法的一层封装。
patchBlockChildren
会遍历 newChildren
,即 dynamicChildren
来处理每个同级别的 oldVNode
和 newVNode
,以及它们做为参数来调用 patch
函数。以此类推,不断重复上述过程。
const patchBlockChildren: PatchBlockChildrenFn = ( oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, isSVG ) => { for (let i = 0; i < newChildren.length; i++) { const oldVNode = oldChildren[i] const newVNode = newChildren[i] const container = oldVNode.type === Fragment || !isSameVNodeType(oldVNode, newVNode) || oldVNode.shapeFlag & ShapeFlags.COMPONENT || oldVNode.shapeFlag & ShapeFlags.TELEPORT ? hostParentNode(oldVNode.el!)! : fallbackContainer patch( oldVNode, newVNode, container, null, parentComponent, parentSuspense, isSVG, true ) } }
你们应该会注意到,此时还会获取当前 VNode
须要挂载的容器,由于 dynamicChildren
有时候会是跨层级的,并非此时的 VNode
就是它的 parent
。具体会分为两种状况:
1. oldVNode 的父节点做为容器
oldVNode
的类型为文档碎片时。oldVNode
和 newVNode
不是同一个节点时。shapeFlag
为 teleport
或 component
时。2. 初始调用 patch 的容器
patch
方法传入的根 VNode
的挂载点做为容器。具体每一种状况为何须要这样处理,讲起来又将是 长篇大论,预计会放在下一篇文章中和你们见面。
原本初衷是想化繁为简,没想到最后仍是写了 3k+ 的字。由于,「Vue3」将 compile
和 runtime
结合运用实现了诸多优化。因此,已经不可能出现如「Vue2.x」同样分析 patch
只须要关注 runtime
,不须要关注在这以前的 compile
作了一些奠基基调的处理。所以,文章总会不可避免地有点晦涩,这里建议想加深印象的同窗能够结合实际栗子单步调式一番。
从零到一,带你完全搞懂 vite 中的 HMR 原理(源码分析)
经过阅读,若是你以为有收获的话,能够爱心三连击!!!
微信公众号: Code center