Vue3读源码系列(一):根组件渲染

202 阅读6分钟

本系列阅读的是vue3.3版本的源码。在列举源码的时候会省略部分源码,只保留与讲解内容相关的代码(运行环境代码块和兼容以前版本的代码也可能被省略),以此来方便读者理解。

源码阅读方式

我们通常会结合案例和debugger去看源码,这样我们能清楚的沿着代码执行的脉络去屡清楚代码执行的逻辑、以及观察到执行到每一步各个变量的变化。

当然,我们最好是分块、有目的的去看,比如看根组件的挂载过程;怎么加载组件;怎么更新组件;如何实现响应式;diff算法是怎么实现的等等。我们不必想着一下把所有东西都搞清楚,这样势必会被源码复杂的逻辑给搞晕。

代码调试

package/vue/examples中有三类示例,classic还是使用的options API,composition使用的是vue3升级的重点composition API,transition则是transition内置组件的案例,我们应该主要关注composition文件夹里的实例。

我们需要先到根目录打开终端,运行pnpm dev命令,会发现vue文件夹下出现一个dist包,其中就是vue的浏览器可用js文件了。这是我们就可以直接运行example中的实例了。

浏览器打开开发者工具,选中sources,选择对应的运行html文件就可以打上断点去一步步的看源码啦!

vscode插件推荐 bookmarks

bookmarks插件可以帮我们给某行代码打上一个书签(command+option+k),可以帮助我们快速地定位到已经标记过的地方,对于阅读庞大的源码来说非常有用。

根组件渲染

createApp

vue3相较于vue2使用了新的创建app的方法:createApp,我们在createApp代码行打上断点并进入到函数实现,会是先面一段代码

// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    ...
  }
  return app
})

我们可以看到,app的创建并不是由当前文件的createApp函数创建的,而是先执行ensureRenderer函数,再调用此函数返回值上的createApp创建的。

baseCreateRenderer

那我们接下来看看ensureRenderer是干啥的。点进去函数发现套了两层娃:ensureRenderer => createRenderer => baseCreateRenderer,最终是执行baseCreateRenderer函数

我们点进baseCreateRenderer函数一看发现两千行代码!!不要慌,记住阅读代码的宗旨,我们要带着目的性,我们只想看他返回了啥,然后找到createApp就可以了:

// packages/runtime-core/src/renderer.ts
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  ...
  return {
    render, // 把虚拟DOM(第一个参数)转换成原生DOM,追加到宿主元素上(第二个参数)
    hydrate,  // SSR,将一个vnode转换成html字符串
    createApp: createAppAPI(render, hydrate)
  }
}

我们发现baseCreateRenderer函数返回了一个对象,里面有一个属性叫createApp,这个属性的值是createAppAPI的返回值。其实baseCreateRenderer函数返回的对象我们称之为渲染器,他可以帮助我们处理浏览器端和服务端的渲染,同时他也是赋予vue框架跨平台能力的关键。

createAppAPI

进入到createAppAPI函数,其返回了一个createApp函数,这才是createApp的最终实现函数

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    if (!isFunction(rootComponent)) {
      // 浅拷贝 extend = Object.assign
      rootComponent = extend({}, rootComponent)
    }

    if (rootProps != null && !isObject(rootProps)) {
      __DEV__ && warn(`root props passed to app.mount() must be an object.`)
      rootProps = null
    }
    // 创建app上下文 此上下文将绑定到每个组件实例
    const context = createAppContext()
    const installedPlugins = new Set()

    let isMounted = false
    // app实例
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      _instance: null,

      version,

      get config() {
        return context.config
      },

      set config(v) {...},
      // 注册plugin
      use(plugin: Plugin, ...options: any[]) {...},
      // 全局混入
      mixin(mixin: ComponentOptions) {...},
      // 注册全局组件
      component(name: string, component?: Component): any {...},
      // 注册全局指令
      directive(name: string, directive?: Directive) {...},
      // 挂载根组件
      mount(
        rootContainer: HostElement, // 挂载容器
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {...},
      // 卸载
      unmount() {...},
      // 分享数据
      provide(key, value) {...}
    })
    // 返回app
    return app
  }
}

