Vue3源码解读(二)-mount

10,858 阅读9分钟

前言

前面讲到了Vue3中mount里面做的事情,其中第一步的mount前面已经讲解了,本篇文章将会从第二步的runtime-core文件夹下面的apiCreateApp文件mount函数开始讲起,依次讲解剩下的几步。按照主流程进行核心代码的解读,跳跃可能会比较大,不过在讲解过程中都会标明是哪个文件。

正文

正文从这开始,mount从此开始。 先来看下源码,前面讲到的dom部分的源码此处不多说,从core部分开始讲起:

packages/runtime-core/src/apiCreateApp.ts

mount(rootContainer: HostElement, isHydrate?: boolean): any {
  if (!isMounted) {
    const vnode = createVNode(
      rootComponent as ConcreteComponent,
      rootProps
    )
    vnode.appContext = context

    if (isHydrate && hydrate) {
      hydrate(vnode as VNode<Node, Element>, rootContainer as any)
    } else {
      render(vnode, rootContainer)
    }
    isMounted = true
    app._container = rootContainer
    ;(rootContainer as any).__vue_app__ = app
    
    return vnode.component!.proxy
  }
}

此处的源代码其实还是很简单的,

  • 调用createVNode获取vnode,rootComponent即为调用createApp(config)的时候传递进来的config数据,rootProps为root props,前面提到过会对此进行校验,一般在使用过程中,rootProps为null;
  • 保存context在跟节点上;
  • 调用渲染函数,此处只讲解render;
  • isMounted置为true;
  • 实例的_container保存为当前rootContainer;
  • rootContainer增加属性__vue_app__,置为当前app实例;
  • 返回vnode.component的代理。

核心渲染代码为render函数。

render

render函数的作用在Vue2和Vue3中是完全不一样的,

  • Vue2中render函数是做具体工作的,是真正的render操作,返回的结果是vnode,可以在这回顾下Vue2源码解读(七)-mount
  • Vue3中render函数是做分发工作的,相当于是一个路由器,两条线路,unmount和patch,无返回结果。

来看下render的源码:

packages/runtime-core/src/renderer.ts

const render: RootRenderFunction = (vnode, container) => {
    if (vnode == null) {
        if (container._vnode) {
            unmount(container._vnode, null, null, true)
        }
    } else {
        patch(container._vnode || null, vnode, container)
    }
    flushPostFlushCbs()
    container._vnode = vnode
}

上面代码为render函数的源码:

  • 参数1:vnode,是要更新到页面上的vnode,通过上面createVNode获得;container为展现的容器;
  • 先是对vnode进行了判断,如果为空,并且container._vnode有值,也就是有之前的dom渲染,则进行unmount操作;
  • 如果vnode不为空,则进行patch操作,dom diff和渲染;
  • 执行flushPostFlushCbs函数,回调调度器,使用Promise实现,与Vue2的区别是Vue2是宏任务或微任务来处理的
  • 把container的_vnode存储为当前vnode,方便后面进行dom diff操作,此处和Vue2中是一样的。

因为是渲染,vnode不会为空,肯定会走到patch函数部分,来看下patch部分的代码:

packages/runtime-core/src/renderer.ts

const patch: PatchFn = (
    n1, // old
    n2, // new
    container, // 容器
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
) => {
    // 如果type不相同,则把n1直接卸载掉
    if (n1 && !isSameVNodeType(n1, n2)) {
        anchor = getNextHostNode(n1)
        unmount(n1, parentComponent, parentSuspense, true)
        n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
        optimized = false
        n2.dynamicChildren = null
    }

    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(
                n1,
                n2,
                container,
                anchor,
                parentComponent,
                parentSuspense,
                isSVG,
                optimized
            )
            break
        default:
            if (shapeFlag & ShapeFlags.ELEMENT) {
                processElement(
                    n1,
                    n2,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    optimized
                )
            } else if (shapeFlag & ShapeFlags.COMPONENT) {
                processComponent(
                    n1,
                    n2,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    optimized
                )
            } else if (shapeFlag & ShapeFlags.TELEPORT) {
                ;(type as typeof TeleportImpl).process(
                    n1 as TeleportVNode,
                    n2 as TeleportVNode,
                    container,
                    anchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    optimized,
                    internals
                )
            } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
                ;(type as typeof SuspenseImpl).process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized, internals)
            } else if (__DEV__) {
                warn('Invalid VNode type:', type, `(${typeof type})`)
            }
    }

    if (ref != null && parentComponent) {
        setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
    }
}

已上门patch函数为入口进行梳理分析,得到了下面的图,在其中有几条比较常用的线路:

  • processFragment:处理片段(dom数组)的函数;
  • processElement:处理元素的函数;
  • processComponent:处理组件的函数; 接下来我们将会研究一个例子,会涉及到processFragment和processElement,做一个dom的diff操作;

render例子

我们现在将从头到尾开始讲解一个例子,将会从头到尾,一步步讲解用到的函数。假如现在有一个列表:

