likes
comments
collection
share

Vue3源码分析(10)-block与patch解析

作者站长头像
站长
· 阅读数 40

本文介绍

  • 之前我们讲解了组件的初始化、组件的挂载、更新流程等。我们知道patch函数的主要作用是找出之前vnode与当前vnode的不同并进行渲染。本文我们跟着patch函数的流程继续讲解HTML元素的挂载流程与更新流程
  • 为了能流畅阅读文章,我们先简单说一下dynamicChildren与patchFlags

靶向更新标识(patchFlags)

  • 我们知道通过编译优化可以找到使用了变量的节点并编译生成特殊的函数去收集这些节点,最终放到dynamicChildren中,那么在更新组件的时候就不需要全量更新只需要更新使用了变量的节点即可。当然除了知道那些节点使用了变量还不够,例如我使用的变量是交给style属性的,对于这个节点而言没有使用其他的变量,那么是不是就只需要diff比较style属性就可以了,所以为了具体标识到底哪里使用了变量,产生了patchFlags

  • patchFlag:靶向更新标识。在编译阶段会判断当前的节点是否包含动态的props、动态style、动态class、Fragment是否稳定、当key属性是动态的时候需要全量比较props等。这样就可以在更新阶段判断patchFlag来实现靶向更新。比较特殊的有HOISTED:-1表示静态节点不需要diff(HMR的时候还是需要,用户可能手动直接改变静态节点)BAIL表示应该结束patch

  const PatchFlags = {
  DEV_ROOT_FRAGMENT: 2048,
  //动态插槽
  DYNAMIC_SLOTS: 1024,
  //不带key的Fragment
  UNKEYED_FRAGMENT: 256,
  //带key的Fragment
  KEYED_FRAGMENT: 128,
  //稳定的Fragment
  STABLE_FRAGMENT: 64,
  //带有监听事件的节点
  HYDRATE_EVENTS: 32,
  FULL_PROPS: 16, //具有动态:key,key改变需要全量比较
  PROPS: 8, //动态属性但不包含style class属性
  STYLE: 4, //动态的style
  CLASS: 2, //动态的class
  TEXT: 1, //动态的文本
  HOISTED: -1, //静态节点
  BAIL: -2, //表示diff应该结束
};
  • 我们再来看看哪些情况会产生不同的patchFlags。
<template>
  <Comp @click="a"></Comp> 
</template>

//编译后
function render(_ctx, _cache) {
  //在注册的组件中获取这个组件
  const _component_Comp = _resolveComponent("Comp");
  return (
    _openBlock(),
    _createBlock(_component_P, { onClick: _ctx.a }, null, 8 /* PROPS */, [
      "onClick",
    ])
  );
}
  • 当给组件传递参数后,那么patchFlag会被赋值为8,代表当前组件只需要比较props属性就可以了
<template>
  <div :class="{c}" :style="{b}"></div>
</template>

//编译后
function render(_ctx, _cache) {
  return (
    _openBlock(),
    _createElementBlock(
      "div",
      {
        class: _normalizeClass({ c: _ctx.c }),
        style: _normalizeStyle({ b: _ctx.b }),
      },
      null,
      6 /* CLASS, STYLE */
    )
  );
}
  • 同样当style、class属性为变量的时候会将patchFlag赋值为2+4=6
  • 这样我们就可以通过patchFlag知道当前具体需要进行哪些diff操作,减少了不必要的比较,提高了性能

收集dynamicChildren

  • 当然,仅仅依靠patchFlags还是不够的,我们还需要知道哪些是静态节点,哪些是使用了变量的动态节点。对于静态节点在更新组件的时候就无需关注了,进而只需要比较dynamicChildren中的节点。而实现这个的关键有两个部分,一个是编译部分,另一个是运行时部分。
  • 编译部分:我们需要遍历字符串识别:class、:style这样的字符串,最终的编译结果需要添加openBlock,createElementBlock等函数。
  • 运行时部分:因为在编译部分已经识别出了哪些组件是使用了变量的,而使用变量的组件会被编译为特殊的函数,但是这依然是字符串,所以收集函数的实现还需要在运行时中完成。

