vue3.0 pre-alpha之mount源碼解析

例子

分析以下mount過程vue

const App = {
    template: `<div>{{counter.num}}</div><button @click="add">增長</button>`,
    setup() {
      const counter = reactive({ num: 0 })
      function add() {
        counter.num ++;
      }
      return {
        counter,
        add
      }
    }
}

createApp().mount(App, '#app');
複製代碼

先上一個流程圖,而後對照源碼一步一步理解。 node

createApp

生成返回一個App對象,對象中包括mount、use、mixin、component、directive等方法。這裏主要分析mount方法react

return createApp(): App {
    // 建立components、derectives等上下文
    const context = createAppContext()

    let isMounted = false

    const app: App = {
      use(plugin: Plugin) {
        // ...
        return app
      },
      
      // 核心函數mount
      mount(
        rootComponent: Component,
        rootContainer: HostElement,
        rootProps?: Data
      ): any {
        if (!isMounted) {
          // 生成vnode: rootComponent對象。rootProps:數據
          const vnode = createVNode(rootComponent, rootProps)
          // store app context on the root VNode.
          // this will be set on the root instance on initial mount.
          vnode.appContext = context
          // 核心函數,渲染
          render(vnode, rootContainer)
          isMounted = true
          return vnode.component!.renderProxy
        }
      }
    }

    return app
  }
複製代碼

createVNode

生成Vnode對象,將class和style標準化,bash

export function createVNode(
  type: VNodeTypes,
  props: { [key: string]: any } | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null
): VNode {
  // class和style標準化
  // class & style normalization.
  if (props !== null) {
    // for reactive or proxy objects, we need to clone it to enable mutation.
    if (isReactive(props) || SetupProxySymbol in props) {
      // proxy對象還原成源對象,extend相似於Object.assign
      props = extend({}, props)
    }
    let { class: klass, style } = props
    if (klass != null && !isString(klass)) {
      props.class = normalizeClass(klass)
    }
    if (style != null) {
      // reactive state objects need to be cloned since they are likely to be
      // mutated
      if (isReactive(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }
  
  // 判斷vnode類型
  // encode the vnode type information into a bitmap
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspenseType(type)
      ? ShapeFlags.SUSPENSE
      : isObject(type)
        ? ShapeFlags.STATEFUL_COMPONENT
        : isFunction(type)
          ? ShapeFlags.FUNCTIONAL_COMPONENT
          : 0

  const vnode: VNode = {
    _isVNode: true, // 用來判斷是不是vnode
    type,
    props,
    key: (props !== null && props.key) || null,
    ref: (props !== null && props.ref) || null,
    children: null,
    component: null,
    suspense: null,
    dirs: null,
    el: null,
    anchor: null,
    target: null,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
  }

  // 將children掛到vnode.children上
  normalizeChildren(vnode, children)

  // ...

  return vnode
}

複製代碼

render

核心函數patchapp

const render: RootRenderFunction<
    HostNode,
    HostElement & {
      _vnode: HostVNode | null
    } > = (vnode, container) => {
    if (vnode == null) {
      if (container._vnode) {
        // !vnode && container上已掛載vnode,卸載vnode
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 核心函數patch,將vnode掛載到container上
      patch(container._vnode || null, vnode, container)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }

複製代碼

patch

patch用於比較兩個vnode的不一樣,將差別部分已打補丁的形式更新到頁面上。mount過程時n1爲null。本例直接掛載的類型是COMPONENT,核心函數爲processComponentasync

function patch(
    n1: HostVNode | null, // null means this is a mount
    n2: HostVNode,
    container: HostElement,
    anchor: HostNode | null = null,
    parentComponent: ComponentInternalInstance | null = null,
    parentSuspense: HostSuspenseBoundary | null = null,
    isSVG: boolean = false,
    optimized: boolean = false
  ) {
    // patching & not same type, unmount old tree
    // 若是tag類型不一樣,卸載老的tree
    if (n1 != null && !isSameType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    const { type, shapeFlag } = n2
    switch (type) {
      case Text:
        processText(n1, n2, container, anchor)
        break
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        break
      case Portal:
        processPortal(
          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) {
          // 本例掛載的是component
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } 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 HostVNode type:', n2.type, `(${typeof n2.type})`)
        }
    }
  }
複製代碼

processComponent

判斷掛載,調用函數mountComponentide

function processComponent(
    n1: HostVNode | null,
    n2: HostVNode,
    container: HostElement,
    anchor: HostNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: HostSuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) {
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.STATEFUL_COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.sink as KeepAliveSink).activate(
          n2,
          container,
          anchor
        )
      } else {
        // 核心函數mountComponent
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG
        )
      }
    } else {
      // ...
  }
複製代碼

mountComponent

兩個核心函數,setupStatefulComponentsetupRenderEffectsetupStatefulComponent: 執行setup,將返回值setupResult轉換爲reactive數據。將template轉化成render函數。setupRenderEffect: 調用effectrender渲染頁面,將依賴存入targetMapsvg

function mountComponent(
    initialVNode: HostVNode,
    container: HostElement,
    anchor: HostNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: HostSuspenseBoundary | null,
    isSVG: boolean
  ) {
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent
    ))

    if (__DEV__) {
      pushWarningContext(initialVNode)
    }

    const Comp = initialVNode.type as Component

    // inject renderer internals for keepAlive
    if ((Comp as any).__isKeepAlive) {
      const sink = instance.sink as KeepAliveSink
      sink.renderer = internals
      sink.parentSuspense = parentSuspense
    }

    // resolve props and slots for setup context
    const propsOptions = Comp.props
    resolveProps(instance, initialVNode.props, propsOptions)
    resolveSlots(instance, initialVNode.children)

    // setup stateful logic
    if (initialVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
      <!--執行setup-->
      setupStatefulComponent(instance, parentSuspense)
    }

    // setup() is async. This component relies on async logic to be resolved
    // before proceeding
    if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
      if (!parentSuspense) {
        if (__DEV__) warn('async setup() is used without a suspense boundary!')
        return
      }

      parentSuspense.registerDep(instance, setupRenderEffect)

      // give it a placeholder
      const placeholder = (instance.subTree = createVNode(Comment))
      processCommentNode(null, placeholder, container, anchor)
      initialVNode.el = placeholder.el
      return
    }
    
    // 默認調用一次effect渲染頁面,並將依賴存入targetMap中
    setupRenderEffect(
      instance,
      parentSuspense,
      initialVNode,
      container,
      anchor,
      isSVG
    )

    if (__DEV__) {
      popWarningContext()
    }
  }
