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

23次阅读

共计 11906 个字符,预计需要花费 30 分钟才能阅读完成。

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

正文完
 0