vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue3组件挂载

源码浅析Vue3中的组件挂载

作者:沽汣

这篇文章主要带大家从源码分析一下Vue3中的组件挂载的相关知识,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的小伙伴可以跟随小编一起了解一下

前言

前面我们一起探讨了 Vue3 的 响应式原理编译过程,render函数我们已经拿到了,那具体到底要怎么用呢?这一节,我们就开启一个新篇章——组件的挂载

组件挂载/更新函数——setupRenderEffect

组件挂载和更新的核心函数都是setupRenderEffect,这里我们就从setupRenderEffect函数作为切入点:

const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
      const componentUpdateFn = () => {
        if (!instance.isMounted) {
          // 组件尚未挂载,执行挂载操作
          ...
        } else {
          // 组件已经挂载,执行更新操作
          ...
      };
      // 初始化响应式副作用函数
      const effect = instance.effect = new ReactiveEffect(
        componentUpdateFn,
        () => queueJob(update),
        instance.scope
      );
      const update = instance.update = () => effect.run();
      update.id = instance.uid;
      ...
      // 执行副作用函数
      update();
    };

我们可以看到在 setupRenderEffect 函数中:

可以看出,组件更新的核心就在于 componentUpdateFn 函数,接下来我们深入来看一下这个函数内部都执行了哪些操作。

componentUpdateFn

我们首先来看实例尚未挂载的情况下,componentUpdateFn函数是如何处理挂载的:

const componentUpdateFn = () => {
  // 组件尚未挂载,执行挂载操作
  if (!instance.isMounted) {
    let vnodeHook;
    const { el, props } = initialVNode;
    const { bm, m, parent } = instance;
    ...
    // 将实例上的allowRecurse属性(允许递归)设置为false
    toggleRecurse(instance, false);
    // 如果存在onBeforeMount生命周期函数
    if (bm) {
      // 执行onBeforeMount中的函数
      invokeArrayFns(bm);
    }
    ...
    // 将实例上的allowRecurse属性(允许递归)设置为true
    toggleRecurse(instance, true);
    if (el && hydrateNode) {
      ...
    } else {
     // 生成子树的vnode
      const subTree = (instance.subTree = renderComponentRoot(instance));
      // 挂载子树vnode到容器中
      patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
      initialVNode.el = subTree.el;
    }
    ...
    // 将实例上的isMounted属性设置为true
    instance.isMounted = true;
    initialVNode = container = anchor = null;
  } else {
    // 组件已经挂载过,执行更新操作
    ...
};

componentUpdateFn在处理组件挂载时主要做的事情就是:

接下来,我们进入renderComponentRoot函数,看一看生成子树vnode的整个过程是怎样的。

生成vnode的函数——renderComponentRoot

function renderComponentRoot( instance: ComponentInternalInstance ): VNode {
  ...
  let result
  ...
      const proxyToUse = withProxy || proxy
      // 取出render函数,并执行
      result = normalizeVNode(
        render!.call(
          proxyToUse,
          proxyToUse!,
          renderCache,
          props,
          setupState,
          data,
          ctx
        )
      )
  ...
  return result
}

上面我们抽离出该函数的核心,可以看到,renderComponentRoot函数的关键逻辑就是执行了render函数

前面章节中,我们已经介绍了render函数生成的过程,我们还用之前的例子:

模板template:

<div>
    <span> {{x}} </span>
    <div>123</div>
</div>

经过编译后,生成的render函数是这个样子的:

function render(_ctx, _cache) {
  with (_ctx) {
    const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
    return (_openBlock(), _createElementBlock("div", null, [
      _createElementVNode("span", null, _toDisplayString(x), 1 /* TEXT */),
      _hoisted_1
    ]))
  }
}

最终,我们通过执行上面的render函数,得到节点的虚拟DOM也就是vnode

然后,我们将这样的一个数据结构(vnode)传入normalizeVNode函数中做进一步的处理,我们看一下normalizeVNode函数又做了什么操作:

function normalizeVNode(child) {
    // 如果节点vnode为空,则创建为注释节点
    if (child == null || typeof child === "boolean") {
      return createVNode(Comment);
    } else if (isArray(child)) {
      // 如果节点为数组,则在外层包裹一层根节点fragment
      return createVNode(
        Fragment,
        null,
        child.slice()
      );
    } else if (typeof child === "object") {
      // 如果是对象形式
      return cloneIfMounted(child);
    } else {
      // 其他情况,比如创建文本类型的节点
      return createVNode(Text, null, String(child));
    }
}
function cloneIfMounted(child) {
    // 如果节点已经挂载,则直接返回对应的vnode,否则克隆一份返回
    return child.el === null && child.patchFlag !== -1 /* HOISTED */ || child.memo ? child : cloneVNode(child);
  }

在这个函数中会对传入的参数进行分情况讨论:

我们传入的 child参数 是一个对象形式,所以会最终执行的是cloneIfMounted函数,而这个函数中,会去判断 vnode节点 是否已经被挂载过,如果已经执行过挂载操作,那么其 vnodeel属性上就会被赋值,该函数就直接将原vnode节点返回,否则,执行拷贝操作再返回。

这里,因为的组件在该阶段还未挂载,所以normalizeVNode函数最终的返回结果也是直接将上面render函数生成的vnode直接返回,而我们最终renderComponentRoot函数的返回值同样也是执行render函数得到的vnode

至此,我们大概理清楚了生成子树vnode的函数renderComponentRoot的逻辑,它的主要工作就是通过执行模板编译后生成的render函数,再进行相应的处理,得到最终的vnode

挂载/更新函数patch

接下来我们开启下一个环节,也是vue中极其重要的一个函数——patch函数

patch直译过来就是“补丁”的意思,可以理解为在vue中,组件的挂载和更新都是通过打“补丁”的方式来进行的。当然,打“补丁”前要先比对一下,看看两个节点到底是哪些信息不一样了,然后再进行定点的更新。

在进入patch函数之前先说明一下patch函数的几个关键参数:

const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = isHmrUpdating ? false : !!n2.dynamicChildren) => {
      // 如果新旧vnode节点相同,则无须patch
      if (n1 === n2) {
        return;
      }
      // 如果新旧vnode节点,type类型不同,则直接卸载旧节点
      // 这里isSameVNodeType会判断规则为n1.type === n2.type && n1.key === n2.key
      if (n1 && !isSameVNodeType(n1, n2)) {
        anchor = getNextHostNode(n1);
        unmount(n1, parentComponent, parentSuspense, true);
        n1 = null;
      }
      ...
      const { type, ref: ref2, 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 (true) {
            patchStaticNode(n1, n2, container, isSVG);
          }
          break;
        // 处理Fragment
        case Fragment:
          // Fragment
          ...
          break;
        default:
          if (shapeFlag & 1 /* ELEMENT */) {
            // element类型
            processElement(
              n1,
              n2,
              container,
              anchor,
              parentComponent,
              parentSuspense,
              isSVG,
              slotScopeIds,
              optimized
            );
          } else if (shapeFlag & 6 /* COMPONENT */) {
            // 组件
            ...
          } else if (shapeFlag & 64 /* TELEPORT */) {
            // teleport 
            ...
          } else if (shapeFlag & 128 /* SUSPENSE */) {
            // suspense
            ...
          } else if (true) {
            warn2("Invalid VNode type:", type, `(${typeof type})`);
          }
      }
      ...
    };

