开篇导读:
Vue作为一个相当出名的响应式框架。模板语法、声明式、组件化、轻量级、虚拟DOM的优化、组件级更新等都是它的优点,Vue3相比于Vue2更是重写了大部分源码,采用了更严格的Typescript重写,同时采用Proxy代替了Object.defineProperty做响应式劫持使得功能性得到了延展,对于添加属性,减少对象属性等有了更好的处理,同时项目采用monorepo进行管理,使用一个项目管理多个包,并且每个包的功能划分相当明确,每个模块都可以进行单独的单元测试等,并且还加入了组合式Api,对于处理超大组件有了更好的拆分和利用。
随着前端发展,仅仅是了解如何使用框架是不够的,对于学习的过程,我认为应该是了解使用,知其然,更要知其所以然,所以阅读源码是相当有必要的,并且现在招聘的要求越来越高,一步一步提升自己是完全正确的。
本系列更多是自己的观点和思考,因为本系列也是边读源码边写的。,如果有错误的地方请指出,喜欢的童鞋记得留下一个👍。
1.Vue源码结构解读
compiler-core编译的核心包,与运行平台无关。compiler-dom浏览器平台下的编译器,依赖于compiler-corecompiler-ssr服务端渲染的编译器,同样依赖于compiler-corecompiler-sfc单文件组件的编译器,依赖于compiler-code、compiler-domreactivity可单独运行的响应式系统,runtime-coreVue的运行时核心代码,与平台无关runtime-domVue在浏览器平台运行的核心代码 好的,大概就是这些,本次我们依赖于vite构建一个vue项目,我们主要讲解vue的运行时,也就是runtime-dom与runtime-core这两个包。
2.".vue"文件的编译结果
首先我们创建main.ts文件:
import { createApp } from "vue";//引入的runtime-dom
import App from "./App.vue";
//今天主要分析createApp这个函数。
createApp(App).mount("#app");
我们先来App看看编译后的结果吧!
//App.vue
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
</script>
<template>
<div>
<a href="https://vitejs.dev" target="_blank">
<img src="/vite.svg" class="logo" alt="Vite logo" />
</a>
<a href="https://vuejs.org/" target="_blank">
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
</a>
</div>
<HelloWorld msg="Vite + Vue" />
</template>
<style scoped>
//省略此处代码...
</style>
//编译后的App.vue
import { defineComponent as _defineComponent }
from "/node_modules/.vite/deps/vue.js?v=742c4470";
import HelloWorld from "/src/components/HelloWorld.vue";
//编译出的主要对象,这里主要是对setup语法糖进行的编译
//所以虽然你没有写setup函数 那是因为这一步编译器帮你做了
const _sfc_main = _defineComponent({
__name: "App",
setup(__props, { expose }) {
expose();
//咱们引入的HelloWorld作为返回值返回
const __returned__ = { HelloWorld };
Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
return __returned__;
}
});
import {
//这个就是runtime-core中的createBaseVNode函数
//通过type props children等属性创建VNode
//便于后续使用后面会讲到这个方法
createElementVNode as _createElementVNode,
createVNode as _createVNode,
Fragment as _Fragment,
openBlock as _openBlock,
createElementBlock as _createElementBlock,
//全局有一个currentScopeId,用于给当前的作用域Id设置值
pushScopeId as _pushScopeId,
//消除这个值
popScopeId as _popScopeId
} from "/node_modules/.vite/deps/vue.js?v=742c4470";
//以编译得到的scopeId创建Vnode 创建完成后销毁scopeId
const _withScopeId = (n) => (_pushScopeId("data-v-7a7a37b1"), n = n(), _popScopeId(), n);
//得到template中所有标签组成的Vnode
const _hoisted_1 = _withScopeId(() => _createElementVNode("div", null, [
_createElementVNode("a", {
href: "https://vitejs.dev",
target: "_blank"
}, [
_createElementVNode("img", {
src: "/vite.svg",
class: "logo",
alt: "Vite logo"
})
]),
_createElementVNode("a", {
href: "https://vuejs.org/",
target: "_blank"
}, [
_createElementVNode("img", {
src: "/src/assets/vue.svg",
class: "logo vue",
alt: "Vue logo"
})
])
], -1));
//渲染函数
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_1,
_createVNode($setup["HelloWorld"], { msg: "Vite + Vue" })
], 64);
}
//对于css的代码 直接编译为import语句导入即可
import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";
export const _rerender_only = true;
//这个函数的作用就是将第二个参数的数组中的第一个元素作为key
//第二个元素作为value赋值给第一个参数
import _export_sfc from "/@id/__x00__plugin-vue:export-helper";
//{__name:App,setup,render:_src_render,...}
//这就是最终通过编译得到的对象
export default _export_sfc(
_sfc_main,
[
["render", _sfc_render],
["__scopeId", "data-v-7a7a37b1"],
["__file", "E:/\u5927\u4E8C\u5B66\u4E60\u6587\u4EF6/23.vue-debugger/src/App.vue"]
]
);
//还有一些省略的hmr代码 我们以后在讲解吧!
//defineComponent
function defineComponent(options) {
//如果是一个函数,包装成一个对象
return shared.isFunction(options) ? { setup: options, name: options.name } : options;
}
- 现在我们知道了,我们写的
Vue组件会被编译成一个对象,这个对象中包含setup,name(如果你没写,会自动通过读取文件名读取这个值,例如App.vue=>"App" ),以及render函数。看看浏览器中的显示吧!
知道了
import App from "App.vue"这个App到底是什么,我们就开始分析本文的重点createApp吧!
3.分析createApp发生了什么?
下面代码来自node_modules/@vue/runtime-dom,当然我省略了部分不是重点逻辑的代码
//我们暂且认为,createApp传递的参数就是App对象且只有这一个参数
const createApp =
(...args) => {
//获取runtimeCore的渲染器并调用createApp方法
const app = ensureRenderer().createApp(...args);
//获取mount方法
const { mount } = app;
//重写mount方法这就是咱们后面要讲的挂载流程的起点
app.mount = (containerOrSelector) => {
//获取<div id="root"></div>的DOM对象(这个函数处理了字符串和真实DOM两种情况)
const container = normalizeContainer(containerOrSelector);
if (!container)
return;
const component = app._component;//获取App组件
if (!shared.isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML;
}
container.innerHTML = '';
//执行挂载 arguments(挂载容器,是否是ssr,是否是svg元素)
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;
};
//ensureRenderer
function ensureRenderer() {
return (renderer ||
(renderer = runtimeCore.createRenderer(rendererOptions)));
}
- 我们可以发现首先调用了
ensureRenderer方法,显然这个方法是为了初始化,并且通过runtime-core这个包创建渲染器,同时传入了rendererOptions参数,所以实际上我们最终的app是由runtime-core这个包提供的方法创建的,在分析runtime-core包之前,我们先来看看renderOptions是什么。
const nodeOps = {
//插入节点
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null);
},
//移除节点
remove: child => {
const parent = child.parentNode;
if (parent) {
parent.removeChild(child);
}
},
//创建Html元素
createElement: (tag, isSVG, is, props) => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined);
if (tag === 'select' && props && props.multiple != null) {
el.setAttribute('multiple', props.multiple);
}
return el;
},
//创建文本节点
createText: text => doc.createTextNode(text),
//创建注释节点
createComment: text => doc.createComment(text),
setText: (node, text) => {
node.nodeValue = text;
},
//设置元素的内容
setElementText: (el, text) => {
el.textContent = text;
},
//获取元素的父元素
parentNode: node => node.parentNode,
//获取下一个兄弟元素
nextSibling: node => node.nextSibling,
//使用选择器获取元素
querySelector: selector => doc.querySelector(selector),
setScopeId(el, id) {
el.setAttribute(id, '');
},
//克隆节点
cloneNode(el) {
const cloned = el.cloneNode(true);
if (`_value` in el) {
cloned._value = el._value;
}
return cloned;
},
//插入静态内容
insertStaticContent(content, parent, anchor, isSVG, start, end) {
//省略部分代码...
}
};
//Object.assgin()
const rendererOptions = shared.extend({ patchProp }, nodeOps);
renderOptions实际上就是一些浏览器的Api,传递给runtime-core,便于runtime-core内部执行使用的。 接下来我们来到runtime-core包的createRenderer函数。
function createRenderer(options) {
//创建基础的渲染器
return baseCreateRenderer(options);
}
//创建渲染器
function baseCreateRenderer(options, createHydrationFns) {
const target = shared.getGlobalThis();//获取全局对象
target.__VUE__ = true;
//重options中解构方法
const { insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = shared.NOOP,
cloneNode: hostCloneNode,
insertStaticContent: hostInsertStaticContent } = options;
const patch = ()=>{}
const processText = ()=>{}
const processCommentNode = ()=>{}
const mountStaticNode = ()=>{}
const patchStaticNode =()=>{}
const moveStaticNode = ()=>{}
const removeStaticNode = ()=>{}
const processElement = ()=>{}
const mountElement = ()=>{}
const setScopeId = ()=>{}
const patchElement = ()=>{}
const patchProps = ()=>{}
const processFragment = ()=>{}
const mountComponent = ()=>{}
const updateComponent = ()=>{}
const setupRenderEffect = ()=>{}
const patchChildren = ()=>{}
const patchUnkeyedChildren =()=>{}
const patchKeyedChildren =()=>{}
const move = ()=>{}
const unmount = ()=>{}
const remove = ()=>{}
const removeFragment = ()=>{}
const unmountComponent = ()=>{}
const getNextHostNode = ()=>{}
const render = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true);
}
}
else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG);
}
flushPreFlushCbs();
flushPostFlushCbs();
container._vnode = vnode;
};
return {
render,
//主要逻辑
createApp: createAppAPI(render, hydrate)
};
}
- 这个函数咋一看相当的复杂,我们可以这些函数的函数名读出大概的意思,他们都是用于挂载,比较,卸载的方法,显然,这就是这个渲染器最核心的功能了,但是这一段代码非常长,我们以后在慢慢来解读,现在我们只需要知道
createRenderer返回了一个对象主要有render和createApp方法(还有一个hydrate方法,用于服务端渲染我们不关注这部分逻辑)。createApp的值是通过createAppAPI获得的,所以我们接下来看看这个函数的返回值就可以了!
function createAppAPI(render, hydrate) {
return function createApp(rootComponent, rootProps = null) {
//import App from "App.vue"
//判断当前引入的App是否是一个函数,目前应该是一个对现象
if (!shared.isFunction(rootComponent)) {
//浅克隆
rootComponent = { ...rootComponent };
}
if (rootProps != null && !shared.isObject(rootProps)) {
warn(`root props passed to app.mount() must be an object.`);
rootProps = null;
}
const context = createAppContext();//创建App上下文
const installedPlugins = new Set();//已经安装过的插件
let isMounted = false;//是否挂载过
//main.ts中createApp创造出来的app
const app = (context.app = {
_uid: uid++,//标识符
_component: rootComponent,//App组件
_props: rootProps,//根props
_container: null,//挂载容器
_context: context,//刚才创建的上下文
_instance: null,//根组件的实例
version,//当前vue的版本
//config的getter
get config() {
return context.config;
},
//setter
set config(v) {
{
warn(`app.config cannot be replaced. Modify individual options instead.`);
}
},
//注入插件的方法
use(plugin, ...options) {
//已经安装过这个插件了提示用户
if (installedPlugins.has(plugin)) {
warn(`Plugin has already been applied to target app.`);
}
//所有的插件应当具有install方法,参数为app全局对象以及传入的options
else if (plugin && shared.isFunction(plugin.install)) {
installedPlugins.add(plugin);//添加到集合中防止重复应用插件
plugin.install(app, ...options);//执行插件的install方法
}
//如果插件是一个函数,执行
else if (shared.isFunction(plugin)) {
installedPlugins.add(plugin);
plugin(app, ...options);
}
else {
//warn提示
}
return app;
},
mixin(mixin) {
//省略这部分代码...
},
component(name, component) {
//省略这部分代码...
},
directive(name, directive) {
//省略这部分代码...
},
mount(rootContainer, isHydrate, isSVG) {
if (!isMounted) {
//这里表示已经挂载过了
if (rootContainer.__vue_app__) {
//warn提示
}
//创建Vnode
const vnode = createVNode(rootComponent, rootProps);
vnode.appContext = context;
//通过Vnode渲染节点
render(vnode, rootContainer, isSVG);
isMounted = true;
app._container = rootContainer;
rootContainer.__vue_app__ = app;
app._instance = vnode.component;
}
else {
//warn提示
}
},
unmount() {
//如果已经挂载过了卸载之前的
if(isMounted) {
render(null, app._container);
delete app._container.__vue_app__;
}
else {
warn(`Cannot unmount an app that is not mounted.`);
}
},
provide(key, value) {
//省略代码...
}
});
return app;
};
}
- 到了这里我们终于是看到了
app的真面目,他本质就是一个包含了许多信息和方法的对象,其中就用挂载的方法mount注册插件的方法app.use(),我们也在这里看到了插件的格式必须是一个对象带有install方法或则是一个函数,而其他例如mixin,component等方法,都是将传入的参数缓存到了app.context中,我们就不展开讲了。app中比较重要的属性我已经在注释中给出了,接下来我们看看app.context属性吧!
//大家知道有这么个对象就行了,后面我们在慢慢讲他的作用
function createAppContext() {
return {
app: null,//createApp返回的Vue对象
config: {
isNativeTag: shared.NO,//是否是原生tag
performance: false,
globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: {}
},
mixins: [],//缓存mixin的地方
components: {},//缓存componen
directives: {},//缓存directive
provides: Object.create(null),
optionsCache: new WeakMap(),
propsCache: new WeakMap(),
emitsCache: new WeakMap()
};
}
好啦,到这里我们的app就已经完成了创建了,我们最后看看创建出来的app结构吧!
component: ƒ component(name, component)
config: Object /context.config
directive: ƒ directive(name, directive)
mixin: ƒ mixin(mixin)
mount: (containerOrSelector) => {…}
provide: ƒ provide(key, value)
unmount: ƒ unmount()//卸载的方法
use: ƒ use(plugin, ...options)//使用插件
version: "3.2.39"//vue的版本
//根组件App的编译后结果
_component: {__name: 'App', __scopeId: 'data-v-7a7a37b1', setup: ƒ, render: ƒ, …}
_container: null//挂载的容器 mount阶段才会赋值
_context: {app: {…}, config: {…}...}
_instance: null//组件实例
_props: null
_uid: 0//标识符id
4.本文总结:
- 首先我们简单介绍了
Vue3的更新以及优点 - 然后我们分析了
App.vue编译后的文件,了解到了".vue"文件最终会被编译成一个包含有name,render,setup等信息和方法的对象。 - 在然后我们通过
createApp为入口一步一步分析,最终发现了app的来源,首先调用了runtime-dom中的ensureRenderer方法,然后这个方法会调用runtime-core中的createRenderer并且传入renderOptions,这个renderOptions就是包含一些浏览器操作dom的方法,在createRenderer写了多达二十几个方法一千多行代码,最终返回了一个对象{render,createApp},然后再调用这个对象中的createApp方法创建出了app对象,同时这个对象包含了mount方法,也就是挂载流程的起点。我们下一节再继续进行分析吧!