1.block

  • block节点:block节点代表的是使用了v-if、v-else-if、v-else、v-for这样的节点为block节点
  • 为什么要搞个block节点呢?对于带有v-if、v-for这样属性的节点我们无法判断当前需要渲染的节点有几个,可能有几个也可能不渲染则是0个。但是在dynamicChildren中进行diff是一一对应的,假设某个节点使用了v-if进行渲染那么这个节点将会进入dynamicChildren中,第二次渲染的时候这个节点不需要渲染了,对dynamicChildren进行比较的时候第一个将会和第二个进行对比,后面将会一步错步步错。所以block是用来确保当前一定是一个节点(尽管v-for、v-if会导致多个节点或0个节点)
  • openBlock:创建一个block并放入blockStack中。
let currentBlock = null
const blockStack = []
//如果openBlock(true)代表禁止追踪
//这个节点的所有子节点都不添加到dynamicChildren中
const openBlock = function(disableTracking=false){
  blockStack.push((currentBlock = disableTracking ? null : []));
}
  • closeBlock:currentBlock已经收集完了当前节点的所有动态节点。currentBlock指向父block
function closeBlock() {
  blockStack.pop();
  currentBlock = blockStack[blockStack.length - 1] || null;
}
  • setupBlock:将currentBlock收集到的所有动态节点赋值给vnode.dynamicChildren,如果当前block节点存在父block节点将当前vnode放入父block中。
function setupBlock(vnode) {
  vnode.dynamicChildren =
    isBlockTreeEnabled > 0 ? currentBlock || shared.EMPTY_ARR : null;
  closeBlock();
  if (isBlockTreeEnabled > 0 && currentBlock) {
    //当前block是父block,
    currentBlock.push(vnode);
  }
  return vnode;
}

2.不使用变量的节点

<template>
  <div></div>
</template>
//编译后
const _hoisted_1 = _createElementVNode("div",null,null, -1);
const _hoisted_2 = _createElementVNode("div",null,null, -1);
function render(_ctx, _cache) {
  return (
    _openBlock(),
    _createElementBlock(
      _Fragment,
      null,
      [_hoisted_1, _hoisted_2],
      64 /* STABLE_FRAGMENT */
    )
  );
}
  • 首先对于没有使用变量的节点则会被提取出来作为常量(_hoisted1、_hoisted2),然后创建vnode。下面我们来看看createElementVNode的实现。
const createElememntVNode = function(){
  const vnode = {/*省略所有属性*/}
  //省略大部分代码
  //判断是否加入dynamicChildren
  if (
    getBlockTreeEnabled() > 0 && //允许追踪
    !isBlockNode && //当前不是block
    getCurrentBlock() && //currentBlock存在
    //不是静态节点,或者是组件
    (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT)) 
  {
    //放入dynamicChildren
    getCurrentBlock().push(vnode);
  }
  return vnode;
}
  • 这里我们省略大部分代码,createElemenmtVNode除了需要生成vnode节点以外还需要判断当前生成的节点是否需要放入dynamicChildren中。
  • 条件一:当前必须是允许跟踪的状态。
  • 条件二:当前不是block节点,这个由传递给createElementVNode参数决定
  • 条件三:当前block存在。
  • 条件四:当前的vnode不是静态节点且是组件。
  • 显然对于在openBlock之外创建的节点(_hoisted1、_hoisted2)并不具备block,并且他们是静态节点且不是组件,所以他们并不会放到currentBlock中。所以不使用变量的节点将无法进入到dynamicChildren中。自然在比较的时候也就会跳过这些节点。

2.使用变量的节点

