Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue(v3.0.11)源码简析之render对象的相关实现(1) #20

Open
unproductive-wanyicheng opened this issue May 14, 2021 · 0 comments
Open

Comments

@unproductive-wanyicheng
Copy link
Owner

全局唯一的render对象,提供了patch方法,可以支持任意类型的vnode映射到实际的宿主dom元素中,且支持挂载和更新;更新也实现了全量对比和优化对比2种方式,也用到了最长递增子序列算法,可谓是vue中最终把虚拟dom的信息实打实的同步到实际dom中的最终实现者,看下它们是如何实现的:

// dev模式下 创建响应式effect render函数时候的默认配置参数
function createDevEffectOptions(instance) {
  return {
      // trigger触发后原函数被推入job队列中 并不马上重新执行
      scheduler: queueJob,
      allowRecurse: true,
      // 2个钩子
      onTrack: instance.rtc ? e => invokeArrayFns(instance.rtc, e) : void 0,
      onTrigger: instance.rtg ? e => invokeArrayFns(instance.rtg, e) : void 0
  };
}
// 推入post队列 注意组件实例父链上存在Suspense的场景即可
const queuePostRenderEffect = queueEffectWithSuspense
  ;
// 组件patch挂载dom完成之后 组件实例和vnode都已经是最新的稳定状态了 我们可以在后序遍历的最后调用这个方法
// 去更新父组件对子组件的ref属性中的引用信息了
const setRef = (rawRef, oldRawRef, parentSuspense, vnode) => {
  // 文档中有写 ref是个数组是对v-for和ref同时存在的情况
  if (isArray(rawRef)) {
      // 遍历逐一处理即可
      rawRef.forEach((r, i) => setRef(r, oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef), parentSuspense, vnode));
      return;
  }
  // 待更新新ref值 父组件需要更新的引用值
  let value;
  // 卸载的vnode 父组件对这个子组件的ref为null
  if (!vnode) {
      // means unmount
      value = null;
  }
  // 异步包装组件不作处理 在异步组件内部 实际的子组件会继承这个ref 从而让父组件可以找到这个实际的ref指向的组件
  else if (isAsyncWrapper(vnode)) {
      // when mounting async components, nothing needs to be done,
      // because the template ref is forwarded to inner component
      return;
  }
  // 组件ref 指向组件实例
  else if (vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */) {
      value = vnode.component.exposed || vnode.component.proxy;
  }
  // 指向元素
  else {
      value = vnode.el;
  }
  // 父组件咯
  const { i: owner, r: ref } = rawRef;
  if (!owner) {
      warn(`Missing ref owner context. ref cannot be used on hoisted vnodes. ` +
          `A vnode with ref must be created inside the render function.`);
      return;
  }
  const oldRef = oldRawRef && oldRawRef.r;
  const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs;
  // setupState见后文的分析 这里同步更新就好了
  const setupState = owner.setupState;
  // unset old ref
  // 移除旧引用
  if (oldRef != null && oldRef !== ref) {
      if (isString(oldRef)) {
          refs[oldRef] = null;
          if (hasOwn(setupState, oldRef)) {
              setupState[oldRef] = null;
          }
      }
      else if (isRef(oldRef)) {
          oldRef.value = null;
      }
  }
  // 更新新引用
  if (isString(ref)) {
      // 设置更新job
      const doSet = () => {
          refs[ref] = value;
          if (hasOwn(setupState, ref)) {
              setupState[ref] = value;
          }
      };
      // #1789: for non-null values, set them after render
      // null values means this is unmount and it should not overwrite another
      // ref with the same key
      if (value) {
          // 以最高优先级推入post队列中 render之后去更新ref
          doSet.id = -1;
          queuePostRenderEffect(doSet, parentSuspense);
      }
      else {
          doSet();
      }
  }
  // 文档描述的ref还有第二种值的形式
  else if (isRef(ref)) {
      // 分析同上
      const doSet = () => {
          ref.value = value;
      };
      if (value) {
          doSet.id = -1;
          queuePostRenderEffect(doSet, parentSuspense);
      }
      else {
          doSet();
      }
  }
  // 第三种 函数 执行就可以了
  else if (isFunction(ref)) {
      callWithErrorHandling(ref, owner, 12 /* FUNCTION_REF */, [value, refs]);
  }
  else {
      warn('Invalid template ref type:', value, `(${typeof value})`);
  }
};
/**
* The createRenderer function accepts two generic arguments:
* HostNode and HostElement, corresponding to Node and Element types in the
* host environment. For example, for runtime-dom, HostNode would be the DOM
* `Node` interface and HostElement would be the DOM `Element` interface.
*
* Custom renderers can pass in the platform specific types like this:
*
* ``` js
* const { render, createApp } = createRenderer<Node, Element>({
*   patchProp,
*   ...nodeOps
* })
* ```
*/
function createRenderer(options) {
  return baseCreateRenderer(options);
}
// Separate API for creating hydration-enabled renderer.
// Hydration logic is only used when calling this function, making it
// tree-shakable.
function createHydrationRenderer(options) {
  return baseCreateRenderer(options, createHydrationFunctions);
}
// 直接看下面的具体基础render的实现就好了
// 负责把vnode渲染成真实的dom树并且挂载到宿主dom中去的render对象究竟是如何实现的
// implementation
function baseCreateRenderer(options, createHydrationFns) {
  // 先忽略
  {
      const target = getGlobalThis();
      target.__VUE__ = true;
      setDevtoolsHook(target.__VUE_DEVTOOLS_GLOBAL_HOOK__);
  }
  // 这些个方法都是浏览器下DOM操作的函数简单封装 实现见后文 目前只需要关注它可以实现实际的DOM操作即可
  const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, forcePatchProp: hostForcePatchProp, createElement: hostCreateElement, createText: hostCreateText, createComment: hostCreateComment, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, setScopeId: hostSetScopeId = NOOP, cloneNode: hostCloneNode, insertStaticContent: hostInsertStaticContent } = options;
  // Note: functions inside this closure should use `const xxx = () => {}`
  // style in order to prevent being inlined by minifiers.
  // 高级调用API 任意形式的 vnode都可以被这个方法调用完成渲染过程
  // 所以它内部调用了很多其他基础方法 控制逻辑也很复杂
  /**
   * 参数很多:
   * 1. n1 旧vnode 2. n2 新vnode 3. container 待插入的宿主DOM元素 4. parentComponent 插入时候参考的宿主DOM的锚点位置
   * 5. parentComponent 父组件实例对象 6. parentSuspense 父组件链条中存在的最近一个Suspense对象
   * 7. isSVG是否是svg元素
   * 8. slotScopeIds TODO
   * 9. optimized TODO
   */
  const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = false) => {
      // patching & not same type, unmount old tree
      // 前后vnode的类型发生了改变 直接先无脑移除第一个就是了
      if (n1 && !isSameVNodeType(n1, n2)) {
          // 找到原vnode节点挂载的dom的后一个元素作为锚点位置
          anchor = getNextHostNode(n1);
          // 执行挂载vnode逻辑
          unmount(n1, parentComponent, parentSuspense, true);
          n1 = null;
      }
      // 关闭优化模式 需要全量对比
      if (n2.patchFlag === -2 /* BAIL */) {
          optimized = false;
          n2.dynamicChildren = null;
      }
      // 取出判断vnode类型的 type和辅助flag vnode所属的ref信息
      const { type, ref, shapeFlag } = n2;
      // 分清楚调用其他基础方法去实现
      switch (type) {
          // 可以直接处理的基础类型 text node
          case Text:
              processText(n1, n2, container, anchor);
              break;
          // 可以直接处理的基础类型 comment node
          case Comment:
              processCommentNode(n1, n2, container, anchor);
              break;
          // 可以直接处理的基础类型 一段html内容直接插入
          case Static:
              if (n1 == null) {
                  mountStaticNode(n2, container, anchor, isSVG);
              }
              else {
                  patchStaticNode(n1, n2, container, isSVG);
              }
              break;
          // 一种辅助模拟节点 代表有多个子节点的情况 实际并不反映在最终的dom结构中
          // 只会把子节点们插入到目标dom中
          case Fragment:
              processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
              break;
          default:
              // 普通元素vnode
              if (shapeFlag & 1 /* ELEMENT */) {
                  processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
              }
              // 用户和大部分内置组件对象的vnode
              else if (shapeFlag & 6 /* COMPONENT */) {
                  processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
              }
              // 特殊的内置组件vnode TELEPORT 调用自己实现的 process
              else if (shapeFlag & 64 /* TELEPORT */) {
                  type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals);
              }
              // 特殊的内置组件vnode SUSPENSE 调用自己实现的 process
              else if (shapeFlag & 128 /* SUSPENSE */) {
                  type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals);
              }
              else {
                  warn('Invalid VNode type:', type, `(${typeof type})`);
              }
      }
      // set ref
      // 上面的更新完成后 最终完成父组件中关于新vnode的引用更新
      if (ref != null && parentComponent) {
          setRef(ref, n1 && n1.ref, parentSuspense, n2);
      }
  };
  // text node 更新比较简单 往对应的宿主dom中插入text node或者更新node内容即可
  const processText = (n1, n2, container, anchor) => {
      if (n1 == null) {
          hostInsert((n2.el = hostCreateText(n2.children)), container, anchor);
      }
      else {
          const el = (n2.el = n1.el);
          if (n2.children !== n1.children) {
              hostSetText(el, n2.children);
          }
      }
  };
  // 插入注释node即可
  const processCommentNode = (n1, n2, container, anchor) => {
      if (n1 == null) {
          hostInsert((n2.el = hostCreateComment(n2.children || '')), container, anchor);
      }
      else {
          // there's no support for dynamic comments
          n2.el = n1.el;
      }
  };
  // 插入一段html内容 对应v-html 注意返回值是插入元素列表的 首尾元素
  const mountStaticNode = (n2, container, anchor, isSVG) => {
      [n2.el, n2.anchor] = hostInsertStaticContent(n2.children, container, anchor, isSVG);
  };
  /**
   * Dev / HMR only
   */
  // 更新之前的v-html内容
  const patchStaticNode = (n1, n2, container, isSVG) => {
      // static nodes are only patched during dev for HMR
      if (n2.children !== n1.children) {
          // 取出后面的锚点
          const anchor = hostNextSibling(n1.anchor);
          // remove existing
          // 移除之前的v-html插入的内容
          removeStaticNode(n1);
          // 插入新的 更新引用
          [n2.el, n2.anchor] = hostInsertStaticContent(n2.children, container, anchor, isSVG);
      }
      // 更新引用
      else {
          n2.el = n1.el;
          n2.anchor = n1.anchor;
      }
  };
  // 移动一段之前的html节点列表到指定dom中去
  const moveStaticNode = ({ el, anchor }, container, nextSibling) => {
      let next;
      // 从前到后一个一个插在nextSibling的前面即可
      while (el && el !== anchor) {
          next = hostNextSibling(el);
          hostInsert(el, container, nextSibling);
          el = next;
      }
      // 最后一个也插入 因为anchor是列表中最后一个
      hostInsert(anchor, container, nextSibling);
  };
  // 删除一段html节点内容
  const removeStaticNode = ({ el, anchor }) => {
      let next;
      // 一个一个删除
      while (el && el !== anchor) {
          next = hostNextSibling(el);
          hostRemove(el);
          el = next;
      }
      hostRemove(anchor);
  };
  // 处理元素vnode
  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);
      }
  };
  // 挂载vnode到宿主dom中
  const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
      let el;
      let vnodeHook;
      // 取出vnode上的属性变量对象们 准备构建最终的元素了
      const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode;
      {
          // 注意第三个参数 is 代表自定义dom元素的情况 props参数另有它用 具体下面的注释
          // 目前只需要知道 创建了目标tag元素节点就可以了
          el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is, props);
          // mount children first, since some props may rely on child content
          // being already rendered, e.g. `<select value>`
          // 递归出口 节点的文字内容需要更新 基础情况
          if (shapeFlag & 8 /* TEXT_CHILDREN */) {
              // 赋值更新dom元素text内容即可
              hostSetElementText(el, vnode.children);
          }
          // 存在子节点数组 哪怕只有一个 也被视为需要递归去处理
          // 采用深度递归 优先处理子节点 把当前el当做子节点的宿主dom元素 后续遍历完成后最后再插入父节点到最终的宿主dom中
          else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
              // 对于列表节点 需要额外遍历处理 调用 mountChildren 帮忙
              mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', slotScopeIds, optimized || !!vnode.dynamicChildren);
          }
          // 运行时指令 钩子 created 触发 :元素的创建后执行
          if (dirs) {
              invokeDirectiveHook(vnode, null, parentComponent, 'created');
          }
          // props
          // props中的属性们 也需要一一映射到实际的元素上了
          if (props) {
              for (const key in props) {
                  if (!isReservedProp(key)) {
                      // 调用 hostPatchProp 也就是 patchProp 来完成
                      hostPatchProp(el, key, null, props[key], isSVG, vnode.children, parentComponent, parentSuspense, unmountChildren);
                  }
              }
              // 内置保留的6个vnode钩子之一 onVnodeBeforeMount 触发
              if ((vnodeHook = props.onVnodeBeforeMount)) {
                  invokeVNodeHook(vnodeHook, parentComponent, vnode);
              }
          }
          // scopeId
          // 存在scopeId的话 设置到元素上去
          setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent);
      }
      // dom元素保持2个引用指向vnode和父组件实例 方面之后取出来对比
      {
          Object.defineProperty(el, '__vnode', {
              value: vnode,
              enumerable: false
          });
          Object.defineProperty(el, '__vueParentComponent', {
              value: parentComponent,
              enumerable: false
          });
      }
      // 运行时指令 钩子 beforeMount 触发 :元素的未挂载到宿主父元素之前执行
      if (dirs) {
          invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount');
      }
      // #1583 For inside suspense + suspense not resolved case, enter hook should call when suspense resolved
      // #1689 For inside suspense + suspense resolved case, just call it
      // 具体场景有待Suspense的分析 不过从注释来看 是 Suspense 组件的渲染结果已确定之后
      // transition包裹下的内容需要触发动画效果
      const needCallTransitionHooks = (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
          transition &&
          !transition.persisted;
      // 调用 transition.beforeEnter 具体分析见transition组件
      if (needCallTransitionHooks) {
          transition.beforeEnter(el);
      }
      // 完成当前元素的插入到宿主dom中 完成dom操作
      hostInsert(el, container, anchor);
      // 下面3个钩子 在el实际插入后都需要触发
      // 我们需要和组件的生命周期钩子区分开 组件的生命周期指的往往是组件实例所处哪个阶段
      // 而vnode钩子 指令钩子 transition钩子都是针对el元素讨论的
      if ((vnodeHook = props && props.onVnodeMounted) ||
          needCallTransitionHooks ||
          dirs) {
          // 3个方法作为一个job一起入列
          queuePostRenderEffect(() => {
              vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode);
              needCallTransitionHooks && transition.enter(el);
              dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted');
          }, parentSuspense);
      }
  };
  // 设置作用域id
  const setScopeId = (el, vnode, scopeId, slotScopeIds, parentComponent) => {
      // 单个
      if (scopeId) {
          hostSetScopeId(el, scopeId);
      }
      // 从前往后依次覆盖
      if (slotScopeIds) {
          for (let i = 0; i < slotScopeIds.length; i++) {
              hostSetScopeId(el, slotScopeIds[i]);
          }
      }
      // 存在父组件对象的vnode
      if (parentComponent) {
          let subTree = parentComponent.subTree;
          if (subTree.patchFlag > 0 &&
              subTree.patchFlag & 2048 /* DEV_ROOT_FRAGMENT */) {
              // 特殊情况 片段中只有一个有效子vnode 更新子树根节点
              subTree =
                  filterSingleRoot(subTree.children) || subTree;
          }
          // 这个条件只有HOC高阶组件才满足 继承id 具体原因见其他高阶组件的分析 TODO
          if (vnode === subTree) {
              const parentVNode = parentComponent.vnode;
              setScopeId(el, parentVNode, parentVNode.scopeId, parentVNode.slotScopeIds, parentComponent.parent);
          }
      }
  };
  // 看下存在多个子vnode的情况如何初次挂载
  const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, slotScopeIds, start = 0) => {
      for (let i = start; i < children.length; i++) {
          // 对child还做了格式化处理
          // 具体实现见vnode的分析章节
          const child = (children[i] = optimized
              ? cloneIfMounted(children[i])
              : normalizeVNode(children[i]));
          // 可以看到 确实是深度遍历 递归调用高级方法patch去处理
          patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized, slotScopeIds);
      }
  };
  // 来看如何对比2个元素vnode
  const patchElement = (n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
      // 同一个dom元素 不同的话之前已经考虑过了 能到这里来说明就是一个
      const el = (n2.el = n1.el);
      let { patchFlag, dynamicChildren, dirs } = n2;
      // #1426 take the old vnode's patch flag into account since user may clone a
      // compiler-generated vnode, which de-opts to FULL_PROPS
      patchFlag |= n1.patchFlag & 16 /* FULL_PROPS */;
      // 取出新旧props对象
      const oldProps = n1.props || EMPTY_OBJ;
      const newProps = n2.props || EMPTY_OBJ;
      let vnodeHook;
      // 触发vnode BeforeUpdate钩子
      if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
          invokeVNodeHook(vnodeHook, parentComponent, n2, n1);
      }
      // 触发指令 beforeUpdate钩子
      if (dirs) {
          invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate');
      }
      // 全量对比 这3个参数的值值得注意
      if (isHmrUpdating) {
          // HMR updated, force full diff
          patchFlag = 0;
          optimized = false;
          dynamicChildren = null;
      }
      // 之前编译过程得到的一些辅助标志位 可以帮忙指示我们如何处理更新
      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
              // 动态prop key 全量对比props对象
              patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG);
          }
          // 处理正常的key的props
          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
                  // 编译过程把动态props值对应的key都提前提取出来了已经
                  const propsToUpdate = n2.dynamicProps;
                  for (let i = 0; i < propsToUpdate.length; i++) {
                      const key = propsToUpdate[i];
                      const prev = oldProps[key];
                      const next = newProps[key];
                      // 根据情况 更新prop hostForcePatchProp指的是value prop
                      if (next !== prev ||
                          (hostForcePatchProp && hostForcePatchProp(el, key))) {
                          hostPatchProp(el, key, prev, next, isSVG, n1.children, parentComponent, parentSuspense, unmountChildren);
                      }
                  }
              }
          }
          // text
          // This flag is matched when the element has only dynamic text children.
          // 更新node的text内容
          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);
      }
      const areChildrenSVG = isSVG && n2.type !== 'foreignObject';
      // 可以看到 元素更新对比 是先序处理 因为被删除的旧节点 根本不需要更新 直接转为unmoutn和mount新节点的情况即可
      // 内容会改变的动态vnode都会收集在一个block的顶级vnode的dynamicChildren中
      // 优化模式下 我们可以只对比更新这里面的内容 节省对比的开销
      // 只有一个块中的顶层节点才有dynamicChildren属性
      if (dynamicChildren) {
          // 调用 patchBlockChildren 对比动态子节点
          patchBlockChildren(n1.dynamicChildren, dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds);
          // 一些场景下 需要遍历一次静态节点 详见下文
          if (parentComponent && parentComponent.type.__hmrId) {
              traverseStaticChildren(n1, n2);
          }
      }
      // 无优化 全量对比
      else if (!optimized) {
          // full diff
          patchChildren(n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds, false);
      }
      // 触发vnode和指令的updated钩子
      if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
          queuePostRenderEffect(() => {
              vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1);
              dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated');
          }, parentSuspense);
      }
  };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant