页面到底是从什么时候开始渲染的

前言

这是vue3系列源码的第三章,使用的vue3版本是3.2.45

背景

在前两篇文章里面,我们简单看了一下createAppmount的主流程。对整体的调用流程有了一个大概的了解。

现在我们来思考两个问题,页面上,到底是从什么时候开始出现内容的,又是从什么时候开始加载内部元素的。

带着这两个问题,我们详细去了解一下上篇mount中提到的一些函数。

前置

为了更简单明了的了解渲染的流程和看见效果,我们需要对我们的项目做一些前置工作。 我们在App.vue文件里面,把script都去掉,只留下一个template元素,里面放上简单的元素。 同时,我们给容器标签#app加一点样式,看的更直观一点。

js 复制代码
<template>
  <div>111</div>
</template>

<style>
#app {
  color: #2c3e50;
  width: 100px;
  height: 100px;
  background-color: pink;
}
</style>

#app

这里我们发现,在我们执行createApp之前,#app这个元素就已经渲染出来了。

不过此时显示的只是一个空的容器,我们写的组件并没有被渲染出来。

所以接下来让我们去看看,组件及其内容到底是什么时候渲染在页面上的。

setupRenderEffect

这里我们接着上一篇mount的流程,我们直接到setupRenderEffect这个函数中。

它是定义在baseCreateRender函数中的,老规矩,我们先看一下传进来的参数。

  • instance,组件实例
  • initialVNode,组件的vnode
  • container, #app的DOM对象
js 复制代码
const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    const componentUpdateFn = () => {...}
    // create reactive effect for rendering
    const effect = (instance.effect = new ReactiveEffect(
      componentUpdateFn,
      () => queueJob(update),
      instance.scope // track it in component's effect scope
    ))

    const update: SchedulerJob = (instance.update = () => effect.run())
    update.id = instance.uid
 
    update()
  }

这个函数先定义了一个函数componentUpdateFn,这个函数后面作为参数传进了new ReactiveEffect中, 最后执行的updata执行就是执行new ReactiveEffect得到对象的run方法。

那我们先看一下这个ReactiveEffect

ReactiveEffect

js 复制代码
class ReactiveEffect {
    constructor(fn, scheduler = null, scope) {
        this.fn = fn;
        this.scheduler = scheduler;
        this.active = true;
        this.deps = [];
        this.parent = undefined;
        recordEffectScope(this, scope);
    }
    run() {
        let parent = activeEffect;
        let lastShouldTrack = shouldTrack;
        while (parent) {
            if (parent === this) {
                return;
            }
            parent = parent.parent;
        }
        try {
            this.parent = activeEffect;
            activeEffect = this;
            shouldTrack = true;
            trackOpBit = 1 << ++effectTrackDepth;
            if (effectTrackDepth <= maxMarkerBits) {
                initDepMarkers(this);
            }
            else {
                cleanupEffect(this);
            }
            return this.fn();
        }
    }
}

ReactiveEffect类是定义在reactivity部分。 先看一下参数:

  • fn,就是我们定义的componentUpdateFn函数
  • scheduler,() => queueJob(update)
  • scope, 见下图

这里activeEffect是undefined,所以直接跳过while循环,进入try代码端。

try代码端里的核心就是return this.fn(), 而这个fn就是之前定义的componentUpdateFn函数

componentUpdateFn

js 复制代码
const componentUpdateFn = () => {
      if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, parent } = instance
        const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)

        toggleRecurse(instance, false)
        // beforeMount hook
        if (bm) {
          invokeArrayFns(bm)
        }
        // onVnodeBeforeMount
        if (
          !isAsyncWrapperVNode &&
          (vnodeHook = props && props.onVnodeBeforeMount)
        ) {
          invokeVNodeHook(vnodeHook, parent, 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
        }       
       ...
      } else {
       ...
      }
    }

这里的核心代码是

  • renderComponentRoot,它其实就是把app组件里内容的vnode返回了出来,所以这里的subTree是vnode
  • patch, 把得到的subTree穿进去,那么其实就是处理app组件内容了。