<template>
  <div v-if="a">
    <nav></nav>
    你好
  </div>
  <p>{{a}}</p>
</template>

//编译后
const hoisted_1 = { key: 0 }
const hoisted_2 = createElementVNode("nav", null, null, -1)
function render(ctx, cache) {
  return (openBlock(), createElementBlock(Fragment, null, [
    (ctx.a)
      ? (openBlock(), createElementBlock("div", hoisted_1, [
          hoisted_2,
          createTextVNode(" 你好 ")
        ]))
      : createCommentVNode("v-if", true),
    createElementVNode("p", null, _ctx.a, 1)
  ], 64))
  • 下面我们来分析一下动态节点是如何放入到currentBlock当中的。
  1. 对于hoisted节点是不会放入currentBlock中的。分析第一个openBlock,此时创建第一个block(调用openBlock的时候没有传递参数为true所以currentBlock=[]),放入到blockStack中。
currentBlock = []//当前currentBlock的情况
//当前block栈中存放了第一个block
blockStack=[currentBlock]
  1. 函数是从最内部开始调用的(假设a的值为true)。那么会再次调用openBlock。此时currentBlockblockStack的状态为:
beforeBlock = []
currentBlock = []
blockStack = [beforeBlock,currentBlock]
  1. 继续调用createTextVNode("你好"),createVNode最终还是会调用createElementVNode,所以会判断是否加入currentBlock中,这取决于是否满足上面说的四个条件,显然是满足的。
beforeBlock = []
currentBlock= [你好vnode]
blockStack = [beforeBlock,currentBlock]
  1. 调用createElementBlock。我们看看这个函数的实现。
//createElemenetVNode === createBaseVNode
function createElementBlock(
  //省略参数
) {
  return setupBlock(
    createBaseVNode(
      /*省略6个参数*/
      //第七个参数表示当前是block节点
      true
    )
  );
}
  • createElemenetVNode就是createBaseVNode他们是同一个函数。
  • 调用了setupBlock,这将会导致currentBlock被弹出blockStack中,同时弹出后bloackStack还有一个元素所以需要将当前vnode放入beforeBlock中。注意这里调用createBaseVNode传递了当前是block节点,那么当前节点就不会进入currentBlock中。
//调用createBaseVNode后
beforeBlock=[]
currentBlock=[你好vnode]
blockStack=[beforeBlock,currentBlock]
//调用setupBlock后
//完成了dynamicChildren的收集
divvnode.dynamicChildren = [你好vnode]
//弹出后 currentBlock指向beforeBlock
currentBlock = [divvnode]//被弹出的block
blockStack = [currentBlock]

5.调用createElementVNode("p"),这个节点需要添加到currentBlock中。

currentBlock = [divvnode,pvnode]
blockStack = [currentBlock]
  1. 继续调用createElementBlock(Fragment),先调用createBaseVNode再调用setupBlock
//调用createBaseVNode后什么都不改变
//因为当前节点是block节点且
//弹出后不在有元素不需要在添加
//调用setupBlock后
Fragmentvnode.dynamicChildren = [divvnode,pnode]
currentBlock = null
blockStack = []
  1. 最终的收集的结果
Fragmentvnode.dynamicChildren = [
  {
    dynamicChildren:[你好node],
  },
  pnode
]
  • 当我们更新组件的时候就可以直接通过dynamicChildren属性进行diff更新。便实现了靶向更新。

文本节点(processText)

//处理text
const processText = (beforeVnode, currentVNode, container, anchor) => {
  //挂载流程
  if (beforeVnode == null) {
    //将创建的Text节点插入container中
    hostInsert(
      (currentVNode.el = hostCreateText(currentVNode.children)),
      container,
      anchor
    );
  } 
  //更新流程
  else {
    //获取最新的el这里的el就是挂载流程
    //中调用hostCreateText创建的节点
    const el = (currentVNode.el = beforeVnode.el);
    if (currentVNode.children !== beforeVnode.children) {
      //重新设置文本内容
      hostSetText(el, currentVNode.children);
    }
  }
};
  • 如果是挂载流程,调用hostCreateText方法创建文本节点,并插入到容器中。
  • anchor:插入到container节点anchor节点之前。如果为anchor=null表示container无子节点,创建的文本节点作为第一个子节点插入。
  • 如果是更新流程,先判断先后的文本节点的值是否相同,不同才修改。
//修改文本节点的值
const hostSetText = (node,value) => node.nodeValue = value
//创建文本节点
const hostCreateText = (text) => document.createText(text)
//插入节点
const hostInsert = (child, parent, anchor) => {
  parent.insertBefore(chile,anchor||null)
}
  • 这里的方法都是通过runtime-dom中传递的,目的是为了适应不同的平台,runtime-core是实现与平台无关的运行时功能(即runtime-core中的节点渲染)。换句话说需要传递其他平台的insert等功能的实现,runtime-core依旧能运,实现了跨平台。

注释节点(processCommentNode)

const processCommentNode = function(
   beforeVnode, 
   currentVNode, 
   container, 
   anchor)
   {
    //挂载流程
    if (beforeVnode == null) {
      hostInsert(
        (currentVNode.el = hostCreateComment(currentVNode.children || "")),
        container,
        anchor
      );
    } 
    //更新流程
    else {
      currentVNode.el = beforeVnode.el;
    }
};
  • 如果是挂载流程,创建注释节点并插入即可。
  • 如果是更新流程,更新当前vnode的el属性,因为注释节点内容不会改变
//创建注释节点(runtime-dom负责实现)
const hostCreateComment = (comment) => document.createComment(comment)
  • 通过document(浏览器平台)实现的创建注释节点的方法。

片段节点(processFragment)

<template>
  <div>hello</div>
  <p>world</p>
</template>

//编译后
function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _hoisted_1,//div节点
    _hoisted_2,//p节点
  ])
}
  • 当组件模板返回多个子节点的时候需要在最外部包裹一层Fragment。当然这一层是依靠编译器实现的,用户无需关心,但是在patch的时候却需要处理。

