Vue3 source code analysis: createApp

Vue3 source analysis: mount

Vue3 source code analysis: createRenderer

A simple example

Mount method to mount the root component of the application instance on the supplied DOM element. Data response; Vnode fetching, vNode diff, and finally DOM rendering are all done in mount execution.

Let’s take a look at the implementation from a simple example

<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>
Copy the code

Mount execution process

The following is the execution of app.mount() in this example

Here is a preliminary breakdown of how each function executes

app.mount

The app.mount method is an extension of the mount method in createApp, passing the root element to the internal mount method

The execution process is as follows:

  1. NormalizeContainer gets the DOM element via Document. querySelector

    In this case containerOrSelector is #app, and the resulting container is div#app and its children

  2. Get the configuration object for createApp, stored in component

    In this case app._component is the setup function

  3. Determine the configuration object Component, get the template element, and save it in component

    In this case component.template is a child of div#app

  4. Pass the root element to the internal mount method for execution

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

mount

Mount method is the application method of app instance, and the execution process is as follows :(SSR will not be discussed for the moment)

  1. Call createVNode to get vNode with rootComponent as the Config object and template passed in when createApp(config) is called

    In this example, the createVNode parameter is set as follows

    rootComponent rootProps
    null
  2. Context is obtained by createAppContext(), which stores the context on the root node

    context
  3. Execute Render to convert the vNode to a DOM element and mount it to the root element

    In this example, the parameters in render are as follows

    vnode rootContainer
    div#app
//** \packages\runtime-core\src\apiCreateApp.ts */
  constcontext = 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; (rootContaineras any).__vue_app__ = app


      returnvnode.component! .proxy }else if(__DEV__) { warn(...) }},Copy the code

render

The render method performs the following steps:

  1. Check whether vnode exists. If it does not exist and container._vnode has a value, unmount the previous DOM rendering

  2. If the vNode is not empty, patch, DOM diff, and render are performed

    In this example, the patch method is executed on the Vnode

  3. The flushPostFlushCbs function is executed to call back the scheduler, using the Promise implementation

  4. The _vnode of the container is stored as the current VNode for dom diff operations

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

patch

In the mind map below, N1 represents the previous Vnode and n2 represents the current Vnode

The path method is executed as follows:

  1. Check whether n1 is not null and whether the types of N1 and n2 are the same. If yes, run unmount

    In this example, n1 is null, and unmount is not performed

  2. Select the execution mode based on the vnode.type of n2

    In this case, vNode. type is component, the processComponent method is executed, and the mountComponent method is called

//*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.` (The ${typeof type}) `)}}// set ref
    if(ref ! =null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2)
    }
  }
Copy the code

mountComponent

The execution process is as follows:

  1. CreateComponentInstance generates the current component instance instance

    In this example, instance is as follows

  2. Call setupComponent(instance) to initialize components, props, slots, etc., and perform data responsiveness

  3. Call setupRenderEffect() to install the render function

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

  }
Copy the code

setupComponent

Perform the following steps:

  1. Run initProps to initialize the props
  2. Execute initSlots to initialize 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
}
Copy the code

setupRenderEffect

SetupRenderEffect has only one step:

  1. Mount the update method for the current instance, which is generated by Effect

Effect in Vue3 is equivalent to observe and update in Vue2. After it is generated, the generated effect method will be run before mounting, and the current effect method will be returned to update. Running effect is the equivalent of watcher calling GET in Vue2.

//** \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) }Copy the code

componentEffect

The componentEffect function has two logics to determine whether it has rendered: instance.isMounted; If already rendered, go to update logic; Unrendered, follows unrendered logic

This function is executed in effect –> createReactiveEffect. The unrendered execution is as follows:

  1. BeforeMount life cycle Executes the life function if it exists

  2. The BeforeMount life cycle of the parent class, if present, executes the life function

  3. RenderComponentRoot is called to render the root element of the component to obtain the subTree

    The instance instance subTree
  4. Executing the patch method

    In this example, patch is executed for the second time, vNode. type is element, and the processElement method is executed. Finally, the mountElement method is called

  5. Calls the mounted hook function of the current instance. The mounted hook function of n2 is called. Call the activated hook function of the current instance. Not directly, but via queuePostRenderEffect into a queue

  6. Set isMounted to 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{... }Copy the code

patch

This time, patch is executed for the second time. The object of the first patch is a single component, while this time it is an element in div#app. The execution process is the same as the first time

mountElement

MountElement is the element rendering method, which renders the VNode as a real DOM using the INSERT method and calls mountChildren if it encounters a child element

//** \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
    constneedCallTransitionHooks = (! parentSuspense || (parentSuspense && ! parentSuspense.pendingBranch)) && transition && ! transition.persistedif(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)
    }
  }
Copy the code

effect

As the core of Reactive, Effect is mainly responsible for collecting and updating dependencies

Effect saves the component update function componentEffect, which is re-executed if data changes

The effect function takes two parameters

fn prodEffectOptions
Fn = ƒ componentEffect ()
//* vue-next\packages\reactivity\src\effect.ts */
export interfaceReactiveEffectOptions { lazy? :boolean // Whether to delay triggering effectcomputed? :boolean // Whether it is a calculated attributescheduler? :(job: ReactiveEffect) = > void // Scheduling functiononTrack? :(event: DebuggerEvent) = > void // Triggered when tracingonTrigger? :(event: DebuggerEvent) = > void // Triggered when the callback is triggeredonStop? :() = > void // Triggered when listening is stopped
}

//* packages\reactivity\src\effect.ts */
export function effect<T = any> (fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ) :ReactiveEffect<T> {
    // If it is already 'effect', reset it to the original object
  if (isEffect(fn)) {
    fn = fn.raw
  }
    / / create a ` effect `
  const effect = createReactiveEffect(fn, options)
  // Effect is executed once if no lazy is passed
  if(! options.lazy) { effect() }return effect
}
Copy the code

createReactiveEffect

CreateReactiveEffect Creates a function for effect, and the dependency collection process is shown in figure 1

The three functions function as follows:

Effect: Saves the callback function for later use, and immediately executes the callback function to trigger the getter for some of the response data in it

Track: Call track in the getter to map the previously stored callback function to the current target and key

Trigger: Trigger is called in the setter, and the target and key response functions are executed

//* 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 is not activated.
    if(! effect.active) {// If there is no scheduler, return directly, otherwise execute fn directly
      return options.scheduler ? undefined: fn(... args) }// Check whether effectStack has an effect
    if(! effectStack.includes(effect)) {// Clear effect dependencies, as defined below
      cleanup(effect)
      try {
        // Start collecting dependencies again
        enableTracking()
        / / pressure into the Stack
        effectStack.push(effect)
        // activeEffect Specifies the current effect
        activeEffect = effect
        returnfn(... args) }finally {
        // Pop effect when done
        effectStack.pop()
        // Reset dependencies
        resetTracking()
        / / reset activeEffect
        activeEffect = effectStack[effectStack.length - 1]}}}as ReactiveEffect
  effect.id = uid++ // Add id, effect unique identifier
  effect._isEffect = true  // whether is effect
  effect.active = true // Whether to activate
  effect.raw = fn // Mount the original object
  effect.deps = []  // The current effect DEP array
  effect.options = options // The passed options, the field explained in effect
  return effect
}

const effectStack: ReactiveEffect[] = []

// Every time effect runs, dependencies are recollected. Deps is an effect dependency array that needs to be emptied
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}}Copy the code

reference

Effect source code analysis

Mount source code analysis

Vue3 que