难得有时间看看源码,所以写了个总结分享一下.如果觉得不对欢迎指正.
另外即使没看过源码,知道里面的流程,对面试也挺有帮助的
你将会知道
- Vue如何进行初始化
- 如何实现全局,例如全局组件,指令等的注册,并且可以链式调用
- 为什么app.config.globalProperties可以注册能够被应用内所有组件实例访问到的全局属性的对象
createApp
作为Vue的入口函数
,也就是完成初始化的核心函数.它的作用是将你的应用程序实例化并挂载到一个现有的 HTML 元素
具体来说,一个Vue应用,应该有多个组件,而createApp
的任务是渲染这些组件中最上层组件(根组件
)
因为与渲染相关,所以 渲染器 为createApp提供了渲染能力
.
源码位置: packages\runtime-core\src\apiCreateApp.ts
先说一下代码结构,createApp
由baseCreateRenderer
(创建渲染器)返回,调试源码的时候可以看到
const app = ensureRenderer().createApp(...args);
ensureRenderer里面还是会调用baseCreateRenderer
,所以可以得到createApp
js
baseCreateRenderer()
// 省略
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
可以看到baseCreateRenderer
返回的createApp还是由createAppAPI
创建的,这个函数就是从apiCreateApp.ts文件引入的。因此这个函数是核心函数。
另外传入的render函数是渲染器赋予createApp的渲染能力
createAppContext
首先先看到这个函数createAppContext
可以看到它直接返回了一个对象,现在只需要把它理解为一个全局
的上下文对象
即可
createAppAPI
接着看createAppAPI
,可以看到我们需要的createApp
就是这个函数的返回,这里使用了柯里化
,简化参数
它的第一个参数render
,即是渲染器赋予的渲染能力
createApp
最后我们进入这个最核心的函数一探究竟.接收两个参数: rootComponent
rootProps
- 首先进入这个判断,我看了git,这是为了解决一个 bug . extend会产生一个新对象副本,将
rootComponent
更改为这个新的对象副本,以确保在createApp
中使用的是一个不会改变原始对象的对象
。
js
if (!isFunction(rootComponent)) {
rootComponent = extend({}, rootComponent)
}
2.相信到这里你已经知道怎么给根组件传递props,那就是rootProps
.下面代码很简单就是校验rootProps必须是一个对象
js
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
- 最最核心的从现在开始.
js
const context = createAppContext()
提前可以告诉大家,这个context对象
,中有一个app
属性即应用实例,这个实例中有很多方法,例如有注册组件,指令的函数,那么注册肯定要找个地方存下来吧,存在哪呢,其实还是在context
.
并且这个app正是createApp的返回值
,想想我们注册全局组件,指令等是不是在这个返回的app进行操作呢.
- 我们接着看看app实例上的注册方法是怎么写的
mixin
- 组合式api不支持mixin
- 如果 mixin 已经存在于 mixins 数组中,它会在开发模式下发出警告
- 最后,它将mixin 添加到 context.mixins 数组中
js
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
- 如果处于开发模式调用 validateComponentName 函数来验证组件名称是否符合规范
- 如果没有提供组件对象,它会返回已经注册的名为 name 的组件
- 在开发模式下,如果已经存在名为 name 的组件,它会发出警告,指出该组件已在目标应用程序中注册过
- 最后,它将组件对象 component 添加到 context.components 对象中
directive
- 调用 validateDirectiveName 函数来验证指令名称是否符合规范
- 如果没有提供指令对象,它会返回已经注册的名为 name 的指令
- 如果已经存在名为 name 的指令,它会发出警告,指出该指令已在目标应用程序中注册
- 最后,它将指令对象 directive 添加到 context.directives 对象中
provide
- 在开发环境如果已经提供了具有相同键值(key)的属性。并且这个新的值将会覆盖之前的值
- 最后,将其存储在context.provides对象中
context.provides的声明很有意思
provides: Object.create(null)
它创建一个空的对象,并且不会继承任何原型属性,单纯用作映射
plugin
插件有点特殊,它是上面的集合体,所以不需要再全局上下文context中声明
使用方式是app.use(插件名,参数),之后他会调用自身的install方法
- 如果插件已经被注册,它将发出一个警告
- 插件存在且具有 install 方法,那么它会调用该 install 方法,将应用程序对象 app 传递给插件,同时还可以传递其他选项。这允许插件初始化自己,并在应用程序上执行所需的操作。
- 如果插件不存在 install 方法,但是它本身是一个函数,也会被认为是有效的插件,并将该函数调用,同样传递应用程序对象 app 和其他选项
- 否则注册失败
js
const installedPlugins = new Set()
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
},
可以发现这些方法都返回了app实例,因此可以链式调用
mount
isMounted
代表在这个容器上有没有应用程序实例挂载,这里使用到了闭包
,举个例子,flag的值会被改变
js
function a() {
let flag = false;
const app = {
setFlag() {
console.log(flag);
flag = true;
},
setFlag1() {
console.log(flag);
flag = false;
}
};
return app;
}
const app=a()
app.setFlag();
app.setFlag1();
如果isMounted
为false,执行mount
,创建VNode
,利用render
函数进行渲染,isMounted
变为true.
另外还存储了一些其他数据,这里只需要先有个印象。
js
if (!isMounted) {
const vnode = createVNode(rootComponent, rootProps)
vnode.appContext = context
render(vnode, rootContainer, isSVG)
isMounted = true
app._container = rootContainer
}
unmount
如果没有挂载过将会提示 Cannot unmount an app that is not mounted,否则调用render函数卸载
重写mount
虽然是重写,但是并不代表取代
之前在app实例中的mount函数。相反app实例中的mount
是一个标准的可跨平台函数,所以重写的mount函数还是要用到它。
packages\runtime-dom\src\index.ts
可以看到这个文件还有一个createApp
函数,也就是在这里进行重写
js
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
// app._component代表的是根组件
// 在app实例初始化就有声明 app:{_component: rootComponent as ConcreteComponent}
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
// clear content before mounting
container.innerHTML = ''
const proxy = mount(container, false, container instanceof SVGElement)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
return app
})
- 解构出app中的
mount
,接着开始重写 normalizeContainer
标准化容器元素- 如果应用程序的根组件不是一个函数组件且没有
render
方法和template
字段,那么说明它可能是基于 DOM 元素的模板。在这种情况下,代码将会将容器元素的innerHTML
设置为组件的模板,以供后续渲染使用 - 在挂载之前,代码会将容器元素的内容清空,确保容器是空的。这样做是为了避免在挂载应用程序时与容器中的现有内容发生冲突或干扰。
- 最后,调用了解构出的
mount
方法,将处理过的容器元素作为参数传入,执行真正的挂载操作
现在知道了重写mount做了容器以及根组件的跨平台
(其实还有兼容vue2的内容这里知道即可),因此,可以看出重写mount是为了进一步提升跨平台能力
app.config.globalProperties
它是一个用于注册能够被应用内所有组件实例访问到的全局属性的对象
先说说app实例中还有一个访问器属性config
js
// 如果在开发模式下不能替换整个 app.config 对象,而应该通过修改对象里各个配置选项来自定义应用程序的行为
get config() {
return context.config
},
set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
}
},
context初始化的config
js
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: {}
},
现在我们只需要在其中的globalProperties
属性中添加属性,就可以全局共享数据了,类似于Vuex的功能。
创造不易,有帮助的话点个赞吧,要不然没信心写下去了🦾。