1.挂载Fragment流程

const processFragment = (
  beforeVnode,
  currentVnode,
  container,//容器DOM
  anchor,
  parentComponent,
) => {
  //Fragment开始的锚点
  const fragmentStartAnchor = (currentVnode.el = beforeVnode
    ? beforeVnode.el
    : hostCreateText(""));
  //Fragment结束的锚点
  const fragmentEndAnchor = (currentVnode.anchor = beforeVnode
    ? beforeVnode.anchor
    : hostCreateText(""));
  let {
    patchFlag,
    dynamicChildren,
  } = currentVnode;
  //处理挂载流程
  if (beforeVnode == null) {
    //插入一个空的开始的text节点(你可在vnode.el访问到)
    hostInsert(fragmentStartAnchor, container, anchor);
    //插入一个空的结束的text节点(你可以在vnode.anchor访问到)
    hostInsert(fragmentEndAnchor, container, anchor);
    //一个fragment元素只能有数组children属性(如果是通过compiler编译所得)
    //将children挂载到fragmentEndAnchor元素之前
    mountChildren(
      currentVnode.children, //child 的vnode
      container, //父DOM
      //插入的锚点,所有的children都应该被
      //挂载到fragmentEndAnchor之前
      fragmentEndAnchor, 
      parentComponent, //最近的组件实例
    );
  }
  //处理更新逻辑
  else {}
};
Vue3源码分析(10)-block与patch解析
  • Fragment:相当于创建了两个子文本节点,一个永远占据父DOM的第一个子节点,一个永远占据父DOM的最后一个子节点。用户所写的子节点都需要渲染到这两个节点之间。简单的说规定了渲染边界
  • 由于Fragment的子节点可能有一个也可能有多个,所以调用mountChildren去挂载剩下的节点。具体的执行流程我们下面再分析。

