【vue3源碼】十一翠语、初始vue3中的渲染器

【vue3源碼】十一叽躯、初始vue3中的渲染器

在介紹渲染器之前。我們先簡單了解下渲染器的作用是什么肌括。

渲染器最主要的任務(wù)就是將虛擬DOM渲染成真實的DOM對象到對應(yīng)的平臺上点骑,這里的平臺可以是瀏覽器DOM平臺,也可以是其他諸如canvas的一些平臺谍夭『诘危總之vue3的渲染器提供了跨平臺的能力。

渲染器的生成

當(dāng)使用createApp創(chuàng)建應(yīng)用實例時紧索,會首先調(diào)用一個ensureRenderer方法袁辈。

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

ensureRenderer函數(shù)會返回一個渲染器renderer,這個renderer是個全局變量齐板,如果不存在吵瞻,會使用createRenderer方法進行創(chuàng)建,并將創(chuàng)建好的renderer賦值給這個全局變量甘磨。

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

createRenderer函數(shù)接收一個options參數(shù)橡羞,至于這個options中是什么,這里我們暫且先不深究济舆。createRenderer函數(shù)中會調(diào)用baseCreateRenderer函數(shù)卿泽,并返回其結(jié)果。

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

至此滋觉,我們就找到了真正創(chuàng)建渲染器的方法baseCreateRenderer签夭。當(dāng)我們找到baseCreateRenderer的具體實現(xiàn),你會發(fā)現(xiàn)這個函數(shù)是十分長的椎侠,單baseCreateRenderer這一個函數(shù)就占據(jù)了2044行代碼第租,其中更是聲明了30+個函數(shù)。

在此我們先不用關(guān)心這些函數(shù)的作用我纪,在后續(xù)介紹組件加載及更新過程時慎宾,你會慢慢了解這些函數(shù)丐吓。

接下來我們繼續(xù)看渲染器對象的結(jié)構(gòu)。

渲染器

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

baseCreateRenderer最后返回了一個對象趟据,這個對象包含了三個屬性:render(渲染函數(shù))券犁、hydrate(同構(gòu)渲染)、createApp汹碱。這里的createApp是不是很熟悉粘衬,在createApp中調(diào)用ensureRenderer方法后面會緊跟著調(diào)用了createApp函數(shù):

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

注意這里不要兩個createApp混淆了。渲染器中的createApp并不是我們平時使用到的createApp咳促。當(dāng)我們調(diào)用createApp方法進行創(chuàng)建實例時稚新,會調(diào)用渲染器中的createApp生成app實例。

接下來我們來看下渲染器中的createApp等缀。首先createApp方法通過一個createAppAPI方法生成枷莉,這個方法接收渲染器中的renderhydrate

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

createAppAPI函數(shù)會返回一個閉包函數(shù)createApp。這個createApp就是通過ensureRenderer().createApp(...args)調(diào)用的方法了尺迂。接下來看createApp的具體實現(xiàn):

<details>
<summary><code>createApp</code>完整代碼</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函數(shù)與vue提供的createApp一樣笤妙,都接受一個根組件參數(shù)和一個rootProps(根組件的props)參數(shù)诵盼。

首先惜浅,如果根組件不是方法時俄讹,會將rootComponent使用解構(gòu)的方式重新賦值為一個新的對象整袁,然后判斷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
}

然后調(diào)用createAppContext()方法創(chuàng)建一個上下文對象天吓。

