Version: 3.0.11
1.准备工作-源码结构
1.1目录结构
在正式学习源码之前,首先在Vue官方 github 官网下载源码 ,下载之后解压目录大概是这样的。
- compiler-core
- compiler-dom
- compiler-sfc
- compiler-ssr
- reactivity
- runtime-core 运行时核心 先上一张图来看看runtime-core里面有啥东西。
通过上图我们不难看出Vue的主要的核心api都在运行时核心里面,而且Vue3全部使用TypeScript作为开发语言。
在runtime-core中先说说主要的几个模块vnode、h、components、apiCreateApp、apiLifecycle,因为这个模块是实现Vue3 compositionApi功能的核心
- runtime-dom
- server-render
- sfc-playground
- shared
- size-check
- template-explorer
- vue
1.2 源码编译后的样子
大概就长成这个样子!不难发现每个功能模块有3个js文件和一个ts,首先说js文件,在平时使用dev模式开发的时候调用的是带有esm-bundler版本,项目打包 build的时候调用的是prod版本,剩余的一个版本暂时不清楚什么时候调用,ts文件里面定义了接口类型,并非真正意义上的运行代码,辅助开发的时候数据类型推断。
2 createApp做了什么
2.1 从main.js入手createApp
首先我们看一下Vue3初始化代码,vue3初始化是通过执行createApp方法创建vue实例,而在vue2中使用new Vue()创建。那么下面我们来看一下createApp做了什么。
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
通过浏览器devtool断点执行我们看到createApp方法里面,先执行下面这段代码。
这里主要干了三件事,第一创建renderer,第二创建app实例,第三重写mount函数。 首先通过ensureRenderer方法创建renderer渲染器,然后再通过createApp创建app实例,最后重写app.mount方法,返回proxy代理组件实例
const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args);
if ((process.env.NODE_ENV !== 'production')) {
injectNativeTagCheck(app);
injectCustomElementCheck(app);
}
const { mount } = app;
app.mount = (containerOrSelector) => {
const container = normalizeContainer(containerOrSelector);
if (!container)
return;
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;
});
2.2 创建renderer
那么看到第一个执行的方法是ensureRender(),通过源码注释了解到惰性创建渲染器renderer,这样做的原因是用户通过Vue只引入了reactivity时可以保证tree-shaking可用 如果renderer已经创建那么直接返回已经创建好的renderer,否则通过createRenderer创建新的渲染器renderer。
const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps);
// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
let renderer;
let enabledHydration = false;
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions));
}
createRenderer方法接收一个rendererOption参数,其中nodeOps里面封装的方法就是dom节点操作,当template模板数据变化的时候调用。
patchProp更新dom属性,包括style内敛样式,class类样式,以及patchEvent事件绑定
再看createRenderer方法,内执行baseCreateRenderer方法并将option参数传入。
function createRenderer(options) {
console.log('%c调用 baseCreateRenderer >>>','color:red')
return baseCreateRenderer(options);
}
接着再看baseCreateRenderer方法,内部函数众多代码长先跳过,调用baseCreateRenderer方法是返回一个对象,包含三个元素:render,hydrate和createApp。
var count = 0
function baseCreateRenderer(options, createHydrationFns) {
// 代码过长省略N行 ...
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
};
}
2.3 创建app实例
在返回参数里面看到调用了createAppAPI方法,也就是说在我们main.js里面执行createApp实际上调用的就是createAppAPI,返回一个app对象,我们再通过createAppAPI看app对象具体有哪些参数
function createAppContext() {
return {
app: null,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
isCustomElement: NO,
errorHandler: undefined,
warnHandler: undefined
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null)
};
}
let uid = 0;
function createAppAPI(render, hydrate) {
return function createApp(rootComponent, rootProps = null) {
if (rootProps != null && !isObject(rootProps)) {
(process.env.NODE_ENV !== 'production') && 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 = (context.app = {
_uid: uid++,
_component: rootComponent,
_props: rootProps,
_container: null,
_context: context,
version,
get config() {
return context.config;
},
set config(v) {
if ((process.env.NODE_ENV !== 'production')) {
warn(`app.config cannot be replaced. Modify individual options instead.`);
}
},
use(plugin, ...options) {
if (installedPlugins.has(plugin)) {
(process.env.NODE_ENV !== 'production') && 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 ((process.env.NODE_ENV !== 'production')) {
warn(`A plugin must either be a function or an object with an "install" ` +
`function.`);
}
return app;
},
mixin(mixin) {
if (__VUE_OPTIONS_API__) {
if (!context.mixins.includes(mixin)) {
context.mixins.push(mixin);
// global mixin with props/emits de-optimizes props/emits
// normalization caching.
if (mixin.props || mixin.emits) {
context.deopt = true;
}
}
else if ((process.env.NODE_ENV !== 'production')) {
warn('Mixin has already been applied to target app' +
(mixin.name ? `: ${mixin.name}` : ''));
}
}
else if ((process.env.NODE_ENV !== 'production')) {
warn('Mixins are only available in builds supporting Options API');
}
return app;
},
component(name, component) {
if ((process.env.NODE_ENV !== 'production')) {
validateComponentName(name, context.config);
}
if (!component) {
return context.components[name];
}
if ((process.env.NODE_ENV !== 'production') && context.components[name]) {
warn(`Component "${name}" has already been registered in target app.`);
}
context.components[name] = component;
return app;
},
directive(name, directive) {
if ((process.env.NODE_ENV !== 'production')) {
validateDirectiveName(name);
}
if (!directive) {
return context.directives[name];
}
if ((process.env.NODE_ENV !== 'production') && context.directives[name]) {
warn(`Directive "${name}" has already been registered in target app.`);
}
context.directives[name] = directive;
return app;
},
mount(rootContainer, isHydrate, isSVG) {
if (!isMounted) {
const vnode = createVNode(rootComponent, 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 ((process.env.NODE_ENV !== 'production')) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG);
};
}
if (isHydrate && hydrate) {
hydrate(vnode, rootContainer);
}
else {
render(vnode, rootContainer, isSVG);
}
isMounted = true;
app._container = rootContainer;
rootContainer.__vue_app__ = app;
if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
devtoolsInitApp(app, version);
}
return vnode.component.proxy;
}
else if ((process.env.NODE_ENV !== 'production')) {
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 ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
devtoolsUnmountApp(app);
}
delete app._container.__vue_app__;
}
else if ((process.env.NODE_ENV !== 'production')) {
warn(`Cannot unmount an app that is not mounted.`);
}
},
provide(key, value) {
if ((process.env.NODE_ENV !== 'production') && key in context.provides) {
warn(`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`);
}
// TypeScript doesn't allow symbols as index type
// https://github.com/Microsoft/TypeScript/issues/24587
context.provides[key] = value;
return app;
}
});
return app;
};
}
createAppAPI的核心就是调用createAppContext 返回一个包含app、config、mixins、components、directives和provides基础参数对象, 然后再创建context.app对象,也就是vue的实例对象,包含内部参数和版本号,暴露出use、component、mount、mixin、directive、unmount、provide程序api给我们使用。
本文作者:自如大前端研发中心-贾文莉