2.更新Fragment流程

(1).含有dynamicChildren属性

if (
  //如果patchFlag>0代表当前的元素使用
  //了变量
  patchFlag > 0 &&
  //当前是稳定的fragment
  patchFlag & PatchFlags.STABLE_FRAGMENT &&
  //当前需要更新的节点中含有dynamicChildren
  dynamicChildren &&
  //之前也含有dynamicChildren
  beforeVnode.dynamicChildren
) {
  //靶向比较使用了变量的节点
  patchBlockChildren(
    beforeVnode.dynamicChildren,
    dynamicChildren,
    container,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds
  );
}
else {
  //省略第二部分代码
}
  • 对于使用了v-for指令的是没有dynamicChildren属性的,这是因为v-for指令渲染出的节点需要使用key去比较,如果节点的顺序发生了改变是需要进行调整的,而不是简单的一一对比。这一点我们可以看看源码是如何控制的。
<template>
  <p v-for="a in b">{{a}}</p>
</template>
//编译后
function render(ctx) {
  return (
    openBlock(true),
    createElementBlock(
      Fragment,
      null,
      renderList(_ctx.b, (a) => {
        return (
          openBlock(),
          createElementBlock("p",null,a,1)
        );
      }),
      256
    )
  );
}
  • 注意openBlock中传递了true这个参数,这表示当前节点禁止收集所有的子节点。这一点在开篇已经讲解过了。
  • 因此这里我们要分情况讨论,一个是含有dynamicChildren属性的,一个是不含有dynamicChildren属性的。对于含有dynamicChildren属性的,只需要比较dynamicChildren中所有的子节点就可以了。
  • patchBlockChildren:比较dynamicChildren中的节点。
const patchBlockChildren = (
  oldChildren,
  newChildren,
  fallbackContainer, //element
  parentComponent, //最近的父组件实例
) => {
  for (let i = 0; i < newChildren.length; i++) {
    const oldVNode = oldChildren[i];
    const newVNode = newChildren[i];
    //满足以下三个情况之一的,container为其el的父el
    //1.oldVnode是一个fragment标签
    //2.oldVnode与newVnode不是同一个标签(type或者key不同)
    //3.oldVnode是一个TELEPORT
    const container =
      oldVNode.el &&
      (oldVNode.type === Fragment ||
        !isSameVNodeType(oldVNode, newVNode) ||
        oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
        ? hostParentNode(oldVNode.el)
        : fallbackContainer;
    patch(
      oldVNode,
      newVNode,
      container,
      null,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      true
    );
  }
};
  • 遍历所有的dynamicChildren,分别调用patch函数进行比较。
  • 但是这里的contanier可能会不一样。
  • 情况一:如果之前的节点是Fragment类型,那么真实的container应该为父节点。这是因为Fragment实际上是一个空的文本节点,真实的父节点应该为Fragment的父节点
  • 情况二:新旧节点的key或者type不相同那么container也为父节点。这是因为typekey不同代表需要卸载之前的节点,所以container应该变为其父节点
  • 情况三:对于TELEPORT节点我们暂时不作讲解。但是他依然需要改变container为其父节点。

(2).不含有dynamicChildren属性

//此处是else分支中的代码
patchChildren(
  beforeVnode,
  currentVnode,
  container,
  fragmentEndAnchor,
  parentComponent,
  parentSuspense,
  isSVG,
  slotScopeIds,
  optimized
);
  • 因为没有dynamicChildren属性就需要对比节点本身了,调用了patchChildren方法全量比较。
  • 你可能有一个疑问,如果我不写变量那dynamicChildren属性不是也是空的吗?没错,但是这样你就没有设置响应式变量,所以不会发生更新。那你可能又会问,可以强制调用forceUpdate更新,这样做没有意义,而结果就是vue确实会全量比较,但是页面不会发生改变

(3).patchChildren

  • 这个函数包含三个大分支。调用patchChildren的函数只有处理Element、Fragment类型的处理函数。所以patchChildren处理的节点只能是ElementFragment
  • 分支一:patchFlag>0,这里主要是处理Fragment节点,含有key和不含key的。例如<div v-for="a in b"></div>这样的代码则是不含key的;<div v-for="a in b" :key="a"></div>这样的代码是含有key的。对于使用v-for指令的节点一定会包裹一层Fragment
const patchChildren = (
  beforeVnode,
  currentVNode,
  container,
  anchor,
) => {
  const c1 = beforeVnode && beforeVnode.children;
  const prevShapeFlag = beforeVnode ? beforeVnode.shapeFlag : 0;
  const c2 = currentVNode.children;
  const { patchFlag, shapeFlag } = currentVNode;
  if (patchFlag > 0) {
    //处理含有key的fragment<div v-for="a in b" :key="a"></div>
    if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
      patchKeyedChildren(
        c1,
        c2,
        container,
        anchor,
        parentComponent,
      );
      return;
    }
    //处理<div v-for="a in b"></div>
    else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
      patchUnkeyedChildren(
          c1,
          c2,
          container,
          anchor,
          parentComponent,
        );
      return;
    }
  }
  //分支二
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {}
  //分支三
  else {}
};
  1. 使用了v-for且含key属性:需要使用diff算法根据key找出所有子节点是否顺序发生改变、增加、减少。这个算法比较复杂下一章单独讲解。
  2. 使用了v-for但是没有key属性:没有key所以只能根据长度去增加或者减少节点。这个函数我们也放到下一章讲解。
  • 分支二:最新子节点是TEXT类型的。除了处理Fragment节点的函数会调用patchChildren,就只有处理Element节点的函数会调用patchChildren了,所以下面的分支都是处理Element的。
