【vue3源码】十一、初始vue3中的渲染器

在介绍渲染器之前。咱们先简略理解下渲染器的作用是什么。

渲染器最次要的工作就是将虚构DOM渲染成实在的DOM对象到对应的平台上,这里的平台能够是浏览器DOM平台,也能够是其余诸如canvas的一些平台。总之vue3的渲染器提供了跨平台的能力。

渲染器的生成

当应用createApp创立利用实例时,会首先调用一个ensureRenderer办法。

export const createApp = ((...args) => {  const app = ensureRenderer().createApp(...args)  // ...    return app}) as CreateAppFunction<Element>

ensureRenderer函数会返回一个渲染器renderer,这个renderer是个全局变量,如果不存在,会应用createRenderer办法进行创立,并将创立好的renderer赋值给这个全局变量。

function ensureRenderer() {  return (    renderer ||    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))  )}

createRenderer函数接管一个options参数,至于这个options中是什么,这里咱们暂且先不深究。createRenderer函数中会调用baseCreateRenderer函数,并返回其后果。

export function createRenderer<  HostNode = RendererNode,  HostElement = RendererElement>(options: RendererOptions<HostNode, HostElement>) {  return baseCreateRenderer<HostNode, HostElement>(options)}

至此,咱们就找到了真正创立渲染器的办法baseCreateRenderer。当咱们找到baseCreateRenderer的具体实现,你会发现这个函数是非常长的,单baseCreateRenderer这一个函数就占据了2044行代码,其中更是申明了30+个函数。

在此咱们先不必关怀这些函数的作用,在后续介绍组件加载及更新过程时,你会缓缓理解这些函数。

接下来咱们持续看渲染器对象的构造。

渲染器

return {  render,  hydrate,  createApp: createAppAPI(render, hydrate)}

baseCreateRenderer最初返回了一个对象,这个对象蕴含了三个属性:render(渲染函数)、hydrate(同构渲染)、createApp。这里的createApp是不是很相熟,在createApp中调用ensureRenderer办法前面会紧跟着调用了createApp函数:

