# vue3元素渲染流程简介

# 简单渲染流程

以下面这段代码渲染为例:

<div id="app">
  <div>{{a}}</div>
</div>

createApp({
  setup() {
    return {
      a: ref(1)
    }
  }
}).mount('#app')
1
2
3
4
5
6
7
8
9
10
11
  1. patch组件 => mountComponent
  2. 初始化数据,创建effect
  3. 内部effect执行
    • 未挂载: 生成subtree 进行patch挂载
    • 已挂载: 由数据变更触发,patch(prevTree, nextTree)

# mountComponent

mountComponent会执行setupRenderEffect,创建依赖

const mountComponent: MountComponentFn = (
    initialVNode, container, anchor, parentComponent,
    parentSuspense, isSVG, optimized ) => {
    // 创建组件实例
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))
    ...
    // 性能监控
    startMeasure(instance, `init`)
    // 执行setup
    setupComponent(instance)
    endMeasure(instance, `init`)
    ...
    // setup() is async. This component relies on async logic to be resolved
    // before proceeding
    if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
      parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect)
      ...
      return
    }

    setupRenderEffect(instance, initialVNode,  container,
      anchor, parentSuspense, isSVG, optimized )
    ...
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# createComponentInstance

创建组件实例,完成初始化

let uid = 0
export function createComponentInstance(vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null
) {
  const type = vnode.type as ConcreteComponent
  // inherit parent app context - or - if root, adopt from root vnode
  const appContext =
    (parent ? parent.appContext : vnode.appContext) || emptyAppContext

  const instance: ComponentInternalInstance = {
    uid: uid++, vnode, type, parent, appContext, root: null!, // to be immediately set
    next: null, subTree: null!, // will be set synchronously right after creation
    update: null!, // will be set synchronously right after creation
    render: null, proxy: null, exposed: null, withProxy: null, effects: null,
    provides: parent ? parent.provides : Object.create(appContext.provides),
    accessCache: null!, renderCache: [],
    // local resovled assets
    components: null, directives: null,
    // resolved props and emits options
    propsOptions: normalizePropsOptions(type, appContext), 
    emitsOptions: normalizeEmitsOptions(type, appContext),
    // emit
    emit: null as any, // to be set immediately
    emitted: null,
    // props default value
    propsDefaults: EMPTY_OBJ,
    // state
    ctx: EMPTY_OBJ, data: EMPTY_OBJ, props: EMPTY_OBJ, attrs: EMPTY_OBJ,
    slots: EMPTY_OBJ, refs: EMPTY_OBJ, setupState: EMPTY_OBJ, setupContext: null,
    // suspense related
    ...
    // lifecycle hooks
    // not using enums here because it results in computed properties
    ...
  }
  instance.ctx = { _: instance }
  instance.root = parent ? parent.root : instance
  instance.emit = emit.bind(null, instance)

  return instance
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# setupComponent

  1. initProps,initSlots
  2. 执行setup,配合pauseTracking | resetTracking,不进行依赖收集,生成proxy
  3. 执行complier处理template,生成 instance.render
    1. baseParse 创建带渲染模板的ast
    2. transformast树转化为genCode,其中createRootCodegen是处理DOM元素的,会创建codegenNode
    3. generate生成with包裹的可执行代码字符串: 将this修改为当前实例,使用genNode处理codegenNode,生成对应的vnode方法
  4. applyOptions,混合vue2x的方法与属性(data/mixins),proxy[[Target]]添加内容,配合pauseTracking | resetTracking,不进行依赖收集
export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR

  const { props, children } = instance.vnode
  const isStateful = isStatefulComponent(instance)
  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children)

  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# render函数示例

vue3-compiler | render函数解析

template代码:

<div id="app">
  <h1>juest - test </h1>
  <div>
    <div>{{a}}</div>
    <p :class="[a]">{{a}}</p>
  </div>
  <p :id="a">{{a}}</p>
</div>
1
2
3
4
5
6
7
8

编译后:

const _Vue = Vue
const { createVNode: _createVNode } = _Vue
// 静态代码
const _hoisted_1 = /*#__PURE__*/_createVNode("h1", null, "juest - test ", -1 /* HOISTED */)

