• 【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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    function ensureRenderer() {
      return (
        renderer ||
        (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    export function createRenderer<
      HostNode = RendererNode,
      HostElement = RendererElement
    >(options: RendererOptions<HostNode, HostElement>) {
      return baseCreateRenderer<HostNode, HostElement>(options)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    至此,我们就找到了真正创建渲染器的方法baseCreateRenderer。当我们找到baseCreateRenderer的具体实现,你会发现这个函数是十分长的,单baseCreateRenderer这一个函数就占据了2044行代码,其中更是声明了30+个函数。

    在此我们先不用关心这些函数的作用,在后续介绍组件加载及更新过程时,你会慢慢了解这些函数。

    接下来我们继续看渲染器对象的结构。

    渲染器

    return {
      render,
      hydrate,
      createApp: createAppAPI(render, hydrate)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    export const createApp = ((...args) => {
      const app = ensureRenderer().createApp(...args)
      // ...
      
      return app
    }) as CreateAppFunction<Element>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注意这里不要两个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) {
        // ...
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

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

    createApp完整代码
    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188

    这个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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    然后调用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()
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    然后声明了一个installedPlugins集合和一个布尔类型的isMounted。其中installedPlugins会用来存储使用use安装的pluginisMounted代表根组件是否已经挂载。

    const installedPlugins = new Set()
    let isMounted = false
    
    • 1
    • 2

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

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

    然后会处理vue2的兼容。这里我们暂时不深究vue2的兼容处理。

    if (__COMPAT__) {
      installAppCompatProperties(app, context, render)
    }
    
    • 1
    • 2
    • 3

    最后返回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) { //... }
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • _uidapp的唯一标识,每次都会使用uid为新app的唯一标识,在赋值后,uid会进行自增,以便下一个app使用
    • _component:根组件
    • _props:根组件所需的props
    • _container:需要将根组件渲染到的容器
    • _contextapp的上下文
    • _instance:根组件的实例
    • versionvue的版本
    • get config:获取上下文中的config
      get config() {
        return context.config
      }
      
      • 1
      • 2
      • 3
    • set config:拦截app.configset操作,防止app.config被修改
      set config(v) {
        if (__DEV__) {
          warn(
            `app.config cannot be replaced. Modify individual options instead.`
          )
        }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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)\``
        )
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    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.`)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    总结

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

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

  • 相关阅读:
    python 连接配置SSL证书的Minio服务
    yum源配置
    重识Nginx - 01 Nginx 主要应用场景及版本概述
    数字货币中短线策略(数据+回测+实盘)
    【公式输入】 latex和markdown支持的公式写法整理
    深入分析 Java 的序列化与反序列化
    jvm中对象内存空间的分配与回收
    【AGC】典型问题FAQ 5
    使用spring-boot-starter-jdbc访问MySQL大部分程序员都不一定会
    Linux下安装mongodb详细教程
  • 原文地址:https://blog.csdn.net/qq_33635385/article/details/126800409