export const createApp = ((...args) => {  const app = ensureRenderer().createApp(...args)  // ...    return app}) as CreateAppFunction<Element>

留神这里不要两个createApp混同了。渲染器中的createApp并不是咱们平时应用到的createApp。当咱们调用createApp办法进行创立实例时,会调用渲染器中的createApp生成app实例。

接下来咱们来看下渲染器中的createApp。首先createApp办法通过一个createAppAPI办法生成,这个办法接管渲染器中的renderhydrate

export function createAppAPI<HostElement>(  render: RootRenderFunction,  hydrate?: RootHydrateFunction): CreateAppFunction<HostElement> {  return function createApp(rootComponent, rootProps = null) {    // ...  }}

createAppAPI函数会返回一个闭包函数createApp。这个createApp就是通过ensureRenderer().createApp(...args)调用的办法了。接下来看createApp的具体实现:

<details>
<summary>createApp残缺代码</summary>

function createApp(rootComponent, rootProps = null) {  if (!isFunction(rootComponent)) {    rootComponent = { ...rootComponent }  }  if (rootProps != null && !isObject(rootProps)) {    __DEV__ && warn(`root props passed to app.mount() must be an object.`)    rootProps = null  }  const context = createAppContext()  const installedPlugins = new Set()  let isMounted = false  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) {      if (__DEV__) {        warn(          `app.config cannot be replaced. Modify individual options instead.`        )      }    },    use(plugin: Plugin, ...options: any[]) {      if (installedPlugins.has(plugin)) {        __DEV__ && warn(`Plugin has already been applied to target app.`)      } else if (plugin && isFunction(plugin.install)) {        installedPlugins.add(plugin)        plugin.install(app, ...options)      } else if (isFunction(plugin)) {        installedPlugins.add(plugin)        plugin(app, ...options)      } else if (__DEV__) {        warn(          `A plugin must either be a function or an object with an "install" ` +            `function.`        )      }      return app    },    mixin(mixin: ComponentOptions) {      if (__FEATURE_OPTIONS_API__) {        if (!context.mixins.includes(mixin)) {          context.mixins.push(mixin)        } else if (__DEV__) {          warn(            'Mixin has already been applied to target app' +              (mixin.name ? `: ${mixin.name}` : '')          )        }      } else if (__DEV__) {        warn('Mixins are only available in builds supporting Options API')      }      return app    },    component(name: string, component?: Component): any {      if (__DEV__) {        validateComponentName(name, context.config)      }      if (!component) {        return context.components[name]      }      if (__DEV__ && context.components[name]) {        warn(`Component "${name}" has already been registered in target app.`)      }      context.components[name] = component      return app    },    directive(name: string, directive?: Directive) {      if (__DEV__) {        validateDirectiveName(name)      }      if (!directive) {        return context.directives[name] as any      }      if (__DEV__ && context.directives[name]) {        warn(`Directive "${name}" has already been registered in target app.`)      }      context.directives[name] = directive      return app    },    mount(      rootContainer: HostElement,      isHydrate?: boolean,      isSVG?: boolean    ): any {      if (!isMounted) {        // #5571        if (__DEV__ && (rootContainer as any).__vue_app__) {          warn(            `There is already an app instance mounted on the host container.\n` +              ` If you want to mount another app on the same host container,` +              ` you need to unmount the previous app by calling \`app.unmount()\` first.`          )        }        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, isSVG)          }        }        if (isHydrate && hydrate) {          hydrate(vnode as VNode<Node, Element>, rootContainer as any)        } else {          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)        }        return getExposeProxy(vnode.component!) || 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)\``        )      }    },    unmount() {      if (isMounted) {        render(null, app._container)        if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {          app._instance = null          devtoolsUnmountApp(app)        }        delete app._container.__vue_app__      } else if (__DEV__) {        warn(`Cannot unmount an app that is not mounted.`)      }    },    provide(key, value) {      if (__DEV__ && (key as string | symbol) in context.provides) {        warn(          `App already provides property with key "${String(key)}". ` +            `It will be overwritten with the new value.`        )      }      context.provides[key as string | symbol] = value      return app    }  })  if (__COMPAT__) {    installAppCompatProperties(app, context, render)  }  return app}

</details>

这个createApp函数与vue提供的createApp一样,都承受一个根组件参数和一个rootProps(根组件的props)参数。

首先,如果根组件不是办法时,会将rootComponent应用解构的形式从新赋值为一个新的对象,而后判断rootProps如果不为null并且也不是个对象,则会将rootProps置为null

if (!isFunction(rootComponent)) {  rootComponent = { ...rootComponent }}if (rootProps != null && !isObject(rootProps)) {  __DEV__ && warn(`root props passed to app.mount() must be an object.`)  rootProps = null}

而后调用createAppContext()办法创立一个上下文对象。

export function createAppContext(): AppContext {  return {    app: null as any,    config: {      // 一个判断是否为原生标签的函数      isNativeTag: NO,      performance: false,      globalProperties: {},      // 自定义options的合并策略      optionMergeStrategies: {},      errorHandler: undefined,      warnHandler: undefined,      // 组件模板的运行时编译器选项      compilerOptions: {}    },    // 存储全局混入的mixin    mixins: [],    // 保留全局注册的组件    components: {},    // 保留注册的全局指令    directives: {},    // 保留全局provide的值    provides: Object.create(null),    // 缓存组件被解析过的options(合并了全局mixins、extends、部分mixins)    optionsCache: new WeakMap(),    // 缓存每个组件通过标准化的的props options    propsCache: new WeakMap(),    // 缓存每个组件通过标准化的的emits options    emitsCache: new WeakMap()  }}

而后申明了一个installedPlugins汇合和一个布尔类型的isMounted。其中installedPlugins会用来存储应用use装置的pluginisMounted代表根组件是否曾经挂载。

const installedPlugins = new Set()let isMounted = false

紧接着申明了一个app变量,这个app变量就是app实例,在创立app的同时会将app增加到上下文中的app属性中。

const app: APP = (context.app = { //... })

而后会解决vue2的兼容。这里咱们临时不深究vue2的兼容解决。

if (__COMPAT__) {  installAppCompatProperties(app, context, render)}

最初返回app。至此createaApp执行结束。

app利用实例

接下来咱们看下app实例的结构:

const app: App = (context.app = {  _uid: uid++,  _component: rootComponent as ConcreteComponent,  _props: rootProps,  _container: null,  _context: context,  _instance: null,  version,  get config() { // ... },  set config(v) { // ... },  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) { //... }})
  • _uidapp的惟一标识,每次都会应用uid为新app的惟一标识,在赋值后,uid会进行自增,以便下一个app应用
  • _component:根组件
  • _props:根组件所需的props
  • _container:须要将根组件渲染到的容器
  • _contextapp的上下文
  • _instance:根组件的实例
  • versionvue的版本
  • get config:获取上下文中的config

    get config() {  return context.config}
  • set config:拦挡app.configset操作,避免app.config被批改

    set config(v) {  if (__DEV__) {    warn(      `app.config cannot be replaced. Modify individual options instead.`    )  }}

app.use()

应用app.use办法装置plugin。对于反复装置屡次的plugin,只会进行装置一次,这都依附installedPlugins,每次装置新的plugin后,都会将plugin存入installedPlugins,这样如果再次装置同样的plugin,就会防止屡次装置。

use(plugin: Plugin, ...options: any[]) {  // 如果曾经装置过plugin,则不须要再次装置  if (installedPlugins.has(plugin)) {    __DEV__ && warn(`Plugin has already been applied to target app.`)  } else if (plugin && isFunction(plugin.install)) { // 如果存在plugin,并且plugin.install是个办法    // 将plugin增加到installedPlugins    installedPlugins.add(plugin)    // 调用plugin.install    plugin.install(app, ...options)  } else if (isFunction(plugin)) { 如果plugin是办法    // 将plugin增加到installedPlugins    installedPlugins.add(plugin)    // 调plugin    plugin(app, ...options)  } else if (__DEV__) {    warn(      `A plugin must either be a function or an object with an "install" ` +        `function.`    )  }  // 最初返回app,以便能够链式调用app的办法  return app}

app.mixin()

应用app.mixin进行全局混入,被混入的对象会被存在上下文中的mixins中。留神mixin只会在反对options api的版本中能力应用,在mixin中会通过__FEATURE_OPTIONS_API__进行判断,这个变量会在打包过程中借助@rollup/plugin-replace进行替换。

mixin(mixin: ComponentOptions) {  if (__FEATURE_OPTIONS_API__) {    if (!context.mixins.includes(mixin)) {      context.mixins.push(mixin)    } else if (__DEV__) {      warn(        'Mixin has already been applied to target app' +          (mixin.name ? `: ${mixin.name}` : '')      )    }  } else if (__DEV__) {    warn('Mixins are only available in builds supporting Options API')  }  return app}

app.component()

应用app.compoent全局注册组件,也可用来获取name对应的组件。被注册的组件会被存在上下文中的components中。

component(name: string, component?: Component): any {  // 验证组件名是否符合要求  if (__DEV__) {    validateComponentName(name, context.config)  }  // 如果不存在component,那么会返回name对应的组件  if (!component) {    return context.components[name]  }  if (__DEV__ && context.components[name]) {    warn(`Component "${name}" has already been registered in target app.`)  }  context.components[name] = component  return app}

app.directive()

注册全局指令,也可用来获取name对应的指令对象。注册的全局指令会被存入上下文中的directives中。

directive(name: string, directive?: Directive) {  // 验证指令名称  if (__DEV__) {    validateDirectiveName(name)  }  // 如果不存在directive,则返回name对应的指令对象  if (!directive) {    return context.directives[name] as any  }  if (__DEV__ && context.directives[name]) {    warn(`Directive "${name}" has already been registered in target app.`)  }  context.directives[name] = directive  return app}

app.mount()

此处的app.mount并不是咱们平时应用到的mount。创立完渲染器,执行完渲染器的createApp后,会重写mount办法,咱们应用的mount办法是被重写的mount办法。

mount(  rootContainer: HostElement,  isHydrate?: boolean,  isSVG?: boolean): any {  // 如果未挂载,开始挂载  if (!isMounted) {    // 如果存在rootContainer.__vue_app__,阐明容器中曾经存在一个app实例了,须要先应用unmount进行卸载    if (__DEV__ && (rootContainer as any).__vue_app__) {      warn(        `There is already an app instance mounted on the host container.\n` +          ` If you want to mount another app on the same host container,` +          ` you need to unmount the previous app by calling \`app.unmount()\` first.`      )    }    // 创立根组件的虚构DOM    const vnode = createVNode(      rootComponent as ConcreteComponent,      rootProps    )    // 将上下文增加到根组件虚构dom的appContext属性中    vnode.appContext = context    // HMR root reload    if (__DEV__) {      context.reload = () => {        render(cloneVNode(vnode), rootContainer, isSVG)      }    }    // 同构渲染    if (isHydrate && hydrate) {      hydrate(vnode as VNode<Node, Element>, rootContainer as any)    } else {      // 客户端渲染      render(vnode, rootContainer, isSVG)    }    // 渲染实现后将isMounted置为true    isMounted = true    // 将容器增加到app的_container属性中    app._container = rootContainer    // 将rootContainer.__vue_app__指向app实例    ;(rootContainer as any).__vue_app__ = app    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {      // 将根组件实例赋给app._instance      app._instance = vnode.component      devtoolsInitApp(app, version)    }    // 返回根组件expose的属性    return getExposeProxy(vnode.component!) || 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)\``    )  }}

app.unmount()

卸载利用实例。

unmount() {  // 如果曾经挂载能力进行卸载  if (isMounted) {    // 调用redner函数,此时虚构节点为null,代表会清空容器中的内容    render(null, app._container)    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {      // 将app._instance置空      app._instance = null      devtoolsUnmountApp(app)    }    // 删除容器中的__vue_app__    delete app._container.__vue_app__  } else if (__DEV__) {    warn(`Cannot unmount an app that is not mounted.`)  }}

app.provide()

全局注入一些数据。这些数据会被存入上下文对象的provides中。

provide(key, value) {  if (__DEV__ && (key as string | symbol) in context.provides) {    warn(      `App already provides property with key "${String(key)}". ` +        `It will be overwritten with the new value.`    )  }  context.provides[key as string | symbol] = value  return app}

总结

vue3中的渲染器次要作用就是将虚构DOM转为实在DOM渲染到对应平台中,在这个渲染过程中会包含DOM的挂载、DOM的更新等操作。

通过baseCreateRenderer办法会创立一个渲染器rendererrenderer中有三个办法:renderhydratecreateApp,其中render办法用来进行客户端渲染,hydrate用来进行同构渲染,createApp用来创立app实例。baseCreateRenderer中蕴含了大量的函数用来解决挂载组件、更新组件等操作。