vue3源码浅析:mount

715 阅读8分钟

vue3源码浅析:createApp

vue3源码浅析:mount

vue3源码浅析:createRenderer

一个简单的例子

mount方法,将应用实例的根组件挂载在提供的 DOM 元素上。数据响应式;vnode获取,vnode diff,最后渲染成DOM,这些方法都是在mount执行中完成的。

还是从一个简单的例子中看看其执行过程

<div class="" id="app">
    <button @click='add'>
        <h3>{{num}}</h3>
    </button>
</div>
<script>
    const {
        createApp,ref
    } = Vue
    const app = createApp({
        setup(){
            let num = ref(0);
            const add = ()=>{
                num.value++;
            }
            return{
                num,add
            }
        }
    }).mount('#app')
</script>

mount的执行过程

以下为本例中app.mount()的执行

下面针对每一个函数的执行过程做一个初步解析

app.mount

app.mount方法是在createApp中对mount方法的扩展,会将根元素传递给内部的mount方法

执行过程如下:

  1. normalizeContainer通过document.querySelector获得DOM元素

    在本例中参数containerOrSelector为#app,得到的container为div#app及其子元素

  2. 得到createApp的配置对象,保存在component

    在本例中app._component为setup函数

  3. 判断配置对象component,得到template元素,并保存到component

    在本例中component.template为div#app的子元素

  4. 将根元素传递给内部的mount方法执行

//**  \packages\runtime-dom\src\index.ts  */

  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    const component = app._component
    if (!isFunction(component) && !component.render && !component.template) {
      component.template = container.innerHTML
    }
    // clear content before mounting
    container.innerHTML = ''
    const proxy = mount(container)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }

mount

mount方法是app实例的应用方法,就执行过程如下:(SSR暂不做讨论)

  1. 调用createVNode获得vnode,参数rootComponent为调用createApp(config)的时候传递进来的config对象和template

    本例中createVNode参数具体值如下

    rootComponentrootProps
    null
  2. context则是通过createAppContext()获得的,将context保存在根节点上

    context
  3. 执行render将vnode转变成DOM元素并挂载到根元素上

    本例中render中参数如下

    vnoderootContainer
    div#app
//** \packages\runtime-core\src\apiCreateApp.ts */
  const context = createAppContext()
  mount(rootContainer: HostElement, isHydrate?: boolean): any {
    if (!isMounted) {
      const vnode = createVNode(
        rootComponent as ConcreteComponent,
        rootProps
      )
      // store app context on the root VNode.
      // this will be set on the root instance on initial mount.
      vnode.appContext = context

      // HMR root reload
      if (__DEV__) {
        context.reload = () => {
          render(cloneVNode(vnode), rootContainer)
        }
      }

      if (isHydrate && hydrate) {
        hydrate(vnode as VNode<Node, Element>, rootContainer as any)
      } else {
        render(vnode, rootContainer)
      }
      isMounted = true
      app._container = rootContainer
      // for devtools and telemetry
      ;(rootContainer as any).__vue_app__ = app


      return vnode.component!.proxy
    } else if (__DEV__) {
      warn(...)
    }
  },

render

render方法执行步骤如下:

  1. 判断vnode是否存在,不存在且container._vnode有值,也就是有之前的dom渲染,则进行unmount操作

  2. 如果vnode不为空,则进行patch操作,dom diff和渲染

    本例中vnode存在执行patch方法

  3. 执行flushPostFlushCbs函数,回调调度器,使用Promise实现

  4. 把container的_vnode存储为当前vnode,方便后面进行dom diff操作

//** \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
}

patch

在下面思维导图中,n1代表前一次的vnode,n2代表本次的vnode

path方法执行过程如下:

  1. 判断n1是否不为null,n1和n2的type是否相同,如果符合条件执行unmount

    本例中n1为null,不执行unmount

  2. 根据n2的vnode.type的类型选择执行方式

    本例中vnode.type为component,执行的是processComponent方法,最后调用mountComponent方法