//分支二代码,处理新子节点是文本类型
//之前children是数组,现在是text 卸载之前的
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  unmountChildren(c1, parentComponent, parentSuspense);
}
//不相等则修改text
if (c2 !== c1) {
  hostSetElementText(container, c2);
}
  1. 如果之前子节点是数组类型的则卸载之前所有的节点,并且修改text
//之前的
<div>
  <p></p>
  <p></p>
</div>
//现在的
<div>hello</div>
  1. 如果之前子节点也是文本类型,那么不相等则修改。
<div>hello</div>//之前的
<div>h</div>//现在的
  • 分支三:新子节点是数组类型的情况。

//如果之前children是数组
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  //当前children也是数组,diff比较
  if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    patchKeyedChildren(
      c1,
      c2,
      container,
      anchor,
    );
  }
  //当前children不是数组,卸载之前的
  else {
    unmountChildren(c1, parentComponent, parentSuspense, true);
  }
}
//之前children不是数组
else {
  //之前children是text
  if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
    hostSetElementText(container, "");
  }
  //之前不是数组,现在是数组,挂载数组
  if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(
      c2,
      container,
      anchor,
      parentComponent,
    );
  }
}
  1. 之前和现在的子节点都是数组就需要diff比较得出节点的改变情况。如果现在是其他节点则卸载之前的所有子节点。
  2. 之前子节点是文本类型,现在子节点是数组类型,设置文本类型的值为空,然后重新挂载最新的子节点。

一般节点(processElement)

  • 这一部分代码大家知道什么意思就行了,不需要深究。
//patch中处理Element类型
const processElement = (
  beforeVNode,
  currentVNode,
  container,
  anchor,
) => {
  //挂载流程
  if (beforeVNode == null) {
    mountElement(
      currentVNode,
      container,
      anchor,
    );
  }
  //更新流程
  else {
    patchElement(
      beforeVNode,
      currentVNode,
    );
  }
};
  • 如果是第一次渲染则beforeVNode===null,挂载Element。否则为更新流程。