我们先看一下renderComponentRoot函数

renderComponentRoot

js 复制代码
function renderComponentRoot(
  instance: ComponentInternalInstance
): VNode {
  const { type, vnode, render, renderCache, data, ctx ...  } = instance

  let result
  let fallthroughAttrs
  const prev = setCurrentRenderingInstance(instance)
  try {
    if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
      // withProxy is a proxy with a different `has` trap only for
      // runtime-compiled render functions using `with` block.
      const proxyToUse = withProxy || proxy
      result = normalizeVNode(
        render!.call(proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx)
      )
      fallthroughAttrs = attrs
    } else {
  ...
  } catch (err) {
   ...
  }

  // attr merging
  // in dev mode, comments are preserved, and it's possible for a template
  // to have comments along side the root element which makes it a fragment
  let root = result
  let setRoot: SetRootFn = undefined

  if (fallthroughAttrs && inheritAttrs !== false) {
    const keys = Object.keys(fallthroughAttrs)
    const { shapeFlag } = root
    if (keys.length) {
      if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.COMPONENT)) {
        ...
        root = cloneVNode(root, fallthroughAttrs)
      } else if (__DEV__ && !accessedAttrs && root.type !== Comment) {
       ...
        }
    
      }
    }
  }
    result = root
  setCurrentRenderingInstance(prev)
  return result
}

先看一参数:

  • instance, 是App.vue组件的实例

这段的主要工作是:

  • 通过setCurrentRenderingInstance获取已经存在的渲染实例,然后将传入的组件实例保存在currentRenderingInstance变量中,这里获取的是null,所以pre也是null
  • 通过normalizeVNode获取App.vue渲染内容的vnode
  • 通过setCurrentRenderingInstancecurrentRenderingInstance置为pre的值也就是null,然后把上次保存的值也就是app组件的实例返回出来。

这里面我们简单看一下获取子vnode的部分

js 复制代码
result = normalizeVNode( render!.call(proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx) )

render.call()其实就是执行了render渲染函数,那么我们看一下最终得到什么,也就是传入normalizeVNode的值

render.call()最终得到的就是App组件里内容的vnode,normalizeVnode函数最终返回的值其实也就是这个vnode。

总结一下,renderComponentRoot函数就是返回了内容的vnode。

patch

patch函数我们在前一篇mount文章里面说过。 我们这里面的vnode其实就是这一段内容,

js 复制代码
<div>111</div>

就是一个div标签,所以在patch里面走到了ELEMENT里面。

js 复制代码
if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        }

那我们就看看processElement这个函数有什么不一样。

processElement

js 复制代码
const processElement = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    if (n1 == null) {
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {...}
  }

一样的套娃,再看看mountElement

mountElement

到了这个函数,其实离我们内容的渲染已经很近了。

js 复制代码
const mountElement = (
    vnode: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    let el: RendererElement
    let vnodeHook: VNodeHook | undefined | null
    const { type, props, shapeFlag, transition, dirs } = vnode

    el = vnode.el = hostCreateElement(
      vnode.type as string,
      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 & ShapeFlags.TEXT_CHILDREN) {
      hostSetElementText(el, vnode.children as string)
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      mountChildren(
        vnode.children as VNodeArrayChildren,
        el,
        null,
        parentComponent,
        parentSuspense,
        isSVG && type !== 'foreignObject',
        slotScopeIds,
        optimized
      )
    }
    hostInsert(el, container, anchor)
  }

我们先看一下传入的参数:

  • vnode, div的vnode
  • container, #app DOM元素
  • parentComponent, App组件的实例

这个函数的重点流程在:

  • hostCreateElement,根据传入的vnode,创建实际的div DOM元素
  • hostSetElementText,把vnode.children也就是111,塞到div的innerText中
  • hostInsert,这个就是本次显示的最终环节了,这个函数一调用,可以看见页面上app元素上就挂在了这个div元素

这里的shapeFlag是9,ShapeFlags.TEXT_CHILDREN是8,8 & 9 等于8,是true

