前言
提到马拉松,大家都知道马拉松是世界上最长的田径项目(全程42.195公里),是所有体育运动中体力消耗最大,同时也是最磨练一个人的意志的项目。如果说你可以坚持跑完整个马拉松,那还有什么是你不可以坚持下来的呢。
同样,在我们学习研究一些优秀类库的源码时,整个过程也是枯燥乏味,亦如参加一场源码级的马拉松。那今天笔者就带着大家一起跑一场关于Vue.js 3.0渲染系统的源码解析版的马拉松,在整个过程中,笔者也会给大家提供补给站(流程图),方便大家阅读。
思考
在开始今天的文章之前,大家可以想一下:
vue
文件是如何转换成DOM
节点,并渲染到浏览器上的?数据更新时,整个的更新流程又是怎么样的?
🤔️🤔️🤔️🤔️🤔️🤔️🤔️🤔️🤔️🤔️🤔️🤔️🤔️🤔️🤔️🤔️
vuejs
有两个阶段:编译时和运行时。
编译时
我们平常开发时写的.vue
文件是无法直接运行在浏览器中的,所以在webpack
编译阶段,需要通过vue-loader
将.vue
文件编译生成对应的js
代码,vue
组件对应的template
模板会被编译器转化为render函数
。
运行时
接下来,当编译后的代码真正运行在浏览器时,便会执行render函数
并返回VNode
,也就是所谓的虚拟DOM
,最后将VNode
渲染成真实的DOM节点
。
了解完vue
组件渲染的思路后,接下来让我们从Vue.js 3.0(后续简称vue3
)的源码出发,深入了解vue
组件的整个渲染流程是怎么样的?
准备
本文主要是分析
vue3
的渲染系统,为了方便调试,我们直接通过引入vue.js
文件的方式进行源码调试分析。
vue3
源码下载
# 源码地址(推荐ssh方式下载)
https://github.com/vuejs/vue-next
# 或者下载笔者做笔记用的版本
https://github.com/AsyncGuo/vue-next/tree/vue3_notes
生成
vue.global.js
文件
npm run dev
# bundles .../vue-next/packages/vue/src/index.ts → packages/vue/dist/vue.global.js...
# created packages/vue/dist/vue.global.js in 2.8s
启动开发环境
npm run serve
测试代码
<!-- 调试代码目录:/packages/vue/examples/test.html -->
<script src="./../dist/vue.global.js"></script>
<div id="app">
<div>static node</div>
<div>{
{title}}</div>
<button @click="add">click</button>
<Item :msg="title"/>
</div>
<script>
const Item = {
props: ['msg'],
template: `<div>{
{ msg }}</div>`
}
const app = Vue.createApp({
components: {
Item
},
setup() {
return {
title: Vue.ref(0)
}
},
methods: {
add() {
this.title += 1
}
},
})
app.mount('#app')
</script>
创建应用
从上面的测试代码,我们会发现vue3
和vue2
的挂载方式是不同的,vue3
是通过createApp
这个入口函数进行应用的创建。接下来我们来看下createApp
的具体实现:
// 入口文件: /vue-next/packages/runtime-dom/src/index.ts
const createApp = ((...args) => {
console.log('createApp入参:', ...args);
// 创建应用
const app = ensureRenderer().createApp(...args);
const { mount } = app;
// 重写mount
app.mount = (containerOrSelector) => {
// ...
};
return app;
});
ensureRenderer
首先通过ensureRenderer
创建web端的渲染器,我们来看下具体实现:
// 更新属性的方法
const patchProp = () => {
// ...
}
// 操作DOM的方法
const nodeOps = {
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
},
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
...
}
// web端的渲染器所需的参数设置
const rendererOptions = extend({ patchProp }, nodeOps);
let renderer;
// 延迟创建renderer
function ensureRenderer() {
return (renderer || (renderer = createRenderer(rendererOptions)));
}
在这里可以看出,通过延迟创建渲染器,当我们只依赖响应式包的情况下,可以通过tree-shaking移除渲染相关的代码,大大减少包的体积。
createRenderer
通过ensureRenderer
可以看出,真正的入口是这个createRenderer
方法:
// /vue-next/packages/runtime-core/src/renderer.ts
export function createRenderer(options) {
return baseCreateRenderer(options)
}
function baseCreateRenderer(options, createHydrationFns) {
// 通用的DOM操作方法
const {
insert: hostInsert,
remove: hostRemove,
...
} = options
// =======================
// 渲染的核心流程
// 通过闭包缓存内敛函数
// =======================
const patch = () => {} // 核心diff过程
const processElement = () => {} // 处理element
const mountElement = () => {} // 挂载element
const mountChildren = () => {} // 挂载子节点
const processFragment = () => {} // 处理fragment节点
const processComponent = () => {} // 处理组件
const mountComponent = () => {} // 挂载组件
const setupRenderEffect = () => {} // 运行带副作用的render函数
const render = () => {} // 渲染挂载流程
// ...
// =======================
// 🐶2000+行的内敛函数🐶
// =======================
return {
render,
hydrate, // 服务端渲染相关
createApp: createAppAPI(render, hydrate)
}
}
接下来我们先跳过这些内敛函数的实现(后面的渲染流程用到时,我们再具体分析),来看下createAppAPI
的具体实现:
createAppAPI
function createAppAPI(render, hydrate) {
// 真正创建app的入口
return function createApp(rootComponent, rootProps = null) {
// 创建vue应用上下文
const context = createAppContext();
// 已安装的vue插件
const installedPlugins = new Set();
let isMounted = false;
const app = (context.app = {
_uid: uid++,
_component: rootComponent, // 根组件
use(plugin, ...options) {
// ...
return app
},
mixin(mixin) {},
component(name, component) {},
directive(name, directive) {},
mount(rootContainer) {},
unmount() {},
provide(key, value) {}
});
return app;
};
}
可以看出,createAppAPI
返回的createApp
函数才是真正创建应用的入口。在createApp
里会创建vue
应用的上下文,同时初始化app
,并绑定应用上下文到app
实例上,最后返回app
。
这里有个值得注意的点:
app
对象上的use
、mixin
、component
和directive
方法都返回了app
应用实例,开发者可以链式调用。
// 一直use一直爽🐶🐶🐶
createApp(App).use(Router).use(Vuex).component('component',{}).mount("#app")
到此app应用实例已经创建好了~,打印查看下创建的app
应用:总结一下创建app
应用实例的过程:
创建
web端
对应的渲染器(延迟创建,tree-shaking)执