Vue3源码解读(3)-挂载根节点

581 阅读4分钟

1.mount

在上篇文章中,我们讲到 packages/runtime-dom/src/index.ts#createApp 方法内对 app.mount 进行了封装

// packages/runtime-dom/src/index.ts
// 省略代码...
  const { mount } = app
  // 封装 mount
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // ...
    const proxy = mount(container, false, container instanceof SVGElement)
		// ...
    return proxy
  }

  return app
}) as CreateAppFunction<Element>

app.mount 最终还是会调用 packages/runtime-core/src/apiCreateApp.ts 里面 app 对象上定义的 mount 方法执行挂载逻辑。

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // ...
    /**
    createAppAPI 工厂函数接收 render ,返回真正的 createApp 方法.
    其中 render 就是上面提到的 baseCreateRenderer 中定义的 render 方法

    app 就是实际返回的 app 对象,app 对象上定义的 use、mixin、component 
    等方法大多会返回 app 对象,因此这些方法支持链式调用,我们会在后面的文章中详细介绍这些方法,
    现在我们先继续看 Vue3 的初始化过程
    **/
    const app: App = (context.app = {
      
      // 其他 app 实例方法...

      // rootContainer 就是我们要挂载的那个元素,比如 id 为 demo 的节点
      // app.mount('#demo') 调用的就是这里的 mount 方法,执行挂载逻辑
      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        //
        if (!isMounted) {
          // rootComponent 是我们传给 createApp 的 option
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          /**
          vnode 的结构为:
          vnode = {
            anchor: null,
            appContext: null,
            children: null,
            component: null,
            dirs: null,
            dynamicChildren: null,
            dynamicProps: null,
            el: null,
            key: null,
            patchFlag: null,
            props: null,
            ref: null,
            scopeId: null,
            shapeFlag: null,
            slotScopeIds: null,
            ssContent: null,
            ssFallback: null,
            staticCount: null,
            suspense: null,
            target: null,
            targetAnchor: null,
            transition: null,
            __v_isVNode: true,
            __v_skip: true,
            type: {
              setup: '...',
              template: '...'
            }
            // type 是合入了 template 的 option
          }
           */

          // 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, isSVG)
            }
          }

          // 用于 ssr
          if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else {
            // 调用 packages/runtime-core/src/renderer.ts#render
            // 这个 render 并不是我们厂听说的 render function
            render(vnode, rootContainer, isSVG)
          }
          isMounted = true
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app

          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            devtoolsInitApp(app, version)
          }

          return vnode.component!.proxy
        } else if (__DEV__) {
          warn(
            `App has already been mounted.\n` +
              `If you want to remount the same app, move your app creation logic ` +
              `into a factory function and create fresh app instances for each ` +
              `mount - e.g. \`const createMyApp = () => createApp(App)\``
          )
        }
      },

    })

    return app
  }
}

app.mount 会调用 render(vnode, rootContainer, isSVG) 渲染节点。

2.render

上面文章中我们提到 app.mount 会调用 render(vnode, rootContainer, isSVG) 渲染根节点,这里的 render 是在我们前面讲的 baseCreateRenderer 方法内定义的,我们回顾一下这个 render 方法。

// container 就是我们要挂载的那个元素,即 rootContainer
// vnode 是使用根组件创建的根 vnode
const render: RootRenderFunction = (vnode, container, isSVG) => {
  // unmount 逻辑
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    // patch 逻辑,首次挂载会走这里
    patch(container._vnode || null, vnode, container, null, null, null, isSVG)
  }
  flushPostFlushCbs()
  container._vnode = vnode
}

可以看到 render 主要是调用 unmountpatch 方法执行卸载、挂载、更新的逻辑,我们先看下 patch 方法。

4.patch

const patch: PatchFn = (
  n1, // 旧的 VNode
  n2, // 新的 VNode
  container,
  anchor = null,
  parentComponent = null,
  parentSuspense = null,
  isSVG = false,
  slotScopeIds = null,
  optimized = false
) => {
  // 如果有旧VNode,且新旧节点不一样,umount销毁旧节点
  // 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
  // 先通过节点 type 来判断选择处理方法
  // 首次挂载时 n1 = null ,n2 是根 vnode ,代表 rootComponent 
  // 所以会走 processComponent 逻辑
  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(
        n1,
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
      break
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        // 首次挂载会走 processComponent 逻辑,将 n2 渲染到 #demo 节点下
        processComponent(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        // ...
      } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
        // ...
      } else if (__DEV__) {
        warn('Invalid VNode type:', type, `(${typeof type})`)
      }
  }

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

我们省略一些不关键的步骤,首次渲染时,processComponent 在挂载 n2 根 vnode 时,经过一系列方法调用(如下调用栈),会进入 finishComponentSetup 方法内部。

finishComponentSetup.png

我们应该还记得我们怎么创建 app 的:

<script src="../../dist/vue.global.js"></script>
<div id="demo">
	<!-- ... -->
</div>

<script>
// ...
  const app =
    createApp({
      setup() {
        const currentBranch = ref('master')
        const commits = ref(null)

        watchEffect(() => {
          fetch(`${API_URL}${currentBranch.value}`)
            .then(res => res.json())
            .then(data => {
              console.log(data)
              commits.value = data
            })
        })

        return {
          branches: ['master', 'sync'],
          currentBranch,
          commits,
          truncate,
          formatDate,
        }
      }
    });

  app.mount('#demo')
</script>

<style>
/** **/
</style>

Component 就是我们传给 createApp 的根选项 options,并且合入了 template 属性,其值是页面模板字符串。