hostInsert函数其实就是把div插入到app元素中。

js 复制代码
insert: (child, parent, anchor) => {
    parent.insertBefore(child, anchor || null)
  }

至此,我们就借用一个简单的demo,搞清楚了vue3加载显示的流程了。

流程图

graph TD subgraph setupRenderEffect["App组件的setupRenderEffect"] ReactiveEffect类 --run--> componentUpdateFn --> renderComponentRoot["renderComponentRoot得到内容的vnode"] --> patch["对内容的vnode进行patch"] --> processElement --> mountElement --> hostCreateElement["hostCreateElement创建div"] --> hostSetElementText["hostSetElementText向div中插入元素"] --> hostInsert["hostInsert把div挂在到app上"] --> 显示 end

延伸

上面我们用了比较简单的页面结构来认识这个流程。

但是实际工作中的页面肯定比这复杂的多,那么我们这里稍稍拓展一点,加一点复杂度,加上子组件,来看看流程上会有什么不同。

这里我们再次改造一下App.vue文件。

js 复制代码
<template>
  <div>111</div>
  <HelloWorld msg="你好" />
</template>
<script setup>
import HelloWorld from './components/HelloWorld.vue';
</script>
<style>
#app {
  color: #2c3e50;
  width: 100px;
  height: 100px;
  background-color: pink;
}
</style>

HelloWorld.vue文件

js 复制代码
<template>
  <div class="hello">
    {{ msg }}
  </div>
</template>

<script setup>
import { defineProps } from 'vue'
defineProps({msg: String})

</script>

最终显示的效果:

下面我们就再走一遍流程,看看哪里不一样,这里我只提一下不一样的地方。

processFragment

首先第一个不一样的地方。

在patch函数里面,执行的不再是processElement了,而是processFragment,因为此时的type是Symbol(Fragment)

js 复制代码
case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        break

我们看一下这个函数:

js 复制代码
processFragment = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2

    if (n1 == null) {
      // a fragment can only have array children
      // since they are either generated by the compiler, or implicitly created
      // from arrays.
      mountChildren(
        n2.children as VNodeArrayChildren,
        container,
        fragmentEndAnchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
     ...
    }
  }

看一下参数:

  • n1, null
  • n2, app组件内容代码段的vnode
  • container, #app DOM对象
  • parentComponent,app组件实例

这个函数最终的核心是mountChildren函数

mountChildren

js 复制代码
mountChildren: MountChildrenFn = (
    children,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    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,
        slotScopeIds,
        optimized
      )
    }
  }

看一下参数:

  • children,见下图,是div元素和子组件
  • container, app DOM对象
  • parentComponent,app组件实例

这个函数里面干的事情其实很好理解,就是循环把子元素和子组件都丢进patch函数。

那么后面的事情大家应该都熟悉,无论是对组件的patch,还是对元素的patch过程,我们在之前的文章(mount)和上面都提到了。

核心就是这么一个逻辑:

  • fragment的patch最终会向下一级进行,变为component的patch和element的patch。
  • component的patch最终会向下一级进行,变为element的patch。
  • element的patch中就会把元素添加到页面上了。

结尾

那么以上就是页面渲染部分的全部内容了。

从下一篇开始,我们将去探索页面的更新以及各种钩子和副作用的执行。

彻底搞明白vue3!

相关推荐
小白学习日记1 小时前
【复习】HTML常用标签<table>
前端·html
程序员大金1 小时前
基于SpringBoot+Vue+MySQL的装修公司管理系统
vue.js·spring boot·mysql
丁总学Java1 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele2 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀2 小时前
CSS——属性值计算
前端·css
DOKE2 小时前
VSCode终端:提升命令行使用体验
前端
xgq2 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
用户3157476081352 小时前
前端之路-了解原型和原型链
前端
永远不打烊2 小时前
librtmp 原生API做直播推流
前端
北极小狐2 小时前
浏览器事件处理机制:从硬件中断到事件驱动
前端