乐趣区

关于前端:vue3源码十一初始vue3中的渲染器

【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中蕴含了大量的函数用来解决挂载组件、更新组件等操作。

退出移动版