美文网首页
2023.24 vue3 渲染系统

2023.24 vue3 渲染系统

作者: wo不是黄蓉 | 来源:发表于2023-06-16 18:43 被阅读0次
    2023.24 vue3 渲染系统.png

    大家好,我是wo不是黄蓉,今年学习目标从源码共读开始,希望能跟着若川大佬学习源码的思路学到更多的东西。

    5月份事情比较多,没有持续学习,6月份继续学习打卡。上一篇文章结尾写的看下写浏览器插件,这个已经写出来了,但是大多数是api相关的操作,后面就没有分享,只是放在git项目里当作是一个学习吧。

    为什么要学习源码?之前我看源码是为了应付面试,现在觉得看源码学习的是一种撸代码思路和一些值得借鉴的代码编写方式,毕竟我们不是大佬,大部分人都是从ctrl+c、ctrl+v过来的。把学到的东西应用项目中也是一种能力。

    长话短说,开始学习~

    学习调试

    • clone代码到本地,源码链接
    • 本地安装依赖,我是以todomvc.html来调试的,调试前,需要执行pnpm run build打包源码到dist目录,然后在浏览器运行html页面,就可以调试了

    vue运行包含两个阶段:1编译时(源码不可调试,是由其他类库操作的),2运行时(可进行调试)

    createApp 创建应用

    createApp只是准备vue实例对象,在其对象上挂载了一个mount方法,执行自身的mount方法,挂载时需要提供一个挂载的容器

    createAppmount之间还有一个baseCreateRenderer方法,baseCreateRenderer操作dom相关操作

    mount方法,创建vnode执行渲染函数,执行副作用函数进行挂载

    渲染函数里面进行patch操作
    借用网上的一张图,整体流程如下,推荐看的一篇vue渲染相关的文章如下

    image.png

    createApp({}).mount('#app') createApp也就是createApp({})前面这一步,返回app对象,等待挂载,看下createApp做了哪些准备工作

     var createApp = (...args) => {
         //获取实例,调用的实际是baseCreateRenderer里面的createApp方法
        const app = ensureRenderer().createApp(...args);
        if (true) {
          injectNativeTagCheck(app);
          injectCompilerOptionsCheck(app);
        }
        //从实例中结构出Mount方法
        const { mount } = app;
        app.mount = (containerOrSelector) => {
          const container = normalizeContainer(containerOrSelector);
          if (!container)
            return;
          const component = app._component;
          if (!isFunction(component) && !component.render && !component.template) {
              //创建模板渲染对象
            component.template = container.innerHTML;
          }
          container.innerHTML = "";
         //挂载对象
          const proxy = mount(container, false, container instanceof SVGElement);
         
          return proxy;
        };
        return app;
      };
    

    ensureRenderer创建reder的入口函数,返回的是createRenderer方法,createRenderer方法又返回的baseCreateRenderer方法,baseCreateRenderer最终返回的是一个对象,返回的对象是调用ensureRenderer方法最终的结果。

    ensureRenderer调用完后,紧接着调用了createApp方法,其实也就是baseCreateRenderer返回对象中的一个方法,实际上是createAppAPI

    //ensureRenderer
    function ensureRenderer() {
        return renderer || (renderer = createRenderer(rendererOptions));
      }
    function createRenderer(options) {
        return baseCreateRenderer(options);
      }
    //createApp实际返回的内容
    function baseCreateRenderer(){
    //...相关核心操作dom的方法
         const target = getGlobalThis();
         target.__VUE__ = true;
        return {
          render: render2,
          hydrate: hydrate2,
          createApp: createAppAPI(render2, hydrate2)
        };
    }
    

    baseCreateRendererdom操作相关核心方法,都是一些对节点操作的方法,这些函数分析diff算法时以及渲染流程时会看到。例如:

    image.png image.png

    createAppAPI发生了什么?
    可以看到这里创建了app对象,并将其返回了,在此期间,关联了插件、指令、mixin、组件、挂载组件的方法等,将app对象和渲染进行关联,实际上渲染操作是在调用mount方法时进行的

    这里会初始化上下文,以及初始化app,最后返回app

     function createAppAPI(render2, hydrate2) {
        return function createApp2(rootComponent, rootProps = null) {
          if (!isFunction(rootComponent)) {
            rootComponent = extend({}, rootComponent);
          }
          if (rootProps != null && !isObject(rootProps)) {
            warn2(`root props passed to app.mount() must be an object.`);
            rootProps = null;
          }
          //初始化上下文
          const context = createAppContext();
    
          const installedPlugins = /* @__PURE__ */ new Set();
          let isMounted = false;
          const app = context.app = {
            _uid: uid++,
            _component: rootComponent,
            _props: rootProps,
            _container: null,
            _context: context,
            _instance: null,
            version,
            use(plugin, ...options) {},
            mixin(mixin) { },
            component(name, component) { },
            directive(name, directive) {},
            mount(rootContainer, isHydrate, isSVG) { },
            unmount() { },
            provide(key, value) {},
            runWithContext(fn) {
              currentApp = app;
            }
          };
          return app;
        };
      }
    

    直到执行.mount("#app")都是在准备环境,到执行mount方法才开始渲染模板和解析模板

    最主要的方法mount方法,创建vnode执行渲染函数,执行副作用函数进行挂载

    进入mount方法,进入第二阶段挂载阶段

    createApp返回的内容

    image.png
    mount(rootContainer, isHydrate, isSVG) {
                //挂载函数,创建vnode
                const vnode = createVNode(
                  rootComponent,
                  rootProps
                );
                //将vnode和app上下文关联起来
                vnode.appContext = context;
                if (true) {
                  //添加reload函数
                  context.reload = () => {
                    render2(cloneVNode(vnode), rootContainer, isSVG);
                  };
                }
                //执行render函数进行渲染,将vnode渲染成dom节点并进行挂载,render过程会检查组件是否更新,继续调用patch方法对比前后节点
                render2(vnode, rootContainer, isSVG);
                isMounted = true;
                app._container = rootContainer;
                rootContainer.__vue_app__ = app;
                if (true) {
                  app._instance = vnode.component;
                  devtoolsInitApp(app, version);
                }
                return getExposeProxy(vnode.component) || vnode.component.proxy;
              }
            },
    

    挂载阶段又分为几个小阶段

    挂载阶段

    2.1 createVNode创建vnode

    createVNode返回的是createBaseVNode,调用createBaseVNode返回vnodemountconst vnode = createVNode( rootComponent, rootProps );返回的结果就是createBaseVNode调用的结果

    创建虚拟节点,获取到虚拟节点后进行渲染操作render2(vnode, rootContainer, isSVG);接下来到执行render方法

    createBaseVNode时为什么要在顶部创建一个空白的vnode

    到最后面才看出为什么要创建一个空的vnode,这个vnode因为没有对#app根元素进行解析,所以创建的这个空的vnode节点就相当于是根节点的占位符,解析完所有子元素后,render函数最后会执行这样一行代码container._vnode = vnode;而最后vnode内容就是#app下解析的第一个元素的容器

    image.png
    function createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) {
         //type可能是一个组件或者是一个html元素,是vnode的话,直接复制,不用重复解析
         if (isVNode(type)) {
          const cloned = cloneVNode(
            type,
            props,
            true
            /* mergeRef: true */
          );
         }
       //创建vnode
        return createBaseVNode(
          type,
          props,
          children,
          patchFlag,
          dynamicProps,
          shapeFlag, //shapeFlag:代表的元素类型
          isBlockNode,
          true
        );
      }
    //为什么要在顶部创建一个空白的vnode?
      function createBaseVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, shapeFlag = type === Fragment ? 0 : 1 /* ELEMENT */, isBlockNode = false, needFullChildrenNormalization = false) {
        const vnode = {
          __v_isVNode: true,
          __v_skip: true,
          type,
          props,
          key: props && normalizeKey(props),
          ref: props && normalizeRef(props),
          scopeId: currentScopeId,
          slotScopeIds: null,
          children,
          component: null,
          suspense: null,
          ssContent: null,
          ssFallback: null,
          dirs: null,
          transition: null,
          el: null,
          anchor: null,
          target: null,
          targetAnchor: null,
          staticCount: 0,
          shapeFlag,
          patchFlag,
          dynamicProps,
          dynamicChildren: null,
          appContext: null,
          ctx: currentRenderingInstance
        };
        return vnode;
      }
    
    
    

    render函数,执行第一次的patch操作 。挂载vnode进行渲染

        const render2 = (vnode, container, isSVG) => {
          if (vnode == null) {
            if (container._vnode) {
              unmount(container._vnode, null, null, true);
            }
          } else {
            patch(container._vnode || null, vnode, container, null, null, null, isSVG);
          }
          flushPreFlushCbs();
          flushPostFlushCbs();
          container._vnode = vnode;
        };
       
      
    

    2.2 检查更新阶段 patch

    patch确定元素类型,挂载组件。第一次patch 是从render2开始的,第一次判定为组件,执行:processComponent方法

    这里不知道为什么经常用到位运算?

    在网上搜到的答案,使用位运算是为了提升性能

    学习位运算推荐看下这篇文章,其中包括实战中权限方案的设计等,可以通过位运算来添加、校验、删除权限,很是神奇

      const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = isHmrUpdating ? false : !!n2.dynamicChildren) => {
          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;
            case Fragment:
              processFragment(
                n1,
                n2,
                container,
                anchor,
                parentComponent,
                parentSuspense,
                isSVG,
                slotScopeIds,
                optimized
              );
              break;
            default:
              if (shapeFlag & 1 /* ELEMENT */) {
                processElement(
                  n1,
                  n2,
                  container,
                  anchor,
                  parentComponent,
                  parentSuspense,
                  isSVG,
                  slotScopeIds,
                  optimized
                );
              } else if (shapeFlag & 6 /* COMPONENT */) {
                processComponent(
                  n1,
                  n2,
                  container,
                  anchor,
                  parentComponent,
                  parentSuspense,
                  isSVG,
                  slotScopeIds,
                  optimized
                );
              } else if (shapeFlag & 64 /* TELEPORT */) {
                ;
                type.process(
                  n1,
                  n2,
                  container,
                  anchor,
                  parentComponent,
                  parentSuspense,
                  isSVG,
                  slotScopeIds,
                  optimized,
                  internals
                );
              } else if (shapeFlag & 128 /* SUSPENSE */) {
                ;
                type.process(
                  n1,
                  n2,
                  container,
                  anchor,
                  parentComponent,
                  parentSuspense,
                  isSVG,
                  slotScopeIds,
                  optimized,
                  internals
                );
              } else if (true) {
                warn2("Invalid VNode type:", type, `(${typeof type})`);
              }
          }
          if (ref2 != null && parentComponent) {
            setRef(ref2, n1 && n1.ref, parentSuspense, n2 || n1, !n2);
          }
        };
    

    2.3. 组件挂载阶段

    mountComponent,设置render副作用

        const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
          const compatMountInstance = false;
          const instance = compatMountInstance || (initialVNode.component = createComponentInstance(
            initialVNode,
            parentComponent,
            parentSuspense
          ));
            //...前面这些都不重要,最重要是触发setupRenderEffect
          setupRenderEffect(
            instance,
            initialVNode,
            container,
            anchor,
            parentSuspense,
            isSVG,
            optimized
          );
          if (true) {
            popWarningContext();
            endMeasure(instance, `mount`);
          }
        };
    

    2.4 设置副作用 setupRenderEffect

    setupRenderEffect主要部分代码,创建了副作用对象,立即执行其update方法,即执行effect2.run(),而run里面传的可执行函数是componentUpdateFn

        const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
    
          
          //重要部分代码2
          const effect2 = instance.effect = new ReactiveEffect2(
            componentUpdateFn,
            () => queueJob(update),
            instance.scope
            // track it in component's effect scope
          );
          const update = instance.update = () => effect2.run();
          update.id = instance.uid;
    
          update();
        };
    

    componentUpdateFn这里进行了第二次patch操作,此时对比的对象是子树,第二次时遍历的对象是模板里面#app下的元素,会执行到processElement方法,然后执行mountElement方法,

          const componentUpdateFn = () => {
            if (!instance.isMounted) {
              let vnodeHook;
              const { el, props } = initialVNode;
              const { bm, m, parent } = instance;
              const isAsyncWrapperVNode = isAsyncWrapper(initialVNode);
         
              toggleRecurse(instance, true);
              if (el && hydrateNode) {
             
              } else {
              
                const subTree = instance.subTree = renderComponentRoot(instance);
               
                patch(
                  null,
                  subTree,
                  container,
                  anchor,
                  instance,
                  parentSuspense,
                  isSVG
                );
              
                initialVNode.el = subTree.el;
              }
              if (m) {
                queuePostRenderEffect(m, parentSuspense);
              }
              if (!isAsyncWrapperVNode && (vnodeHook = props && props.onVnodeMounted)) {
                const scopedInitialVNode = initialVNode;
                queuePostRenderEffect(
                  () => invokeVNodeHook(vnodeHook, parent, scopedInitialVNode),
                  parentSuspense
                );
              }
              if (false) {
                queuePostRenderEffect(
                  () => instance.emit("hook:mounted"),
                  parentSuspense
                );
              }
              if (initialVNode.shapeFlag & 256 /* COMPONENT_SHOULD_KEEP_ALIVE */ || parent && isAsyncWrapper(parent.vnode) && parent.vnode.shapeFlag & 256 /* COMPONENT_SHOULD_KEEP_ALIVE */) {
                instance.a && queuePostRenderEffect(instance.a, parentSuspense);
                if (false) {
                  queuePostRenderEffect(
                    () => instance.emit("hook:activated"),
                    parentSuspense
                  );
                }
              }
              instance.isMounted = true;
              if (true) {
                devtoolsComponentAdded(instance);
              }
              initialVNode = container = anchor = null;
            } else {
              let { next, bu, u, parent, vnode } = instance;
              let originNext = next;
              let vnodeHook;
              if (true) {
                pushWarningContext(next || instance.vnode);
              }
              toggleRecurse(instance, false);
              if (next) {
                next.el = vnode.el;
                updateComponentPreRender(instance, next, optimized);
              } else {
                next = vnode;
              }
              if (bu) {
                invokeArrayFns(bu);
              }
              if (vnodeHook = next.props && next.props.onVnodeBeforeUpdate) {
                invokeVNodeHook(vnodeHook, parent, next, vnode);
              }
              if (false) {
                instance.emit("hook:beforeUpdate");
              }
              toggleRecurse(instance, true);
              if (true) {
                startMeasure(instance, `render`);
              }
              const nextTree = renderComponentRoot(instance);
              if (true) {
                endMeasure(instance, `render`);
              }
              const prevTree = instance.subTree;
              instance.subTree = nextTree;
              if (true) {
                startMeasure(instance, `patch`);
              }
              patch(
                prevTree,
                nextTree,
                // parent may have changed if it's in a teleport
                hostParentNode(prevTree.el),
                // anchor may have changed if it's in a fragment
                getNextHostNode(prevTree),
                instance,
                parentSuspense,
                isSVG
              );
              if (true) {
                endMeasure(instance, `patch`);
              }
              next.el = nextTree.el;
              if (originNext === null) {
                updateHOCHostEl(instance, nextTree.el);
              }
              if (u) {
                queuePostRenderEffect(u, parentSuspense);
              }
              if (vnodeHook = next.props && next.props.onVnodeUpdated) {
                queuePostRenderEffect(
                  () => invokeVNodeHook(vnodeHook, parent, next, vnode),
                  parentSuspense
                );
              }
              if (false) {
                queuePostRenderEffect(
                  () => instance.emit("hook:updated"),
                  parentSuspense
                );
              }
              if (true) {
                devtoolsComponentUpdated(instance);
              }
              if (true) {
                popWarningContext();
              }
            }
          };
    

    mountElement,挂载元素操作,将vnode转成dom并进行挂载,最后一次执行hostInsert即将所有解析到的元素插入到#app,执行完就可以看到页面已经挂载到页面上了。vue3子节点挂载到父元素的过程不是一次性的,而是每次解析完子元素就进行挂载,最后一次将所有#app下的元素都挂载上去,页面就渲染完成了

    const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
          let el;
          let vnodeHook;
          const { type, props, shapeFlag, transition, dirs } = vnode;
          //从这里可以看出来,vue对每次操作vnode以及将vnode转为dom的记录都是有保存的,等到下次需要时可以直接拿出来使用,不用重复进行解析,其他地方mountChildren   const child = children[i] = optimized ? cloneIfMounted(children[i]) : normalizeVNode(children[i]);判断如果时vnode直接进行复制
          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
            );
          }
          if (dirs) {
            invokeDirectiveHook(vnode, null, parentComponent, "created");
          }
          setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent);
          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);
            }
          }
          if (true) {
            Object.defineProperty(el, "__vnode", {
              value: vnode,
              enumerable: false
            });
            Object.defineProperty(el, "__vueParentComponent", {
              value: parentComponent,
              enumerable: false
            });
          }
    
          hostInsert(el, container, anchor);
          if ((vnodeHook = props && props.onVnodeMounted) || needCallTransitionHooks || dirs) {
            queuePostRenderEffect(() => {
              vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode);
              needCallTransitionHooks && transition.enter(el);
              dirs && invokeDirectiveHook(vnode, null, parentComponent, "mounted");
            }, parentSuspense);
          }
        };
    

    hostCreateElement,根据vnode创建dom元素

     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: (child, parent, anchor) => {
          parent.insertBefore(child, anchor || null);
        },
    

    vue3为什么要每次先创建一个对象然后再在某个时刻触发对象里面的方法呢?

    例如,在createApp阶段,使用ensureRenderer().createApp(...args)来创建app,又例如,在mount阶段执行副作用函数,是将组将更新的操作挂载到effect

    盲猜,其实将操作对象的方法放在外面也是可以的,不过这样和该对象关联的相关操作就比较分散,将所有操作都挂载到对象上其实更方便理解代码逻辑比较清晰。

    这次大多数走的是第一次渲染的流程,代码太多,文章太长,下节看下更新渲染的流程。

    相关文章

      网友评论

          本文标题:2023.24 vue3 渲染系统

          本文链接:https://www.haomeiwen.com/subject/wrwrydtx.html