複製代碼

setupStatefulComponent

執行setup()handleSetupResult將setup的返回值包裝成響應式對象。finishComponentSetuptemplate編譯成render函數函數

export function setupStatefulComponent(
  instance: ComponentInternalInstance,
  parentSuspense: SuspenseBoundary | null
) {
  const Component = instance.type as ComponentOptions
  
  // ...
  const { setup } = Component
  if (setup) {
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)

    currentInstance = instance
    currentSuspense = parentSuspense
    
    <!-- 執行setup(),能夠看出給setup傳了兩個參數,propsProxy和setupContext-->
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [propsProxy, setupContext]
    )
    currentInstance = null
    currentSuspense = null

    if (isPromise(setupResult)) {
      if (__FEATURE_SUSPENSE__) {
        // async類型的setup處理方式
        // async setup returned Promise.
        // bail here and wait for re-entry.
        instance.asyncDep = setupResult
      } else if (__DEV__) {
        warn(
          `setup() returned a Promise, but the version of Vue you are using ` +
            `does not support it yet.`
        )
      }
      return
    } else {
    
      <!-- 將setup的返回值包裝成響應式對象 -->
      handleSetupResult(instance, setupResult, parentSuspense)
    }
  } else {
    finishComponentSetup(instance, parentSuspense)
  }
}