1.挂载Element流程

const mountElement = (
  vnode, //即将挂载的vnode
  container, //根容器id="app"
  anchor, //挂载应该为null直接插入到最后即可
  parentComponent
) => {
  let el; //当前标签对应的真实DOM
  const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode;
  //创建真实的标签
  el = vnode.el = hostCreateElement(
    vnode.type,
    isSVG,
    props && props.is,
    props
  );
  //如果children是文本节点
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    //el.textContent = vnode.children
    hostSetElementText(el, vnode.children);
  }
  //当前vnode的child是数组(多个孩子)
  else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(
      vnode.children,
      el, //container
      null
    );
  }
  //省略第二部分代码...
};
  • 根据type创建真实的DOM,如果当前处理节点的子节点是文本类型,那么直接设置。如果为数组类型,将子节点全部挂载到创建的DOM上。
//处理自定义指令
if (dirs) {
  //第一次挂载没有preVnode 故而为null
  invokeDirectiveHook(vnode, null, parentComponent, "created");
}
//挂载所有的props到DOM上
if (props) {
  for (const key in props) {
    if (key !== "value" && !shared.isReservedProp(key)) {
      hostPatchProp(
        el,
        key,
        null,
        props[key],
        false,//isSvg
        vnode.children,
        parentComponent
      );
    }
  }
  //对于value特殊处理(不传递后五个参数)
  if ("value" in props) {
    hostPatchProp(el, "value", null, props.value);
  }
}
//省略第三部分代码
  • 如果Element传递了指令参数(<div v-custom></div>),我们知道指令可以传递生命周期参数,在这里需要调用对应的created钩子。之后将所有的props属性挂载到DOM上。
//让DOM和vnode component 建立联系
{
  Object.defineProperty(el, "__vnode", {
    value: vnode,
    enumerable: false,
  });
  Object.defineProperty(el, "__vueParentComponent", {
    value: parentComponent,
    enumerable: false,
  });
}
//调用自定义指令的beforeMount
if (dirs) {
  invokeDirectiveHook(vnode, null, parentComponent, "beforeMount");
}
//插入el到anchor之前
hostInsert(el, container, anchor);
//因为是mounted所以需要在DOM更新后操作,放到
//vue的后置调度队列中
if (dirs) {
  queuePostRenderEffect(() => {
    dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted");
  }, parentSuspense);
}
  • 给创建的DOMcomponent、vnode建立关系,可以通过el直接访问到父组件与vnode。调用自定义指令的beforeMount钩子。插入DOM到指定位置。最后将自定义指令的mounted钩子的执行放到当前渲染任务完成之后。

2.更新ELement流程

const patchElement = (
  beforeVnode,
  currentVnode,
  parentComponent,
  parentSuspense,
  isSVG,
  slotScopeIds,
  optimized
) => {
  const el = (currentVnode.el = beforeVnode.el);
  let { patchFlag, dynamicChildren, dirs } = currentVnode;
  //父节点如果有动态的:key那么子节点也要继承全量比较
  patchFlag |= beforeVnode.patchFlag & PatchFlags.FULL_PROPS;
  const oldProps = beforeVnode.props || shared.EMPTY_OBJ;
  const newProps = currentVnode.props || shared.EMPTY_OBJ;
  let vnodeHook;
  //在beforeUpdate钩子中禁止递归收集副作用
  parentComponent && toggleRecurse(parentComponent, false);
  //调用自定义指令的beforeUpdate
  if (dirs) {
    invokeDirectiveHook(
      currentVnode,
      beforeVnode,
      parentComponent,
      "beforeUpdate"
    );
  }
  //取消禁止
  parentComponent && toggleRecurse(parentComponent, true);
  const areChildrenSVG = isSVG && currentVnode.type !== "foreignObject";
  //靶向更新
  if (dynamicChildren) {
    patchBlockChildren(
      beforeVnode.dynamicChildren,
      dynamicChildren,
      el,
      parentComponent,
      parentSuspense,
      areChildrenSVG,
      slotScopeIds
    );
  }
  //没有开启优化(没有dynamicChildren,HMR Update) 全量diff
  else if (!optimized) {
    patchChildren(
      beforeVnode,
      currentVnode,
      el,
      null,
      parentComponent,
      parentSuspense,
      areChildrenSVG,
      slotScopeIds,
      false
    );
  }
  //省略第二部分代码
};
  • 首先调用自定义指令的beforeeUpdate钩子,同时禁止递归收集副作用,为什么这样做在Vue3源码分析(9)中已经讲解过了。
  • 然后我们需要先更新子节点,如果含有dynamicChildren只需要靶向更新就可以了。否则全量比较。