packages/vue/examples/classic/hello.js

const app = Vue.createApp({
    data() {
        return {
            list: ['a', 'b', 'c', 'd']
        }
    }
});
app.mount('#demo')

packages/vue/examples/classic/hello.html

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport"  content="initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,
    user-scalable=no,target-densitydpi=medium-dpi,viewport-fit=cover"/>
    <title>Vue3.js hello example</title>
    <script src="../../dist/vue.global.js"></script>
</head>
<body>
<div id="demo">
    <ul>
        <li v-for="item in list" :key="item">
            {{item}}
        </li>
    </ul>
</div>
<script src="./hello.js"></script>
</body>
</html>

我们在Vue3源代码的根目录,对应的目录下面新建hello.js和hello.html两个文件,把上面代码复制到对象文件中,然后到根目录运行npm run dev,好了,现在项目跑起来了。然后打开浏览器,输入url地址: file:///Users/draven/mywork/vue-3.0.0/packages/vue/examples/classic/hello.html ;可以看到页面的渲染:

进行到这里,就是成功的了,我们下一步研究页面上的效果是如何运行出来的,从上面的render函数说起。

  • 1、开始运行:调用render(vnode, rootContainer),该函数的运行位置位于packages/runtime-core/src/apiCreateApp.ts,render函数的声明位于packages/runtime-core/src/renderer.ts;参数vnode为上面调用createVNode所生成的,参数rootContainer就是我们上面传进来的id为demo的元素;
  • 2、接下来进入的是packages/runtime-core/src/renderer.ts文件,接下来的功能大部分都在这个文件里面,如有特殊情况会进行说明。
  • 3、接下来运行:在render函数内部调用patch(container._vnode || null, vnode, container)
    • 3.1、第一个参数为老的vnode,因为是首次渲染,老的vnode是不存在的,所以为null;第二个参数就是透传的vnode;第三个参数为透传的container(#demo);
    • 3.2、patch函数还接受其他参数,不过咱们暂时用不到:patch(n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false);n1即为null,n2即为要更新的vnode,container为透传#demo;
    • 3.3、此时的n1为null,n2目前还是一个对象: 此时的判断会符合shapeFlag & ShapeFlags.COMPONENT,走到processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)函数;此时参数的值是:n1为null,n2如图所示,container为#demo;
  • 4、processComponent函数会进行对n1的判断,n1不为null,则证明是更新操作,调用updateComponent;此时,我们是首次渲染,所以不会走更新操作,走另外一个逻辑;如果为keepAlive的类型的组件,走activate逻辑;此时,我们不为keepalive的组件,所以走mountComponent函数,mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized),参数n2为上图,container为#demo,其他的参数目前还都是默认null(false)值;
  • 5、mountComponent函数会首先调用createComponentInstance生成对当前n2的实例,然后调用setupComponent初始化props和slots等,最终调用setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ),参数instance为上面生成的实例,initialVNode还是为上图n2,container为#demo,其他为默认值;
  • 6、setupRenderEffect函数是一个非常核心的函数,此函数将会为当前实例挂载上update方法,update方法是通过effect生成的,effect在Vue3中的作用就相当于Vue2中的observe;update生成后,挂载之前会先运行一下生成的effect方法,最后返回当前effect方法给update;运行effect函数就相当于Vue2中watcher调用get的过程.effect接受两个参数,第一个参数就是componentEffect函数,也就是监听变化调用此函数;上面讲到先运行一下生成的effect方法,生成的effect方法内部就会调用这个componentEffect函数;
  • 7、componentEffect函数有两个逻辑,判断是否已经渲染:instance.isMounted;如果已经渲染,则走更新逻辑;咱们还未渲染,则走未渲染的逻辑;来看下这部分的源码。
    function componentEffect() {
      if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const {el, props} = initialVNode
        const {bm, m, parent} = instance
    
        // beforeMount hook
        if (bm) {
            invokeArrayFns(bm)
        }
        // onVnodeBeforeMount
        if ((vnodeHook = props && props.onVnodeBeforeMount)) {
            invokeVNodeHook(vnodeHook, parent, initialVNode)
        }
        const subTree = (instance.subTree = renderComponentRoot(instance))
    
        if (el && hydrateNode) {
            hydrateNode(
                initialVNode.el as Node,
                subTree,
                instance,
                parentSuspense
            )
        } else {
            patch(
                null,
                subTree,
                container,
                anchor,
                instance,
                parentSuspense,
                isSVG
            )
            initialVNode.el = subTree.el
        }
        if (m) {
            queuePostRenderEffect(m, parentSuspense)
        }
        if ((vnodeHook = props && props.onVnodeMounted)) {
            queuePostRenderEffect(() => {
                invokeVNodeHook(vnodeHook!, parent, initialVNode)
            }, parentSuspense)
        }
        const {a} = instance
        if (
            a &&
            initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
        ) {
            queuePostRenderEffect(a, parentSuspense)
        }
        instance.isMounted = true
      } else {
        // no first render
      }
    }
    