複製代碼
export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  parentSuspense: SuspenseBoundary | null
) {
  if (isFunction(setupResult)) {
    // setup returned an inline render function
    instance.render = setupResult as RenderFunction
  } else if (isObject(setupResult)) {
    if (__DEV__ && isVNode(setupResult)) {
      warn(
        `setup() should not return VNodes directly - ` +
          `return a render function instead.`
      )
    }
    // setup returned bindings.
    // assuming a render function compiled from template is present.
    // setupResult就是setup函數中return的值
    // 將setupResult包裝成proxy對象
    instance.renderContext = reactive(setupResult)
  } else if (__DEV__ && setupResult !== undefined) {
    warn(
      `setup() should return an object. Received: ${
        setupResult === null ? 'null' : typeof setupResult
      }`
    )
  }
  <!--將template編譯爲render函數-->
  finishComponentSetup(instance, parentSuspense)
}
複製代碼
function finishComponentSetup(
  instance: ComponentInternalInstance,
  parentSuspense: SuspenseBoundary | null
) {
  const Component = instance.type as ComponentOptions
  if (!instance.render) {
    if (__RUNTIME_COMPILE__ && Component.template && !Component.render) {
      // __RUNTIME_COMPILE__ ensures `compile` is provided
      // 將template轉化爲render函數
      // 在vue.ts中註冊了compile函數
      Component.render = compile!(Component.template, {
        isCustomElement: instance.appContext.config.isCustomElement || NO,
        onError(err: CompilerError) {
          if (__DEV__) {
            const message = `Template compilation error: ${err.message}`
            const codeFrame =
              err.loc &&
              generateCodeFrame(
                Component.template!,
                err.loc.start.offset,
                err.loc.end.offset
              )
            warn(codeFrame ? `${message}\n${codeFrame}` : message)
          }
        }
      })
    }
    if (__DEV__ && !Component.render) {
      /* istanbul ignore if */
      if (!__RUNTIME_COMPILE__ && Component.template) {
        warn(
          `Component provides template but the build of Vue you are running ` +
            `does not support on-the-fly template compilation. Either use the ` +
            `full build or pre-compile the template using Vue CLI.`
        )
      } else {
        warn(
          `Component is missing${
            __RUNTIME_COMPILE__ ? ` template or` : ``
          } render function.`
        )
      }
    }
    // 將component的render賦值給外層render
    // const App = {render: h('div', 'hello world')} =>
    // instance.render = h('div', 'hello world')
    instance.render = (Component.render || NOOP) as RenderFunction
  }

  // support for 2.x options
  if (__FEATURE_OPTIONS__) {
    currentInstance = instance
    currentSuspense = parentSuspense
    applyOptions(instance, Component)
    currentInstance = null
    currentSuspense = null
  }

  if (instance.renderContext === EMPTY_OBJ) {
    instance.renderContext = reactive({})
  }
}
複製代碼

setupRenderEffect

使用effect包裹渲染過程:渲染instance到頁面,並將全部依賴存入targetMap。從而實現頁面響應式。響應式原理可參考這篇文章:juejin.im/post/5da3e3…post

function setupRenderEffect(
    instance: ComponentInternalInstance,
    parentSuspense: HostSuspenseBoundary | null,
    initialVNode: HostVNode,
    container: HostElement,
    anchor: HostNode | null,
    isSVG: boolean
  ) {
    // create reactive effect for rendering
    let mounted = false
    instance.update = effect(function componentEffect() {
      if (!mounted) {
        <!--調用render函數(template編譯生成),將template轉化爲subTree-->
        const subTree = (instance.subTree = renderComponentRoot(instance))
        // beforeMount hook
        if (instance.bm !== null) {
          invokeHooks(instance.bm)
        }
        <!--核心函數patch,將subTree渲染到頁面上-->
        patch(null, subTree, container, anchor, instance, parentSuspense, isSVG)
        initialVNode.el = subTree.el
        // mounted hook
        if (instance.m !== null) {
          queuePostRenderEffect(instance.m, parentSuspense)
        }
        mounted = true
      } else {
          // ...
      }
  }
複製代碼

patch

subTree渲染到頁面上。核心函數processElementprocessElement中調用mountElement

function mountElement(
    vnode: HostVNode,
    container: HostElement,
    anchor: HostNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: HostSuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) {
    const tag = vnode.type as string
    isSVG = isSVG || tag === 'svg'
    const el = (vnode.el = hostCreateElement(tag, isSVG))
    const { props, shapeFlag } = vnode
    if (props != null) {
      for (const key in props) {
        if (isReservedProp(key)) continue
        hostPatchProp(el, key, props[key], null, isSVG)
      }
      if (props.onVnodeBeforeMount != null) {
        invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode)
      }
    }
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      hostSetElementText(el, vnode.children as string)
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      mountChildren(
        vnode.children as HostVNodeChildren,
        el,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized || vnode.dynamicChildren !== null
      )
    }
    hostInsert(el, container, anchor)
    if (props != null && props.onVnodeMounted != null) {
      queuePostRenderEffect(() => {
        invokeDirectiveHook(props.onVnodeMounted, parentComponent, vnode)
      }, parentSuspense)
    }
  }
複製代碼
相關文章
相關標籤/搜索