export function createAppContext(): AppContext {
  return {
    app: null as any,
    config: {
      // 一個判斷是否為原生標(biāo)簽的函數(shù)
      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(),
    // 緩存每個組件經(jīng)過標(biāo)準(zhǔn)化的的props options
    propsCache: new WeakMap(),
    // 緩存每個組件經(jīng)過標(biāo)準(zhǔn)化的的emits options
    emitsCache: new WeakMap()
  }
}

然后聲明了一個installedPlugins集合和一個布爾類型的isMounted苍凛。其中installedPlugins會用來存儲使用use安裝的pluginisMounted代表根組件是否已經(jīng)掛載兵志。

const installedPlugins = new Set()
let isMounted = false

緊接著聲明了一個app變量醇蝴,這個app變量就是app實例,在創(chuàng)建app的同時會將app添加到上下文中的app屬性中想罕。

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

然后會處理vue2的兼容悠栓。這里我們暫時不深究vue2的兼容處理。

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

最后返回app按价。至此createaApp執(zhí)行完畢惭适。

app應(yīng)用實例

接下來我們看下app實例的構(gòu)造:

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的唯一標(biāo)識,每次都會使用uid為新app的唯一標(biāo)識楼镐,在賦值后癞志,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今阳。對于重復(fù)安裝多次的plugin师溅,只會進行安裝一次,這都依靠installedPlugins盾舌,每次安裝新的plugin后,都會將plugin存入installedPlugins蘸鲸,這樣如果再次安裝同樣的plugin妖谴,就會避免多次安裝。

use(plugin: Plugin, ...options: any[]) {
  // 如果已經(jīng)安裝過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)
    // 調(diào)用plugin.install
    plugin.install(app, ...options)
  } else if (isFunction(plugin)) { 如果plugin是方法
    // 將plugin添加到installedPlugins
    installedPlugins.add(plugin)
    // 調(diào)plugin
    plugin(app, ...options)
  } else if (__DEV__) {
    warn(
      `A plugin must either be a function or an object with an "install" ` +
        `function.`
    )
  }
  // 最后返回app,以便可以鏈?zhǔn)秸{(diào)用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對應(yīng)的組件千康。被注冊的組件會被存在上下文中的components中享幽。

component(name: string, component?: Component): any {
  // 驗證組件名是否符合要求
  if (__DEV__) {
    validateComponentName(name, context.config)
  }
  // 如果不存在component,那么會返回name對應(yīng)的組件
  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對應(yīng)的指令對象值桩。注冊的全局指令會被存入上下文中的directives中。

directive(name: string, directive?: Directive) {
  // 驗證指令名稱
  if (__DEV__) {
    validateDirectiveName(name)
  }
  // 如果不存在directive豪椿,則返回name對應(yīng)的指令對象
  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奔坟。創(chuàng)建完渲染器,執(zhí)行完渲染器的createApp后搭盾,會重寫mount方法咳秉,我們使用的mount方法是被重寫的mount方法。

mount(
  rootContainer: HostElement,
  isHydrate?: boolean,
  isSVG?: boolean
): any {
  // 如果未掛載增蹭,開始掛載
  if (!isMounted) {
    // 如果存在rootContainer.__vue_app__滴某,說明容器中已經(jīng)存在一個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.`
      )
    }
    // 創(chuàng)建根組件的虛擬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)
      }
    }

    // 同構(gòu)渲染
    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__) { // 已經(jīng)掛載了
    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()

卸載應(yīng)用實例滋迈。

unmount() {
  // 如果已經(jīng)掛載才能進行卸載
  if (isMounted) {
    // 調(diào)用redner函數(shù)霎奢,此時虛擬節(jié)點為null,代表會清空容器中的內(nèi)容
    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()

全局注入一些數(shù)據(jù)饼灿。這些數(shù)據(jù)會被存入上下文對象的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
}

總結(jié)

vue3中的渲染器主要作用就是將虛擬DOM轉(zhuǎn)為真實DOM渲染到對應(yīng)平臺中,在這個渲染過程中會包括DOM的掛載碍彭、DOM的更新等操作晤硕。

通過baseCreateRenderer方法會創(chuàng)建一個渲染器renderer悼潭,renderer中有三個方法:renderhydrate舞箍、createApp舰褪,其中render方法用來進行客戶端渲染,hydrate用來進行同構(gòu)渲染疏橄,createApp用來創(chuàng)建app實例占拍。baseCreateRenderer中包含了大量的函數(shù)用來處理掛載組件、更新組件等操作捎迫。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末晃酒,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子窄绒,更是在濱河造成了極大的恐慌贝次,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件彰导,死亡現(xiàn)場離奇詭異蛔翅,居然都是意外死亡,警方通過查閱死者的電腦和手機螺戳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門搁宾,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人倔幼,你說我怎么就攤上這事盖腿。” “怎么了损同?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵翩腐,是天一觀的道長。 經(jīng)常有香客問我膏燃,道長茂卦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任组哩,我火速辦了婚禮等龙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘伶贰。我一直安慰自己蛛砰,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布黍衙。 她就那樣靜靜地躺著泥畅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪琅翻。 梳的紋絲不亂的頭發(fā)上位仁,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天柑贞,我揣著相機與錄音,去河邊找鬼聂抢。 笑死钧嘶,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的涛浙。 我是一名探鬼主播康辑,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼轿亮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起胸墙,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤我注,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后迟隅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體但骨,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年智袭,在試婚紗的時候發(fā)現(xiàn)自己被綠了奔缠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡吼野,死狀恐怖校哎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瞳步,我是刑警寧澤闷哆,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站单起,受9級特大地震影響抱怔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嘀倒,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一屈留、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧测蘑,春花似錦灌危、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至固逗,卻和暖如春浅蚪,著一層夾襖步出監(jiān)牢的瞬間藕帜,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工惜傲, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留洽故,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓盗誊,卻偏偏與公主長得像时甚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子哈踱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容