上面是整理后的第一次渲染的componentEffect函数源码;

    • 7.1、先调用了当前实例的beforeMount钩子函数;
    • 7.2、调用n2的父类的BeforeMount钩子函数;
    • 7.3、调用renderComponentRoot函数进行渲染组件的根元素;
    • 7.4、调用patch:patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);此时subtree的值为:;container为#demo;anchor为null,instance为当前实例,parentSuspense为null,isSVG为false;
    • 7.5、调用当前实例的mounted钩子函数;调用n2的父类的mounted钩子函数;调用当前实例的activated钩子函数;不是直接调用,而是通过queuePostRenderEffect放到队列中去调用;
    • 7.6、最终把实例的isMounted置为true;
  • 8、上面componentEffect函数中调用patch才是正式渲染的开始,前面大部分都是相当于数据的整理:
    • 8.1、按照上面componentEffect函数的运行参数传递到patch函数:patch(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);此时n1为null,n2为上图(subtree),container还是#demo,anchor为null,parentComponent为上面的instance实例,parentSuspense为null,isSVG为false,optimized为false;
    • 8.2、代码依次执行,通过上图可以看到,component获取到的实例的subtree的type为Fragment,则会走到processFragment函数;
    • 8.3、processFragment接受的参数和patch函数接受的参数是一样的,还是上面的值,无变化,来看下源码:
        const processFragment = (
            n1: VNode | null,
            n2: VNode,
            container: RendererElement,
            anchor: RendererNode | null,
            parentComponent: ComponentInternalInstance | null,
            parentSuspense: SuspenseBoundary | null,
            isSVG: boolean,
            optimized: boolean
        ) => {
            const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
            const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
            let {patchFlag, dynamicChildren} = n2
            if (patchFlag > 0) {
                optimized = true
            }
            if (n1 == null) {
                hostInsert(fragmentStartAnchor, container, anchor)
                hostInsert(fragmentEndAnchor, container, anchor)
                mountChildren(
                    n2.children as VNodeArrayChildren,
                    container,
                    fragmentEndAnchor,
                    parentComponent,
                    parentSuspense,
                    isSVG,
                    optimized
                )
            } else {
            	 // 其他逻辑
            }
        }
      
      根据参数可以知道会走到当前if逻辑,会先插入骨架;然后执行mountChildren,n2.children通过上面的subtree可以知道,值为一个数组,数组里面有1个元素,就是咱们要渲染的ul;
      const mountChildren: MountChildrenFn = (
          children,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized,
          start = 0
      ) => {
          for (let i = start; i < children.length; i++) {
              const child = (children[i] = optimized
                  ? cloneIfMounted(children[i] as VNode)
                  : normalizeVNode(children[i]))
              patch(
                  null,
                  child,
                  container,
                  anchor,
                  parentComponent,
                  parentSuspense,
                  isSVG,
                  optimized
              )
          }
      }
      
      可以看到将会对n2.children进行遍历,n2.children只有一个元素,是ul
    • 8.4、使用上面的运行时的参数,调用patch(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG = false, optimized);参数:n1为null;child为上面提到的ul;container为#demo,anchor为上面processFragment函数里面的fragmentEndAnchor;parentComponent为instance实例;parentSuspense为null;isSVG为false;optimized为true,因为在上面processFragment里面进行了改变;
    • 8.5、由上面参数可知,ul的类型为ul,此时会走到processElement函数,processElement函数的参数和patch函数的参数是一样的,进行了透传,看下源代码:
      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)
          }
      }
      
      根据参数n1为null可以知晓,会走到mountElement的逻辑,参数不会发生改变。执行mountElement的过程中会检测ul的children,发现ul的children下面有值,则会调用mountChildren函数:
      mountChildren(
          vnode.children as VNodeArrayChildren,
          el,
          null,
          parentComponent,
          parentSuspense,
          isSVG && type !== 'foreignObject',
          optimized || !!vnode.dynamicChildren
      )
      
      此时vnode.children为由4个li组成的数组;el为ul,anchor为null,parentComponent为instance实例;parentSuspense为null;isSVG为false;optimized为true;重复上面mountChildren函数;
    • 8.6、mountChildren函数里面进行for循环的时候,li的type为li,则会继续走到processElement,重复上面步骤,依次执行完成;
  • 9、上面所有的步骤执行完成,现在数据已经呈现到页面上了。
  • 10、此时基本所有的事情都干完了,也就是相当于主队列空闲了,调用flushPostFlushCbs()开始执行队列里面的函数;
  • 11、最后把container的_vnode属性指向当前vnode;方便下次做dom diff使用。
  • 12、第一次渲染运行完成。

结语

本章着重降了first render的渲染过程,下一章会像按照本章的节奏结合Vue2源码解读(七)中的dom diff部分对Vue3中的patch部分进行讲解,慢慢把它们都消化掉。