finishComponentSetup 方法内部判断 options 对象上 render 方法是否存在,如果不存在会调用 compile 方法将 template 模板编译为 render function。我们将在下篇文章中讨论将 template 目标编译为 render function 的过程

这里的 render 是由 template 编译而来的 render function,并不是本系列文章第二篇中提到的 render 方法 packages/runtime-core/src/renderer.ts#render

下篇文章中我们会开始介绍 compile 方法,也就是极其重要的 Vue3 的编译系统。

4.注册compiler

当我们传给 createApp 方法的根选项 options 没有 render 属性时,Vue 会调用 compile 方法将 template 模板编译为 render code。接下来我们看一下编译系统(compile)是如何注入到运行时的。

(1). finishComponentSetup

// packages/runtime-core/src/component.ts

// 在 packages/vue/src/index.ts 中会调用 registerRuntimeCompiler 将 compile 注册进来,
// 以便在 finishComponentSetup 中需要的时候进行调用
/**
 * For runtime-dom to register the compiler.
 * Note the exported method uses any to avoid d.ts relying on the compiler types.
 */
export function registerRuntimeCompiler(_compile: any) {
  compile = _compile
}

function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions

  // template / render function normalization
  if (__NODE_JS__ && isSSR) {
    // SSR。。。
  } else if (!instance.render) {
    // could be set from setup()
    // 判断 render 方法是否存在,如果不存在会调用 compile 方法将 template 转化为 render。
    if (compile && Component.template && !Component.render) {
      if (__DEV__) {
        startMeasure(instance, `compile`)
      }
      Component.render = compile(Component.template, {
        isCustomElement: instance.appContext.config.isCustomElement,
        delimiters: Component.delimiters
      })
      if (__DEV__) {
        endMeasure(instance, `compile`)
      }
    }

    instance.render = (Component.render || NOOP) as InternalRenderFunction

    // for runtime-compiled render functions using `with` blocks, the render
    // proxy used needs a different `has` handler which is more performant and
    // also only allows a whitelist of globals to fallthrough.
    if (instance.render._rc) {
      instance.withProxy = new Proxy(
        instance.ctx,
        RuntimeCompiledPublicInstanceProxyHandlers
      )
    }
  }
  // ...
}

(2). compileToFunction

// packages/vue/src/index.ts
import { compile, CompilerOptions, CompilerError } from '@vue/compiler-dom'
// registerRuntimeCompiler 用于注入 compile 方法
import { registerRuntimeCompiler, RenderFunction, warn } from '@vue/runtime-dom'
// ...

// 封装 compile 方法
function compileToFunction(
  template: string | HTMLElement,
  options?: CompilerOptions
): RenderFunction {
  if (!isString(template)) {
    // 如果 template 不是字符串
    // 则认为是一个 DOM 节点,获取 innerHTML
    if (template.nodeType) {
      template = template.innerHTML
    } else {
      __DEV__ && warn(`invalid template option: `, template)
      return NOOP
    }
  }

  const key = template
  const cached = compileCache[key]
  // 如果缓存中存在,直接从缓存中获取
  if (cached) {
    return cached
  }

  // 如果是 ID 选择器,这获取 DOM 元素后,取 innerHTML
  if (template[0] === '#') {
    const el = document.querySelector(template)
    if (__DEV__ && !el) {
      warn(`Template element not found or is empty: ${template}`)
    }
    // __UNSAFE__
    // Reason: potential execution of JS expressions in in-DOM template.
    // The user must make sure the in-DOM template is trusted. If it's rendered
    // by the server, the template should not contain any user data.
    template = el ? el.innerHTML : ``
  }

  // 调用 compile 获取 render code
  let { code } = compile(
    template,
    extend(
      {
        hoistStatic: true,
        onError(err: CompilerError) {
          if (__DEV__) {
            const message = `Template compilation error: ${err.message}`
            const codeFrame =
              err.loc &&
              generateCodeFrame(
                template as string,
                err.loc.start.offset,
                err.loc.end.offset
              )
            warn(codeFrame ? `${message}\n${codeFrame}` : message)
          } else {
            /* istanbul ignore next */
            throw err
          }
        }
      },
      options
    )
  )

  // The wildcard import results in a huge object with every export
  // with keys that cannot be mangled, and can be quite heavy size-wise.
  // In the global build we know `Vue` is available globally so we can avoid
  // the wildcard object.
  const render = (__GLOBAL__
    ? new Function(code)()
    : new Function('Vue', code)(runtimeDom)) as RenderFunction

  // mark the function as runtime compiled
  ;(render as InternalRenderFunction)._rc = true

  // 返回 render 方法的同时,将其放入缓存
  return (compileCache[key] = render)
}

// 向运行时注入 compile
registerRuntimeCompiler(compileToFunction)

从上面可以看到, compile 方法是通过 registerRuntimeCompiler(compileToFunction) 注入到运行时的,运行时调用 compile 编译 template 得到 render code (而不是 render function),然后经过:

const render = (__GLOBAL__
    ? new Function(code)()
    : new Function('Vue', code)(runtimeDom)) as RenderFunction

将 render code 转换为 render function,之后会将 render function 挂载到 instance.render 属性上。

前面提到, compile 方法是通过 registerRuntimeCompiler(compileToFunction) 注入到运行时的,而 compileToFunction 其实封装了 @vue/compiler-dom 里面导出的 compile 方法,下一章我们将介绍一下这个 compile 方法,也就是 Vue3 的编译时。

Vue3 源码解读