patch函数整体的处理逻辑就是:

这里,我们就用处理element类型来举例,看一下processElement函数,其他情况同理。

processElement函数

const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
      isSVG = isSVG || n2.type === "svg";
      if (n1 == null) {
       // 如果存在旧节点
        mountElement(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        );
      } else {
        // 旧节点不存在
        patchElement(
          n1,
          n2,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        );
      }
    };

processElement函数,会根据旧节点是否存在进行分情况讨论,这里我们主要看挂载阶段的函数——mountElement

const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
      let el;
      let vnodeHook;
      const { type, props, shapeFlag, transition, dirs } = vnode;
      // 创建真实DOM结构,并将其保存在在vnode的el属性上
      el = vnode.el = hostCreateElement(
        vnode.type,
        isSVG,
        props && props.is,
        props
      );
      // 处理文本节点
      if (shapeFlag & 8 /* TEXT_CHILDREN */) {
        hostSetElementText(el, vnode.children);
      } else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
        // 如果节点类型是数组,则递归的对子节点进行处理
        mountChildren(
          vnode.children,
          el,
          null,
          parentComponent,
          parentSuspense,
          isSVG && type !== "foreignObject",
          slotScopeIds,
          optimized
        );
      }
      // 处理vnode上的指令相关内容,并执行指令的生命周期钩子函数
      if (dirs) {
        invokeDirectiveHook(vnode, null, parentComponent, "created");
      }
      setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent);
      // 处理props相关内容
      if (props) {
        for (const key in props) {
          if (key !== "value" && !isReservedProp(key)) {
            hostPatchProp(
              el,
              key,
              null,
              props[key],
              isSVG,
              vnode.children,
              parentComponent,
              parentSuspense,
              unmountChildren
            );
          }
        }
        if ("value" in props) {
          hostPatchProp(el, "value", null, props.value);
        }
        if (vnodeHook = props.onVnodeBeforeMount) {
          invokeVNodeHook(vnodeHook, parentComponent, vnode);
        }
      }
      ...
      // 处理vnode上的指令相关内容,并执行指令的生命周期钩子函数
      if (dirs) {
        invokeDirectiveHook(vnode, null, parentComponent, "beforeMount");
      }
      ...
      // 将dom挂载到container
      hostInsert(el, container, anchor);
      ...
    };

mountElement函数中,我们终于创建出了期待已久的真实DOM结构。该函数的逻辑为:

根据vnode创建出真实DOM结构,并保存在el属性上。

根据子节点类型来进行不同的操作:

对vnode的指令以及props内容进行处理。

最后将生成的DOM挂载到容器container上,也就最终呈现在页面上了。

这里可能有的朋友就是想看一看document.createElement这种API到底在哪里,那就提一下hostCreateElement函数

hostCreateElement函数在源码中是通过解构赋值并重命名得来的,它原来的名字叫createElement,改回本名瞬间就直观了很多~

createElement: (tag, isSVG, is, props) => {
  const el = isSVG ? doc.createElementNS(svgNS, tag) : doc.createElement(tag, is ? { is } : void 0);
  if (tag === "select" && props && props.multiple != null) {
    ;
    el.setAttribute("multiple", props.multiple);
  }
  return el;
}
// hostInsert指向的函数就是insert
insert: (child, parent, anchor) => {
  parent.insertBefore(child, anchor || null);
},

两个工具函数的逻辑也很好理解,就不再多说了吧。总之,终于是看到了document.createElement就是舒服了^_^

最后

这一节,我们深入研究了Vue3中,组件的挂载逻辑,整个的流程虽然过程繁琐,但要做的事比较清晰,总结下来就是:

到此这篇关于源码浅析Vue3中的组件挂载的文章就介绍到这了,更多相关Vue3组件挂载内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文