1. 首页

Vue3源码解读(二)-mount

前言

前面讲到了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 } } //JS中文网 – 前端进阶资源分享 www.javascriptc.com

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

  • 调用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 } //JS中文网 – 前端进阶资源分享 www.javascriptc.com

上面代码为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:处理组件的函数; Vue3源码解读(二)-mount 接下来我们将会研究一个例子,会涉及到processFragment和processElement,做一个dom的diff操作;

render例子

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

packages/vue/examples/classic/hello.js

 js
const app = Vue.createApp({
    data() {
        return {
            list: ['a', 'b', 'c', 'd']
        }
    }
});
app.mount('#demo')
//JS中文网 – 前端进阶资源分享 www.javascriptc.com

packages/vue/examples/classic/hello.html

 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>
//JS中文网 – 前端进阶资源分享 www.javascriptc.com

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

Vue3源码解读(二)-mount

进行到这里,就是成功的了,我们下一步研究页面上的效果是如何运行出来的,从上面的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目前还是一个对象: Vue3源码解读(二)-mount 此时的判断会符合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
      }
    }
    //JS中文网 – 前端进阶资源分享 www.javascriptc.com
上面是整理后的第一次渲染的`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 {
                 // 其他逻辑
              }
          }
        //JS中文网 – 前端进阶资源分享 www.javascriptc.com
    根据参数可以知道会走到当前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
                )
            }
        }
        //JS中文网 – 前端进阶资源分享 www.javascriptc.com
    可以看到将会对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)
            }
        }
        //JS中文网 – 前端进阶资源分享 www.javascriptc.com

JS中文网 – 前端进阶资源教程 www.javascriptC.com
一个致力于帮助开发者用代码改变世界为使命的平台,每天都可以在这里找到技术世界的头条内容

    根据参数n1为null可以知晓,会走到mountElement的逻辑,参数不会发生改变。执行mountElement的过程中会检测ul的children,发现ul的children下面有值,则会调用mountChildren函数:
        mountChildren(
            vnode.children as VNodeArrayChildren,
            el,
            null,
            parentComponent,
            parentSuspense,
            isSVG && type !== 'foreignObject',
            optimized || !!vnode.dynamicChildren
        )
        //JS中文网 – 前端进阶资源分享 www.javascriptc.com
    此时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部分进行讲解,慢慢把它们都消化掉。

作者:德莱问
链接:https://juejin.im/post/6883072260123394061

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:Vue3源码解读(二)-mount

链接:https://www.javascriptc.com/4085.html

« Vue3源码解读(三)-patch
Vue3响应式原理剖析»
Flutter 中文教程资源

相关推荐

QR code