//处理完了children 开始处理自己
if (patchFlag > 0) {
  //全量diff props 例如含有动态的key
  if (patchFlag & PatchFlags.FULL_PROPS) {
    patchProps(
      el,
      currentVnode,
      oldProps,
      newProps,
    );
  } else {
    //diff class
    if (patchFlag & PatchFlags.CLASS) {
      if (oldProps.class !== newProps.class) {
        hostPatchProp(el, "class", null, newProps.class, isSVG);
      }
    }
    //diff style
    if (patchFlag & PatchFlags.STYLE) {
      hostPatchProp(el, "style", oldProps.style, newProps.style, isSVG);
    }
    //diff props
    if (patchFlag & PatchFlags.PROPS) {
      const propsToUpdate = currentVnode.dynamicProps;
      for (let i = 0; i < propsToUpdate.length; i++) {
        const key = propsToUpdate[i];
        const prev = oldProps[key];
        const next = newProps[key];
        // 强制patch value
        if (next !== prev || key === "value") {
          hostPatchProp(
            el,
            key,
            prev,
            next,
            isSVG,
            beforeVnode.children,
            parentComponent,
            parentSuspense,
            unmountChildren
          );
        }
      }
    }
  }
  //修改TEXT
  if (patchFlag & PatchFlags.TEXT) {
    if (beforeVnode.children !== currentVnode.children) {
      hostSetElementText(el, currentVnode.children);
    }
  }
}
//省略第三部分代码
  • 简单的说这里就是对ELement节点的属性进行diff并进行修改。
if(patchFlag > 0){}
//不含有dynamicChildren,全量diff,patchFlag<=0
else if (!optimized && dynamicChildren == null) {
  //全量diff
  patchProps(
    el,
    currentVnode,
    oldProps,
    newProps,
    parentComponent,
    parentSuspense,
    isSVG
  );
}
//调用updated钩子
if (dirs) {
  queuePostRenderEffect(() => {
    dirs &&
      invokeDirectiveHook(
        currentVnode,
        beforeVnode,
        parentComponent,
        "updated"
      );
  });
}
  • 如果没有dynamicChildren属性依旧进行全量diff,最后调用自定义指令的updated钩子,当然这个任务会被放入调度器队列当中,将会在普通队列清空后在执行后置队列任务,这个时候渲染任务都已经完成了。

总结

  • 本文主要讲解了利用block收集dynamicChildren,实现靶向更新。但是如果使用了v-for指令就无法使用靶向更新了,因为需要根据key对比前后节点顺序是否发生改变。

  • 然后我们讲解了patch函数中对Comment、Fragment、Text、Element类型的节点的挂载以及更新流程。组件的挂载更新流程在前面的章节已经讲解完毕了。

  • 当然我们还差patchUnkeyedChildren、patchKeyedChildren还没有讲解,这将放到下一章节深入讲解,因为patchKeyedChildren的执行原理与diff算法相关比较复杂。

转载自:https://juejin.cn/post/7190409681116856377
评论
请登录