使用
在vue3当中是通过createApp将页面给挂在到index.html文件根元素下。下面是一个使用的例子,那么他是怎么运行的呢?又是怎么做的一个链式调用呢?下面就来一一分析
createApp(App) // APP是一个vue组件
.use(ElementPlus, { // 注册El-plus组件
locale: zhCn,
size: 'small',
zIndex: 3000,
})
.use(router) // 注册Vue-Router
.use(ContextMenu) // 一个右键菜单组件
.use(createPinia()) // 注册pinia
.mixin(drawMixin) // 混入
.provide('M', '1') //
.directive('color', (el, binding) => { // 添加自定义指令
el.style.color = binding.value
})
.directive('load', loadingDirective)
.use(i18nPlugin, {
greetings: {
hello: '你好!'
}
})
.mount("#app"); // 挂载
createAppAPI
源码位于packages/runtime-core/src/apiCreateApp.ts
。
- 这里进行了一些简化,可以看到在创建应用程序时,会创建一个
App
对象,然后返回一个createApp
函数,这个函数接收两个参数,一个是根组件,一个是根组件的属性。 - 在得到app实例之后,他里面有很多方法,比如use、mixin、component、directive等。他每一个方法都会返回app,这也就是上面可以使用链式调用的原因
- 最后就是一个
mount
方法,这个方法接收一个参数,一个是挂载的元素。将根组件挂载到挂载元素上。这样就完成了整个vue3的初始化流程。
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction,
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = extend({}, rootComponent)
}
if (rootProps != null && !isObject(rootProps)) {
// 传递给app.mount必须是一个对象
rootProps = null
}
const context = createAppContext()
const installedPlugins = new WeakSet()
let isMounted = false
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
use(plugin: Plugin, ...options: any[]) {
},
mixin(mixin: ComponentOptions) {
},
component(name: string, component?: Component): any {
},
directive(name: string, directive?: Directive) {
},
mount(
rootContainer: HostElement,
isHydrate?: boolean,
namespace?: boolean | ElementNamespace,
): any {
},
unmount() {
},
provide(key, value) {
},
runWithContext(fn) {
},
})
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
return app
}
}
use 使用组件库
先看一下app的use方法的源码。这里就是看使用use方法传递过来的组件当中有没有install方法,如果有的话,就执行install方法,如果没有的话,就执行组件本身。
const app: App = (context.app = {
use(plugin: Plugin, ...options: any[]) {
if (installedPlugins.has(plugin)) {
// 插件已被应用
} else if (plugin && isFunction(plugin.install)) {
// 判断组件当中的install是不是一个函数
installedPlugins.add(plugin);
plugin.install(app, ...options);
} else if (isFunction(plugin)) {
// 看组件是不是函数
installedPlugins.add(plugin);
plugin(app, ...options);
}
return app;
}
});
顺便看一下el-plus的install方法。就是去执行makeInstaller.install方法,遍历el-plus当中的组件通过app.use©进行全局注册。
app是vue3当中创建app对象,然后options是use时传递的配置,会将所有的配置都通过provideGlobalConfig弄成全局的provide。后面我们再看provide的原理
var installer = makeInstaller([...Components, ...Plugins]);
const makeInstaller = (components = []) => {
const install = (app, options) => {
if (app[INSTALLED_KEY])
return;
app[INSTALLED_KEY] = true;
components.forEach((c) => app.use(c));
if (options)
provideGlobalConfig(options, app, true);
};
return {
version,
install
};
};
Provide & Inject
全局Provide
在给app添加provide方法时,其实做的就是将key和value添加到provides对象当中。这些都在根元素上。
const app: App = (context.app = {
provide(key, value) {
context.provides[key as string | symbol] = value;
return app;
}
});
Provide源码分析
先看看provide的源码。源码位于:packages/runtime-core/src/apiInject.ts
在使用provide的时候,会将key和value添加到provides对象当中。这个是到父级别的元素上。
在创建组件实例的时候(createComponentInstance),也会将provides对象添加到父组件实例上。
这也就是在兄弟组件之间去取值时能取到的原因,会从父组件实例的provides对象中取值。
export function provide<T, K = InjectionKey<T> | string | number>(
key: K,
value: K extends InjectionKey<infer V> ? V : T
) {
if (!currentInstance) {
// 组件实例不存在,provide只能在setup函数中调用
} else {
// 数据都会存到组件实例上 provides
let provides = currentInstance.provides;
// 默认情况下,实例继承其父对象的provides对象。但当它需要提供自己的价值观时,它会创建own提供对象使用parent提供对象作为原型。
// 通过这种方式,在inject中,我们可以简单地从direct中查找注射parent并让原型链来完成工作。
const parentProvides =
currentInstance.parent && currentInstance.parent.provides;
if (parentProvides === provides) {
provides = currentInstance.provides = Object.create(parentProvides);
}
provides[key as string] = value;
}
}
// 组件实例
export function createComponentInstance(
vnode: VNode,
parent: ComponentInternalInstance | null,
suspense: SuspenseBoundary | null,
) {
const type = vnode.type as ConcreteComponent
const appContext =
(parent ? parent.appContext : vnode.appContext) || emptyAppContext
const instance: ComponentInternalInstance = {
// 还有别的属性 这里先都不看
provides: parent ? parent.provides : Object.create(appContext.provides),
}
return instance
}
inject源码分析
inject的作用是注入数据,该数据来自于它的祖先组件 provide方法提供的数据
如果在一个组件中使用 inject(key, ‘a’)方法,那么它会先从其父组件的 provides对象本身去查找这个 key,如果找到了就返回对应的数据,如果没有找到,则通过
provides的原型去查找这个 key,此时的 provides的原型指向的就是它的父级 provides对象。实际上,inject查找数据的方法其实就是利用了js中原型链查找方式。
export function inject(
key: InjectionKey<any> | string,
defaultValue?: unknown,
treatDefaultAsFactory = false
) {
// 回退到 currentRenderingInstance。以便可以在中调用
const instance = currentInstance || currentRenderingInstance;
if (instance || currentApp) {
// 如果实例位于根目录,则回退到appContext的provides
const provides = instance
? instance.parent == null
? instance.vnode.appContext && instance.vnode.appContext.provides
: instance.parent.provides
: currentApp!._context.provides;
if (provides && (key as string | symbol) in provides) {
return provides[key as string];
} else if (arguments.length > 1) {
return treatDefaultAsFactory && isFunction(defaultValue)
? defaultValue.call(instance && instance.proxy)
: defaultValue;
}
}
}
mount 挂载
- 先检查是否已经挂载,再创建一个虚拟节点vnode,并且设置context上下文
- 根据namespace设置命名空间
- 根据isHydrate参数的值决定是使用hydrate函数还是render函数来将虚拟节点渲染到根容器中
- 最后返回vnode的组件代理
const app: App = (context.app = {
mount(
rootContainer: HostElement,
isHydrate?: boolean,
namespace?: boolean | ElementNamespace,
): any {
if (!isMounted) {
if (__DEV__ && (rootContainer as any).__vue_app__) {
// 已经挂载了一个app,需要先执行unmount卸载之后再挂载
}
const vnode = createVNode(rootComponent, rootProps)
vnode.appContext = context
if (namespace === true) {
namespace = 'svg'
} else if (namespace === false) {
namespace = undefined
}
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(
cloneVNode(vnode),
rootContainer,
namespace as ElementNamespace,
)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, namespace)
}
isMounted = true
app._container = rootContainer
;(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
}
},
})
unmount卸载
该函数用于卸载一个Vue应用。先检查应用是否已挂载,如果是,则通过render函数将应用的组件渲染为null,从而从DOM中移除应用同时删除应用容器上的Vue应用引用。
const app: App = (context.app = {
unmount() {
if (isMounted) {
render(null, app._container);
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = null;
devtoolsUnmountApp(app);
}
delete app._container.__vue_app__;
}
}
});
render函数
源码位于packages/runtime-core/src/renderer.ts
2357line
如果第一个参数是null,则执行销毁组件的逻辑,否则执行patch函数来创建或者更新组件的逻辑(diff比较更新DOM)
let isFlushing = false;
const render: RootRenderFunction = (vnode, container, namespace) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true);
}
} else {
patch(
container._vnode || null,
vnode,
container,
null,
null,
null,
namespace
);
}
if (!isFlushing) {
isFlushing = true;
flushPreFlushCbs();
flushPostFlushCbs();
isFlushing = false;
}
container._vnode = vnode;
};
directive指令
指令本质就是一个js对象,对象上挂着一些钩子函数。全局注册的指令都挂载到context当中。
const app: App = (context.app = {
directive(name: string, directive?: Directive) {
if (!directive) {
return context.directives[name] as any;
}
context.directives[name] = directive;
return app;
}
});
mixin混入
判断是否支持Options API,以及当前这个mixin还没有被混入
const app: App = (context.app = {
mixin(mixin: ComponentOptions) {
if (__FEATURE_OPTIONS_API__) {
if (!context.mixins.includes(mixin)) {
context.mixins.push(mixin);
}
}
return app;
}
});
component组件
注册组件,如果组件已经存在,则返回组件,否则则注册当前组件返回app
const app: App = (context.app = {
component(name: string, component?: Component): any {
if (!component) {
return context.components[name];
}
context.components[name] = component;
return app;
}
});