//*packages\runtime-core\src\renderer.ts  */
const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
  ) => {
    // patching & not same type, unmount old tree
    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(...)
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(...)
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          processComponent(...)
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          ;(type as typeof TeleportImpl).process(...)
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ;(type as typeof SuspenseImpl).process(...)
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }

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

mountComponent

执行过程如下:

  1. 调用createComponentInstance生成当前组件实例instance

    本例中instance实例如下

  2. 调用setupComponent(instance)进行组件初始化,初始化props和slots等,并完成数据响应式

  3. 调用setupRenderEffect()安装渲染函数

//** \packages\runtime-core\src\renderer.ts */
  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))
    
	...
    
    setupComponent(instance)
      
	...
    
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )

  }

setupComponent

执行步骤如下:

  1. 执行initProps,初始化props
  2. 执行initSlots,初始化slots
//*packages\runtime-core\src\component.ts*//
export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR

  const { props, children, shapeFlag } = instance.vnode
  const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT
  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children)

  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  isInSSRComponentSetup = false
  return setupResult
}

setupRenderEffect

setupRenderEffect只有一步:

  1. 为当前实例挂载上update方法,update方法是通过effect生成的

effect在Vue3中的作用就相当于Vue2中的observe,update生成后,挂载之前会先运行一下生成的effect方法,最后返回当前effect方法给update;运行effect函数就相当于Vue2中watcher调用get的过程.

//** \packages\runtime-core\src\renderer.ts */ 
const setupRenderEffect: SetupRenderEffectFn = (
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    // create reactive effect for rendering
    instance.update = effect(function componentEffect() {
      if (!instance.isMounted){
          ...
      }else {
          ...
      }
    }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
  }        

componentEffect

componentEffect函数有两个逻辑,判断是否已经渲染:instance.isMounted;如果已经渲染,则走更新逻辑;未渲染,则走未渲染的逻辑

该函数的执行位置在effect --> createReactiveEffect中,未渲染的执行过程如下:

  1. beforeMount生命周期如果存在,执行该生命函数

  2. 父类的BeforeMount生命周期如果存在,执行该生命函数

  3. 调用renderComponentRoot,进行渲染组件的根元素,得到subTree

    instance实例subTree
  4. 执行patch方法

    本例中第二次执行patch,vnode.type为element,执行的是processElement方法,最后调用mountElement方法

  5. 调用当前实例的mounted钩子函数;调用n2的父类的mounted钩子函数;调用当前实例的activated钩子函数;不是直接调用,而是通过queuePostRenderEffect放到队列中去调用

  6. 最终把实例的isMounted置为true

//** \packages\runtime-core\src\renderer.ts */ 
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)
        }

        // render
		  ...
        const subTree = (instance.subTree = renderComponentRoot(instance))
		  ...

        if (el && hydrateNode) {
		  ...
        } else {
		  ...
          patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
          )
			...
          initialVNode.el = subTree.el
        }
        // mounted hook
        if (m) {
          queuePostRenderEffect(m, parentSuspense)
        }
        // onVnodeMounted
        if ((vnodeHook = props && props.onVnodeMounted)) {
          const scopedInitialVNode = initialVNode
          queuePostRenderEffect(() => {
            invokeVNodeHook(vnodeHook!, parent, scopedInitialVNode)
          }, parentSuspense)
        }
        // activated hook for keep-alive roots.
        // #1742 activated hook must be accessed after first render
        // since the hook may be injected by a child keep-alive
        const { a } = instance
        if (
          a &&
          initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
        ) {
          queuePostRenderEffect(a, parentSuspense)
        }
        instance.isMounted = true

        // #2458: deference mount-only object parameters to prevent memleaks
        initialVNode = container = anchor = null as any
      } else {
		...
      }

patch

这次第二次执行patch,第一次patch的对象是单个组件,而这次是div#app内的元素,执行过程和第一次的相同

mountElement

mountElement作为元素渲染方法,其将vnode通过insert方法渲染为真实DOM,如果碰到含有子元素则调用mountChildren