return function render(_ctx, _cache) {
  // 利用with,设置代码内的this指向
  with (_ctx) {
    const { createVNode: _createVNode, toDisplayString: _toDisplayString, Fragment: _Fragment, openBlock: _openBlock, createBlock: _createBlock } = _Vue

    return (_openBlock(), _createBlock(_Fragment, null, [
      _hoisted_1,
      _createVNode("div", null, [
        _createVNode("div", null, _toDisplayString(a), 1 /* TEXT */),
        _createVNode("p", { class: [a] }, _toDisplayString(a), 3 /* TEXT, CLASS */)
      ]),
      _createVNode("p", { id: a }, _toDisplayString(a), 9 /* TEXT, PROPS */, ["id"])
    ], 64 /* STABLE_FRAGMENT */))
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# setupRenderEffect设置effect

内部会格式化的实例,调用响应式数据,渲染页面,详见effect-api | setupRenderEffect

# renderComponentRoot格式化模板

执行setupComponent生成的render函数,处理之前生成的vnode:

如添加渲染用数据,生成children,此时会获取响应式数据的值,进行依赖收集

# mountElement

  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, patchFlag, dirs } = vnode
    if (
      !__DEV__ &&
      vnode.el &&
      hostCloneNode !== undefined &&
      patchFlag === PatchFlags.HOISTED
    ) {
      // If a vnode has non-null el, it means it's being reused.
      // Only static vnodes can be reused, so its mounted DOM nodes should be
      // exactly the same, and we can simply do a clone here.
      // only do this in production since cloned trees cannot be HMR updated.
      el = vnode.el = hostCloneNode(vnode.el) // 克隆node节点 => el.cloneNode(deep)
    } else {
      el = vnode.el = hostCreateElement(vnode.type as string,isSVG,
        props && props.is,props) // 创建node节点

      // mount children first, since some props may rely on child content
      // being already rendered, e.g. `<select value>`
      // 子节点是 text 或者 children
      if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
        hostSetElementText(el, vnode.children as string)
      } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        mountChildren(...) // 子元素处理
      }
      // 指令处理
      if (dirs) {
        invokeDirectiveHook(vnode, null, parentComponent, 'created')
      }
      // props
      if (props) {
        for (const key in props) {
          // '"",key,ref,' +
          // 'onVnodeBeforeMount,onVnodeMounted,' +
          // 'onVnodeBeforeUpdate,onVnodeUpdated,' +
          // 'onVnodeBeforeUnmount,onVnodeUnmounted'
          if (!isReservedProp(key)) { 
            // 处理属性
            hostPatchProp(el, key, null, props[key], isSVG, vnode.children as VNode[],
            parentComponent, parentSuspense, unmountChildren)
          }
        }
        if ((vnodeHook = props.onVnodeBeforeMount)) {
          invokeVNodeHook(vnodeHook, parentComponent, vnode)
        }
      }
      // scopeId
      setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
    }
    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
    const needCallTransitionHooks =
      (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) &&
      transition &&
      !transition.persisted
    if (needCallTransitionHooks) {
      transition!.beforeEnter(el)
    }
    // 插入到父节点中
    hostInsert(el, container, anchor) // parent.insertBefore(child, anchor || null)
    if (
      (vnodeHook = props && props.onVnodeMounted) ||
      needCallTransitionHooks ||
      dirs
    ) {
      // 添加到pendingPostFlushCbs队列???确保了元素是挂载成功的
      queuePostRenderEffect(() => {
        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
        needCallTransitionHooks && transition!.enter(el)
        dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
      }, parentSuspense)
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

# invokeVNodeHook

vnode的生命周期hooks的触发函数

export function invokeVNodeHook(
  hook: VNodeHook, // function | array
  instance: ComponentInternalInstance | null,
  vnode: VNode,
  prevVNode: VNode | null = null
) {
  callWithAsyncErrorHandling(hook, instance, ErrorCodes.VNODE_HOOK, [
    vnode,
    prevVNode
  ])
}
1
2
3
4
5
6
7
8
9
10
11