可以看到createApp的内容并不复杂,就是创建并返回了一个app对象,这个对象相信我们应该非常熟悉了,里面有很多我们常用的方法。

挂载(mount)

创建完app我们就要去挂载根组件了,执行mount方法,这里我们要注意,这里执行的mount方法并不是createAppAPI函数返回的app.mount,在最开始的createApp中,对app.mount进行了重新赋值:

// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    ...
  }
  return app
})

可以看到,他先解构得到mount保存了一份createAppAPI创建app的mount,然后才重新赋值,这样做的原因是我们可以看下具体实现:

// packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    // 根组件容器 选择器字符串兼容
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
    // rootComponent
    const component = app._component
    // 如果不是函数式组件 且 根组件没有render方法和template选项
    if (!isFunction(component) && !component.render && !component.template) {
      // 根组件template赋值为 容器dom.innerHTML
      component.template = container.innerHTML
      // 2.x compat check
      if (__COMPAT__ && __DEV__) {
        ...
      }
    }
    // clear content before mounting
    // 挂载前清除容器innerHTML
    container.innerHTML = ''
    // 调用初始app.mount
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      // 组件实例挂载后移除v-cloak属性
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }
  return app
})

可以看到先调用normalizeContainer兼容传入字符串选择器,调用querySelector获取dom。第12行会判断根组件对象有没有render和template选项,没有的话会将容器的innerHTML赋值给template属性。然后下面就是清除容器内容以及调用事先保存的mount方法去实现根组件的挂载了。

所以这里mount执行的主要目的是将容器内容作为没有定义render和template根组件的内容

真正的挂载mount

// packages/runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    ...
    // app实例
    const app: App = (context.app = {
      ...
      // 挂载根组件
      mount(
        rootContainer: HostElement, // 挂载容器
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        // 判断挂载状态
        if (!isMounted) {
          if (__DEV__ && (rootContainer as any).__vue_app__) {
            ...
          }
          // 创建根组件vnode
          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__) {
            ...
          }
          // 判断是否是服务器端渲染
          if (isHydrate && hydrate) {
            hydrate(vnode as VNode<Node, Element>, rootContainer as any)
          } else {
            // 渲染到根dom
            render(vnode, rootContainer, isSVG)
          }
          isMounted = true
          app._container = rootContainer
          // for devtools and telemetry
          ;(rootContainer as any).__vue_app__ = app

          if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
            app._instance = vnode.component
            devtoolsInitApp(app, version)
          }
          // 返回根组件实例的exposed代理或者是组件实例
          // vnode.component是组件内部实例 类型是ComponentInternalInstance
          // component.proxy是组件公共实例 类型是ComponentPublicInstance 我们在组件内部使用this访问的就是此实例上的属性或方法
          return getExposeProxy(vnode.component!) || vnode.component!.proxy
        } else if (__DEV__) {
          ...
        }
      },
      ...
    })
    // 返回app
    return app
  }
}

createVNode可以看成是我们熟知的createElement或者h函数,他创建一个vnode对象,然后使用render函数,将vnode挂在到指定dom,这样就实现了根组件的挂载。

这里的render其实就是我们上面说的渲染器中的render方法:

const render: RootRenderFunction = (vnode, container, isSVG) => {
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    patch(container._vnode || null, vnode, container, null, null, null, isSVG)
  }
  flushPreFlushCbs()
  flushPostFlushCbs()
  container._vnode = vnode
}

可以看到其最终调用了patch函数,他可以实现vnode的挂载和更新。他的实现比较复杂,我们会在后面的章节详细看其具体实现,此章节我们只需要知道他的功能即可。至此我们已经理清楚了根组件的挂载流程。