vue3源码与应用-createApp
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
方法的封装。如果我们基于在其他环境(非浏览器),也只需要传入类似nodeOps
的RendererOptions
变量,就可以生成自定义的渲染器,来构建适配的数据模型。
渲染器其实就是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,全局注入属性注册方法
其中mixins
,directive
,provide
,component
都是直接在app
上下文中直接注册(例如注册指令就是往context.directives
注册)对应的全局属性,方便子孙组件直接调用。
插件的注册方式与上面几个函数不同:插件函数或插件函数的执行结果不需要传递至子孙组件,所以插件都是注册在一个闭包installedPlugins
的set
集合上,存入set
集合只是为了避免重复注册。一般来讲,插件注册全局属性是通过插件函数执行时,间接的向传入的app
参数的config.globalProperties
对象上注册全局属性(属性注册的时候有注意重名的风险)。这里要注意:如果插件函数内存在异步调用后注册,可能子孙组件无法访问插件相关属性。
除了mount
和unmount
,另外几个方法都返回了app
实例以便链式调用。因为vue
子孙组件实例创建、挂载都是同步任务,如果mount
后再注册全局属性其实是访问不到的(异步挂载组件Suspense除外,后续讨论),所以mount
方法没有链式调用的必要,为了避免意外的错误,还是建议在mount
之前注册全局属性。unmount
卸载后也无需其他的操作。
转载自:https://juejin.cn/post/7203285698074247223