//** \packages\runtime-core\src\renderer.ts */  
const mountElement = (
    vnode: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    let el: RendererElement
    let vnodeHook: VNodeHook | undefined | null
    const {
      type,
      props,
      shapeFlag,
      transition,
      scopeId,
      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)
    } else {
      el = vnode.el = hostCreateElement(
        vnode.type as string,
        isSVG,
        props && props.is
      )

      // 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(...)
      }

      if (dirs) {
        invokeDirectiveHook(vnode, null, parentComponent, 'created')
      }
      // props
      if (props) {
        for (const key in props) {
          if (!isReservedProp(key)) {
            hostPatchProp(...)
          }
        }
        if ((vnodeHook = props.onVnodeBeforeMount)) {
          invokeVNodeHook(vnodeHook, parentComponent, vnode)
        }
      }
      // scopeId
      setScopeId(el, scopeId, vnode, parentComponent)
    }
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      Object.defineProperty(el, '__vnode', {
        value: vnode,
        enumerable: false
      })
      Object.defineProperty(el, '__vueParentComponent', {
        value: parentComponent,
        enumerable: false
      })
    }
    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)
    if (
      (vnodeHook = props && props.onVnodeMounted) ||
      needCallTransitionHooks ||
      dirs
    ) {
      queuePostRenderEffect(() => {
        vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, vnode)
        needCallTransitionHooks && transition!.enter(el)
        dirs && invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
      }, parentSuspense)
    }
  }

effect

effect 作为 reactive 的核心,主要负责收集依赖,更新依赖

effect保存了组件更新函数componentEffect,如果数据发生变化,重新执行组件更新函数

effect函数有两个参数

fnprodEffectOptions
fn = ƒ componentEffect()
//* vue-next\packages\reactivity\src\effect.ts */
export interface ReactiveEffectOptions {
  lazy?: boolean //  是否延迟触发 effect
  computed?: boolean // 是否为计算属性
  scheduler?: (job: ReactiveEffect) => void // 调度函数
  onTrack?: (event: DebuggerEvent) => void // 追踪时触发
  onTrigger?: (event: DebuggerEvent) => void // 触发回调时触发
  onStop?: () => void // 停止监听时触发
}

//* packages\reactivity\src\effect.ts */
export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
    // 如果已经是 `effect` 先重置为原始对象
  if (isEffect(fn)) {
    fn = fn.raw
  }
    // 创建`effect`
  const effect = createReactiveEffect(fn, options)
  // 如果没有传入 lazy 则直接执行一次 `effect`
  if (!options.lazy) {
    effect()
  }
  return effect
}

createReactiveEffect

createReactiveEffecteffect创建函数,依赖收集过程如图所示

三个函数作用如下:

effect:将回调函数保存起来备用,立即执行一次回调函数触发它里面一些响应数据的getter

track:getter中调用track,把前面存储的回调函数和当前target,key之间建立映射关系

trigger:setter中调用trigger,把target,key对应的响应函数都执行一遍

//* packages\reactivity\src\effect.ts /
function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {

    // 没有激活,说明我们调用了effect stop 函数,
    if (!effect.active) {
      // 如果没有调度者,直接返回,否则直接执行fn
      return options.scheduler ? undefined : fn(...args)
    }

    // 判断effectStack中有没有effect, 如果在则不处理
    if (!effectStack.includes(effect)) {
      // 清除effect依赖,定义在下方
      cleanup(effect)
      try {
        // 开始重新收集依赖
        enableTracking()
        // 压入Stack
        effectStack.push(effect)
        // 将activeEffect当前effect 
        activeEffect = effect
        return fn(...args)
      } finally {
        // 完成后将effect弹出
        effectStack.pop()
        // 重置依赖
        resetTracking()
        // 重置activeEffect 
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++ // 自增id, effect唯一标识
  effect._isEffect = true  // 是否是effect
  effect.active = true // 是否激活 
  effect.raw = fn // 挂载原始对象
  effect.deps = []  // 当前 effect 的dep 数组
  effect.options = options // 传入的options,在effect有解释的那个字段
  return effect
}

const effectStack: ReactiveEffect[] = []

// 每次 effect 运行都会重新收集依赖, deps 是 effect 的依赖数组, 需要全部清空
function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

参考

effect源码分析

mount源码分析

vue3初探