likes
comments
collection
share

vue3源码与应用-createApp

作者站长头像
站长
· 阅读数 43

vue3源码与应用-createApp

前言

1. vue版本:3.2.47
2. 该系列文章为个人学习总结,如有错误,欢迎指正;尚有不足,请多指教!
3. 阅读源码暂未涉及ssr服务端渲染,直接跳过
4. 部分调试代码(例如:console.warn等),不涉及主要内容,直接跳过
5. 涉及兼容vue2的代码直接跳过(例如:__FEATURE_OPTIONS_API__等)
6. 注意源码里的`__DEV__`不是指`dev`调试,详情请看`rollup.config.js`

构建App

import Vue from 'vue'
// vue2使用单例模式构建app
new Vue({
  el: '#app',
  router,
  render: h => h(App)
});
// vue3使用工厂模式创建app
import { createApp } from 'vue'
const app = createApp(App)
app.use(router)
app.mount('#app');

vue3通过执行createApp函数替换vue2中使用new Vue来创建实例。两个版本使用不同的设计模式构建实例,如果只有一个app,差别不大。但是如果存在两个或以上app,vue2这种将全局变量注册在vue原型上就容易造成混淆。例如:

Vue.directive('directive1', {
  /* ... */
});
Vue.directive('directive2', {
  /* ... */
});
// 无法隔离注册到两个独立的app
const vue2App1 = new Vue();
const vue2App2 = new Vue();

createApp实现

// @file core/packages/runtime-dom/src/index.ts

// nodeOps,封装dom节点操作方法
// patchProp 更新dom属性的方法,后面用到时会详解
const rendererOptions = /*#__PURE__*/ extend({ patchProp }, nodeOps)
// 全局缓存renderer
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer
function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

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

入口函数createApp调用ensureRenderer方法获取Renderer对象并缓存,Renderer对象由createRenderer方法创建。然后调用Renderer对象上的createApp来创建app;

// @file core/packages/runtime-core/src/renderer.ts

// 可自定义创建render,用于扩展(不限于浏览器)渲染器
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  // ...省略
  return {
    render,
    hydrate, // ssr相关,忽略
    createApp: createAppAPI(render, hydrate)
  }
}

createRenderer方法,传入的参数是RendererOptions类型,它其实是一系列操作节点数据方法的集合。我们开发web项目时,这里的参数就是上文的rendererOptions,有兴趣的可以看看(/core/packages/runtime-dom/src/nodeOpts.ts/nodeOps)nodeOps属性,它其实就是一系列dom方法的封装。如果我们基于在其他环境(非浏览器),也只需要传入类似nodeOpsRendererOptions变量,就可以生成自定义的渲染器,来构建适配的数据模型。 渲染器其实就是baseCreateRenderer返回包含{render, createApp}的对象。这里的createAppAPI执行返回的是一个函数,也就是我们项目开始调用的createApp的代码:

// @file core/packages/runtime-core/src/apiCreateApp.ts

/**
 * 生成app上下文属性
 */
export function createAppContext(): AppContext {
  return {
    app: null as any,
    config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      errorHandler: undefined,
      warnHandler: undefined,
      compilerOptions: {}
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null),
    optionsCache: new WeakMap(),
    propsCache: new WeakMap(),
    emitsCache: new WeakMap()
  }
}

let uid = 0
export function createAppAPI<HostElement>(
  render: RootRenderFunction<HostElement>,
  hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
    // 1. rootComponent为对象,浅拷贝做组件数app
    // 2. 如果为函数,做函数式组件
    if (!isFunction(rootComponent)) {
      rootComponent = { ...rootComponent }
    }

    if (rootProps != null && !isObject(rootProps)) {
      // 根组件传入的属性对象,必须为对象类型
      rootProps = null
    }

    // 创建app上下文,主要是初始化一些属性
    const context = createAppContext()
    // 使用Set存储已安装的插件,防止重复注册
    const installedPlugins = new Set()

    let isMounted = false

    // 这里循环引用
    // app._context = context
    // context.app = app
    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)) {
          // 1. 传入对象中携带install方法属性,作为安装方法
          installedPlugins.add(plugin)
          plugin.install(app, ...options)
        } else if (isFunction(plugin)) {
          // 2. 传入方法作为安装方法
          installedPlugins.add(plugin)
          plugin(app, ...options)
        }
        return app
      },

      // 全局注册组件
      component(name: string, component?: Component): any {
        if (!component) {
          return context.components[name]
        }
        context.components[name] = component
        return app
      },

      // 全局注册指令
      directive(name: string, directive?: Directive) {
        if (!directive) {
          return context.directives[name] as any
        }
        context.directives[name] = directive
        return app
      },

      // 挂载组件,后续详解
      mount(
        rootContainer: HostElement,
        isHydrate?: boolean,
        isSVG?: boolean
      ): any {
        // ...省略
      },

      // 卸载组件,后续详解
      unmount() {
        // ...省略
      },

      // 全局注入属性,添加到上下文
      provide(key, value) {
        context.provides[key as string | symbol] = value
        return app
      }
    })

    return app
  }
}

createApp方法最终返回的是一个App对象,主要包含:

  • _context,app上下文
  • config,app全局配置属性
  • use,插件注册方法
  • mixins,vu2的属性(不推荐vue3使用)
  • component,组件注册方法
  • directive,指令注册方法
  • mount,app挂载方法
  • unmount,app卸载方法
  • provide,全局注入属性注册方法

其中mixinsdirectiveprovidecomponent都是直接在app上下文中直接注册(例如注册指令就是往context.directives注册)对应的全局属性,方便子孙组件直接调用。

插件的注册方式与上面几个函数不同:插件函数或插件函数的执行结果不需要传递至子孙组件,所以插件都是注册在一个闭包installedPluginsset集合上,存入set集合只是为了避免重复注册。一般来讲,插件注册全局属性是通过插件函数执行时,间接的向传入的app参数的config.globalProperties对象上注册全局属性(属性注册的时候有注意重名的风险)。这里要注意:如果插件函数内存在异步调用后注册,可能子孙组件无法访问插件相关属性。

除了mountunmount,另外几个方法都返回了app实例以便链式调用。因为vue子孙组件实例创建、挂载都是同步任务,如果mount后再注册全局属性其实是访问不到的(异步挂载组件Suspense除外,后续讨论),所以mount方法没有链式调用的必要,为了避免意外的错误,还是建议在mount之前注册全局属性。unmount卸载后也无需其他的操作。

转载自:https://juejin.cn/post/7203285698074247223
评论
请登录