在应用 Vue3
时,咱们须要应用 createApp
来创立一个利用实例,而后应用 mount
办法将利用挂载到某个 DOM
节点上。
那么在调用 createApp
时,Vue
再背地做了些什么事件呢?明天就来扒一扒 Vue3
的源码,看看调用 createApp
产生了些什么。
大家好,这里是田八的【源码 & 库】系列,
Vue3
的源码浏览打算,Vue3
的源码浏览打算不出意外每周一更,欢送大家关注。首发在掘金,如果想一起交换的话,能够点击这里一起独特交换成长
系列章节:
- 【源码 & 库】跟着 Vue3 学习前端模块化
寻找入口
在上一章中,咱们咱们曾经将 Vue3
的源码下载下来了,并且曾经晓得如何编译源码了,先看一下 Vue3
的源码目录:
packages
目录下的包就是 Vue3
的所有源码了,编译之后会在每个工程包上面生成一个 dist
目录,外面就是编译后的文件。
这里我框出了 vue
包,这个大家都相熟,关上 vue
包下的 package.json
文件,能够看到 unpkg
字段指向了 dist/vue.global.js
文件,这个文件就是 Vue3
的全局版本,咱们能够间接在浏览器中引入这个文件来应用Vue3
。
代码逻辑基本上都是雷同的,用打包后的文件来剖析源码,能够更加直观的看到源码的逻辑,因为 Vue
在设计的时候会思考其余平台,如果间接通过源码来查看会有额定的心智累赘。
具体如何应用每个打包后的文件,能够查看 vue
包下的 README.md
文件,如果只是想剖析源码,且不想那么麻烦,能够间接应用 dist/vue.global.js
文件。
如果想理解 Vue3
的目录构造和模块划分能够应用 vue.esm-bundler.js
文件,这个文件是 Vue3
的ESM
版本,会通过 import
来引入其余模块,这样就能够间接看到 Vue3
的模块划分。
本系列就会通过 vue.esm-bundler.js
文件来剖析 Vue3
的源码,并且会通过边剖析边入手的形式来学习 Vue3
的源码。
应用
咱们先来看一下 Vue3
的应用形式:
import {createApp} from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
在 Vue3
中,咱们须要应用 createApp
来创立一个利用实例,而后应用 mount
办法将利用挂载到某个 DOM
节点上。
createApp
是从 vue
包中导出的一个办法,它接管一个组件作为参数,而后返回一个利用实例。
入口 createApp
从 vue
的package.json
能够看到,module
字段指向了 dist/vue.esm-bundler.js
文件,这个文件是 Vue3
的ESM
版本,咱们能够间接应用 import
来引入Vue3
。
而 createApp
办法并不在这个包中,而是在 runtime-dom
包中,这个文件是间接全副导出 runtime-dom
包中的内容:
export * from '@vue/runtime-dom';
不必狐疑 @vue/runtime-dom
指向的就是 runtime-dom
包,应用 esm
版本就间接找 xxx.esm-bundler.js
文件,应用 cjs
版本就间接找 xxx.cjs.js
文件,前面不会再提到这个问题。
关上 runtime-dom.esm-bundler.js
文件,能够看到 createApp
办法:
import { } from '@vue/runtime-core';
export * from '@vue/runtime-core';
import { } from '@vue/shared';
// ... 省略 n 多代码
function createApp(...args) {// ...}
export {createApp};
能够看到 runtime-dom
包中还援用了 runtime-core
包和 shared
包,当初找到入口文件了,在剖析间接能够先搭建一个简略的代码剖析和测试的环境,这样不便本人验证并且能够间接看到代码的执行后果。
demo
环境能够间接在本地搭建,也能够应用 codesandbox
、stackblitz
等在线环境,这里应用 codesandbox
,后续demo
的代码都会放在 codesandbox
上,文末会有链接。
当然大家也能够间接在本地搭建一个 demo
环境,这里就不再赘述了。
源码剖析
下面的环境都筹备好了之后就能够间接开始剖析 Vue3
的源码了,咱们先来看一下 createApp
办法的实现;
createApp
const createApp = (...args) => {const app = ensureRenderer().createApp(...args);
const {mount} = app;
app.mount = (containerOrSelector) => {// ...};
return app;
}
createApp
办法接管一个组件作为参数,而后调用 ensureRenderer
办法;
这个办法的作用是确保渲染器存在,如果不存在就创立一个渲染器,而后调用渲染器的 createApp
办法,这个办法的作用是创立一个利用实例,而后将这个利用实例返回,相当于一个单例模式。
let renderer;
const ensureRenderer = () => renderer || (renderer = createRenderer(rendererOptions));
这里的 rendererOptions
是一些渲染器的配置,次要的作用是用来操作 DOM
的,这里不做过多的介绍,前面会有专门的文章来介绍。
当初先简略的来认识一下rendererOptions
,这个外面会有两个办法前面会用到:
const rendererOptions = {insert: (child, parent, anchor) => {parent.insertBefore(child, anchor || null);
},
createText: text => document.createTextNode(text),
}
当初咱们先简略的入手实现一下 createApp
办法,新建一个 runtime-dom.js
文件,而后内容如下:
import {createRenderer} from "./runtime-core";
const createApp = (...args) => {
const rendererOptions = {insert: (child, parent, anchor) => {parent.insertBefore(child, anchor || null);
},
createText: (text) => document.createTextNode(text)
};
const app = createRenderer(rendererOptions).createApp(...args);
const {mount} = app;
app.mount = (containerOrSelector) => {//... 前面剖析再补上};
return app;
};
export {createApp};
当初能够看到咱们在实现 createApp
办法的时候,间接调用了 createRenderer
办法,这个办法是创立渲染器的办法,这个办法的实现在 runtime-core
包中;
所以咱们须要补上 runtime-core
包中的 createRenderer
办法的实现;
createRenderer
createRenderer
源码实现如下:
function createRenderer(options) {return baseCreateRenderer(options);
}
// implementation
function baseCreateRenderer(options, createHydrationFns) {
// 省略 n 多代码,都是函数定义,并会立刻执行,临时对后果不会有影响
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
};
}
createRenderer
外部返回 baseCreateRenderer
办法的执行后果,这个办法的作用会返回 render
、hydrate
、createApp
三个办法;
而咱们最初须要调用的 createApp
办法就是在这三个办法中的其中一个,而 createApp
办法的是通过 createAppAPI
办法创立的,同时剩下的两个办法 render
和hydrate
也是在 createAppAPI
办法中被调用的,所以咱们还须要看一下 createAppAPI
办法的实现;
createAppAPI
createAppAPI
办法的实现如下:
function createAppContext() {
return {
app: null,
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$1 = 0;
function createAppAPI(render, hydrate) {
// 返回一个函数,这里次要是通过闭包来缓存下面传入的参数
return function createApp(rootComponent, rootProps = null) {
// rootComponent 就是咱们传入的根组件,这里会做一些校验
// 如果传递的不是一个函数,那么就做一个浅拷贝
if (!isFunction(rootComponent)) {rootComponent = Object.assign({}, rootComponent);
}
// rootProps 就是咱们传入的根组件的 props,这个参数必须是一个对象
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();
// 通过 use 创立的插件都存在这里
const installedPlugins = new Set();
// 是否曾经挂载
let isMounted = false;
// 创立 app 对象
const app = (context.app = {
_uid: uid$1++,
_component: rootComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
get config() {// ...},
set config(v) {// ...},
use(plugin, ...options) {// ...},
mixin(mixin) {// ...},
component(name, component) {// ...},
directive(name, directive) {// ...},
mount(rootContainer, isHydrate, isSVG) {// ...},
unmount() {// ...},
provide(key, value) {// ...}
});
// 返回 app 对象
return app;
};
}
看到这里,咱们就能够晓得,createApp
办法的实现其实就是在 createAppAPI
办法中返回一个函数,这个函数就是 createApp
办法;
这个办法并没有如许非凡,就是返回了一堆对象,这些对象就是咱们在应用 createApp
办法时,能够调用的办法;
这里能够看到咱们罕用的 use
、mixin
、component
、directive
、mount
、unmount
、provide
等办法都是在 app
对象上的,也是通过这个函数制作并返回的;
当初咱们持续欠缺咱们的学习 demo
代码,当初新建一个 runtime-core.js
文件夹,而后把下面的代码复制进去;
然而咱们不能全都都间接照搬,下面的对象这么多的属性咱们只须要保留 mount
,因为还须要挂载能力看到成果,demo
代码如下:
function createRenderer(options) {
// 先省略 render 和 hydrate 办法的实现,前面会讲到
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
};
}
function createAppAPI(render, hydrate) {return function createApp(rootComponent, rootProps = null) {
// 省略参数校验
rootComponent = Object.assign({}, rootComponent);
// 省略上下文的创立
const context = {app: null}
// 疏忽其余函数的实现,只保留 mount 函数和公有变量
let isMounted = false;
const app = (context.app = {
_uid: uid$1++,
_component: rootComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
mount(rootContainer, isHydrate, isSVG) {// ...},
});
return app;
};
}
这样咱们就实现了 createApp
函数的简化版实现,接下来咱们就能够开始挂载了;
mount 挂载
下面咱们曾经学习到了 createApp
函数的实现,当初还须要通过 mount
办法来挂载咱们的根组件,能力验证咱们的 demo
代码是否正确;
咱们在调用 createApp
办法时,会返回一个 app
对象,这个对象上有一个 mount
办法,咱们须要通过这个办法来挂载咱们的根组件;
在这之前,咱们看到了 createApp
的实现中重写了 mount
办法,如下:
const createApp = (...args) => {
// ... 省略其余代码
// 备份 mount 办法
const {mount} = app;
// 重写 mount 办法
app.mount = (containerOrSelector) => {
// 获取挂载的容器
const container = normalizeContainer(containerOrSelector);
if (!container)
return;
// _component 指向的是 createApp 传入的根组件
const component = app._component;
// 验证根组件是否是一个对象,并且有 render 和 template 两个属性之一
if (!isFunction(component) && !component.render && !component.template) {
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's
// rendered by the server, the template should not contain any user data.
// 确保模板是可信的,因为模板可能会有 JS 表达式,具体能够翻译下面的正文
component.template = container.innerHTML;
}
// clear content before mounting
// 挂载前清空容器
container.innerHTML = '';
// 正式挂载
const proxy = mount(container, false, container instanceof SVGElement);
// 挂载实现
if (container instanceof Element) {
// 革除容器的 v-cloak 属性,这也就是咱们常常看到的 v-cloak 的作用
container.removeAttribute('v-cloak');
// 设置容器的 data-v-app 属性
container.setAttribute('data-v-app', '');
}
// 返回根组件的实例
return proxy;
};
return app;
}
下面重写的 mount
办法中,其实最次要的做的是三件事:
- 获取挂载的容器
- 调用本来的
mount
办法挂载根组件 - 为容器设置
vue
的专属属性
当初到咱们入手实现一个简易版的 mount
办法了;
// 备份 mount 办法
const {mount} = app;
// 重写 mount 办法
app.mount = (containerOrSelector) => {
// 获取挂载的容器
const container = document.querySelector(containerOrSelector);
if (!container)
return;
const component = app._component;
container.innerHTML = '';
// 正式挂载
return mount(container, false, container instanceof SVGElement);
};
这里的挂载其实还是应用的是 createApp
函数中的 mount
办法,咱们能够看到 mount
办法的实现如下:
function mount(rootContainer, isHydrate, isSVG) {
// 判断是否曾经挂载
if (!isMounted) {
// 这里的 #5571 是一个 issue 的 id,能够在 github 上搜寻,这是一个在雷同容器上反复挂载的问题,这里只做提醒,不做解决
// #5571
if ((process.env.NODE_ENV !== 'production') && rootContainer.__vue_app__) {
warn(`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling `app.unmount()` first.`);
}
// 通过在 createApp 中传递的参数来创立虚构节点
const vnode = createVNode(rootComponent, rootProps);
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
// 下面有正文,在根节点上挂载 app 上下文,这个上下文会在挂载时设置到根实例上
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);
}
// 其余状况下,间接通过 render 函数挂载
// render 函数在 createRenderer 中定义,传递到 createAppAPI 中,通过闭包缓存下来的
else {render(vnode, rootContainer, isSVG);
}
// 挂载实现后,设置 isMounted 为 true
isMounted = true;
// 设置 app 实例的 _container 属性,指向挂载的容器
app._container = rootContainer;
// 挂载的容器上挂载 app 实例,也就是说咱们能够通过容器找到 app 实例
rootContainer.__vue_app__ = app;
// 非生产环境默认开启 devtools,也能够通过全局配置来开启或敞开
// __VUE_PROD_DEVTOOLS__ 能够通过本人应用的构建工具来配置,这里只做提醒
if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
app._instance = vnode.component;
devtoolsInitApp(app, version);
}
// 返回 app 实例,这里不做具体的剖析
return getExposeProxy(vnode.component) || 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)``);
}
}
通过下面的一通剖析,其实挂载次要就是用的两个函数将内容渲染到容器中;
- createVNode 创立虚构节点
- render 渲染虚构节点
咱们这里就实现一个简易版的 mount
函数,来模仿挂载过程,代码如下:
function mount(rootContainer, isHydrate) {
// createApp 中传递的参数在咱们这里必定是一个对象,所以这里不做创立虚构节点的操作,而是模仿一个虚构节点
const vnode = {
type: rootComponent,
children: [],
component: null,
}
// 通过 render 函数渲染虚构节点
render(vnode, rootContainer);
// 返回 app 实例
return vnode.component
}
虚构节点
虚构节点在 Vue
中曾经是十分常见的概念了,其实就是一个 js
对象,蕴含了 dom
的一些属性,比方 tag
、props
、children
等等;
在 Vue3
中保护了一套本人的虚构节点,大略信息如下:
export interface VNode {
__v_isVNode: true;
__v_skip: true;
type: VNodeTypes;
props: VNodeProps | null;
key: Key | null;
ref: Ref<null> | null;
scopeId: string | null;
children: VNodeNormalizedChildren;
component: ComponentInternalInstance | null;
suspense: SuspenseBoundary | null;
dirs: DirectiveBinding[] | null;
transition: TransitionHooks<null> | null;
el: RendererElement | null;
anchor: RendererNode | null;
target: RendererNode | null;
targetAnchor: RendererNode | null;
staticCount: number;
shapeFlag: ShapeFlags;
patchFlag: number;
dynamicProps: string[] | null;
dynamicChildren: VNode[] | null;
appContext: AppContext | null;
}
残缺的 type
信息太多,这里就只贴 VNode
的相干定义,而且这些在 Vue
的实现中也没有那么简略,这一章不做具体的剖析,只是做一个简略的概念介绍;
render
render
函数是在讲 createRenderer
的时候呈现的,是在 baseCreateRenderer
中定义的,具体源码如下:
function baseCreateRenderer(options, createHydrationFns) {
// ...
// 创立 render 函数
const render = (vnode, container, isSVG) => {
// 如果 vnode 不存在,并且容器是产生过渲染,那么将执行卸载操作
if (vnode == null) {
// container._vnode 指向的是上一次渲染的 vnode,在这个函数的最初一行
if (container._vnode) {unmount(container._vnode, null, null, true);
}
}
// 执行 patch 操作,这里不做具体的剖析,牵扯太大,前面会独自讲
else {patch(container._vnode || null, vnode, container, null, null, null, isSVG);
}
// 刷新工作队列,通常指代的是各种回调函数,比方生命周期函数、watcher、nextTick 等等
// 这里不做具体的剖析,前面会独自讲
flushPreFlushCbs();
flushPostFlushCbs();
// 记录 vnode,当初的 vnode 曾经是上一次渲染的 vnode 了
container._vnode = vnode;
};
// ...
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
};
}
render
函数的次要作用就是将虚构节点渲染到容器中,unmount
函数用来卸载容器中的内容,patch
函数用来更新容器中的内容;
当初来实现一个简易版的 render
函数:
const render = (vnode, container) => {patch(container._vnode || null, vnode, container);
// 记录 vnode,当初的 vnode 曾经是上一次渲染的 vnode 了
container._vnode = vnode;
}
unmount
函数不是咱们这次次要学习的内容,所以这里不做具体的剖析;
patch
函数是 Vue
中最外围的函数,这次也不做具体的剖析,前面会独自讲,然而要验证咱们这次的学习成绩,所以咱们须要一个只有挂载性能的 patch
函数,这里咱们就本人实现一个简略的 patch
函数;
patch
patch
函数的次要作用就是将虚构节点渲染到容器中,patch
函数也是在 baseCreateRenderer
中定义的;
patch
函数这次就不看了,因为外部的实现会牵扯到十分多的内容,这次只是它的呈现只是走个过场,前面会独自讲;
咱们这次的目标只是验证咱们这次源码学习的成成绩,所以咱们只须要一个只有挂载性能的 patch
函数,这里咱们就本人实现一个简略的 patch
函数;
// options 是在创立渲染器的时候传入的,还记得在 createApp 的实现中,咱们传入了一个有 insert 和 createText 办法的对象吗?不记得能够往上翻翻
const {insert: hostInsert, createText: hostCreateText} = options;
// Note: functions inside this closure should use `const xxx = () => {}`
// style in order to prevent being inlined by minifiers.
/**
* 简易版的实现,只是删除了一些不必要的逻辑
* @param n1 上一次渲染的 vnode
* @param n2 以后须要渲染的 vnode
* @param container 容器
* @param anchor 锚点, 用来标记插入的地位
*/
const patch = (n1, n2, container, anchor = null) => {
// 上一次渲染的 vnode 和以后须要渲染的 vnode 是同一个 vnode,那么就不须要做任何操作
if (n1 === n2) {return;}
// 获取以后须要渲染的 vnode 的类型
const {type} = n2;
switch (type) {
// 如果是文本节点,那么就间接创立文本节点,而后插入到容器中
case Text:
processText(n1, n2, container, anchor);
break;
// 还会有其余的类型,这里不做具体的剖析,前面会独自讲
// 其余的状况也会有很多种状况,这里对立当做是组件解决
default:
processComponent(n1, n2, container, anchor);
}
};
patch
函数的次要作用就是将虚构节点正确的渲染到容器中,这里咱们只实现了文本节点和组件的渲染,其余的类型的节点,前面会独自讲;
而咱们在应用 createApp
的时候,通常会传入一个根组件,这个根组件就会走到 processComponent
函数中;
所以咱们这里还须要实现了一个简略的 processComponent
函数;
const processComponent = (n1, n2, container, anchor) => {if (n1 == null) {mountComponent(n2, container, anchor);
}
// else {// updateComponent(n1, n2, optimized);
// }
};
processComponent
函数也是定义在 baseCreateRenderer
中的,这里还是和 patch
函数一样,只是实现了一个简略的性能,前面会独自讲;
processComponent
函数做了两件事,一个是挂载组件,一个是更新组件,这里咱们只实现了挂载组件的性能;
挂载组件是通过 mountComponent
函数实现的,这个函数也是定义在 baseCreateRenderer
中的,然而咱们这次就不再持续深刻外部调用了,间接实现一个繁难的:
const mountComponent = (initialVNode, container, anchor) => {
// 通过调用组件的 render 办法,获取组件的 vnode
const subTree = initialVNode.type.render.call(null);
// 将组件的 vnode 渲染到容器中,间接调用 patch 函数
patch(null, subTree, container, anchor);
};
这样咱们就实现了一个简易版的挂载组件的性能,这里咱们只是简略的调用了组件的 render
办法,render
办法会返回一个 vnode
,而后调用patch
函数将 vnode
渲染到容器中;
当初回头看看 patch
函数,还差一个 processText
函数没有实现,这个函数也是定义在 baseCreateRenderer
中的,这个比较简单,上面的代码就是实现的 processText
函数:
const processText = (n1, n2, container, anchor) => {if (n1 == null) {hostInsert((n2.el = hostCreateText(n2.children)), container, anchor);
}
// else {// const el = (n2.el = n1.el);
// if (n2.children !== n1.children) {// hostSetText(el, n2.children);
// }
// }
};
我这里屏蔽掉了更新的操作,这里只管挂载,这里的 hostInsert
和hostCreateText
函数就是在咱们实现繁难 patch
函数的时候,在 patch
函数实现的下面,通过解构赋值获取的,没印象能够回去看看;
验证
当初咱们曾经实现了一个简易版的 createApp
函数,并且咱们能够通过 createApp
函数创立一个利用,而后通过 mount
办法将利用挂载到容器中;
咱们能够通过上面的代码来验证一下:
import {createApp} from "./runtime-dom";
const app = createApp({render() {
return {
type: "Text",
children: "hello world"
};
}
});
app.mount("#app");
源码在 codesandbox
下面,能够间接查看:https://codesandbox.io/s/gallant-sun-khjot0?file=/src/main.js
总结
咱们通过浏览 Vue3
的源码,理解了 Vue3
的createApp
函数的实现,createApp
函数是 Vue3
的入口函数,通过 createApp
函数咱们能够创立一个利用;
createApp
的实现是借助了 createRenderer
函数,createRenderer
的实现就是包装了baseCreateRenderer
;
baseCreateRenderer
函数是一个工厂函数,通过 baseCreateRenderer
函数咱们能够创立一个渲染器;
baseCreateRenderer
函数接管一个 options
对象,这个 options
对象中蕴含了一些渲染器的配置,比方 insert
、createText
等;
这些配置是在 runtime-dom
中实现的,runtime-dom
中的 createApp
函数会将这些配置透传递给 baseCreateRenderer
函数,而后 baseCreateRenderer
函数会返回一个渲染器,这个渲染器中有一个函数就是createApp
;
createApp
函数接管一个组件,而后返回一个利用,这个利用中有一个 mount
办法,这个 mount
办法就是用来将利用挂载到容器中的;
在 createApp
中重写了 mount
办法,外部的实现是通过调用渲染器的 mount
办法;
这个 mount
办法是在 baseCreateRenderer
函数中实现的,baseCreateRenderer
函数中的 mount
办法会调用 patch
函数;
patch
函数外部会做很多的事件,尽管咱们这里只实现了挂载的逻辑,然而也是粗窥了 patch
函数的外部一些逻辑;
最初咱们实现了一个精简版的 createApp
函数,通过这个函数咱们能够创立一个利用,而后通过 mount
办法将利用挂载到容器中,这个过程中咱们也理解了 Vue3
的一些实现细节;
这次就到这里,下次咱们会持续深刻理解 Vue3
的源码,心愿大家可能多多反对,谢谢大家!