下载源码
直接运行 npm run dev 可能产生报错
去 dev.js 把 git 提交相关部分注释
再次运行 npm run dev
可以看到 Vue 的源码打包到了这个位置
创建一个测试页面,注意:这里引入的是刚才源码打包好的 vue.global.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 30px;
}
</style>
</head>
<body>
<div id="app" class="container">
<div>我是div</div>
<span>我是span</span>
<p>我是p</p>
<h1>{{ count }}</h1>
<button @click="decrement">减一</button>
<button @click="increment">加一</button>
</div>
<script src="../../vue/dist/vue.global.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count += 1;
},
decrement() {
this.count -= 1;
}
}
})
app.mount("#app");
</script>
</body>
</html>
其中 createApp 函数可以在这里找到
可以看到,这里创建了 app 对象,调用了 ensureRenderer() 函数的 createApp 函数
其中给 app 对象添加了 mount 方法,对,就是 app.mount("#app") 这个 mount,将 Vue 组件挂载到 id 为 app 的HTML元素上
ensureRenderer 又调用了 createRenderer
之后调用了 baseCreateRenderer
返回了这个对象
其中 createApp 是调用了 createAppAPI 方法
createAppAPI 返回了 createApp 函数 ,这里面定义了 app 对象
这里面有各种方法,包括 mixin、mount 等方法,最后返回 app 对象,所以 app 对象可以调用 mount 方法
mount 方法
app.mount 方法最终调用的是 createApp 中的 mount 方法
而 createApp 中的 mount 方法的根容器参数是一个元素类型,而最外层调用的 app.mount("#app")类型并不一样,这就是参数归一化,其外层调用了normalizeContainer
通过判断是不是 string,如果是就调用 querySelector 进行元素的获取,如果不是可直接返回 container
接下来是模板归一化
对 component 进行判断,如果同时满足不是函数、不是 render、不是模板,则把容器i的 innerHTML 给组件的模板,之后再清空容器的 innerHTML。
为什么要清空容器的 innerHTML?
因为如果不清空,会变成这个样子
为什么会是这样?
是因为,component 是有 template 属性,而参数模板归一化,已经把静态的 HTML 交给模板了
Vue 组件里的处理好的模板会插入到容器中,如果静态内容不清空就会是之前的效果,静态内容与 Vue 组件管理的动态内容共存。
而最后 mount 方法把元素渲染出来,就是通过 render 方法
render 方法需要三个参数,虚拟节点,根容器和 svg,最后调用 patch 函数
简单概括 patch 函数的功能:
1、传递容器和虚拟节点两个参数,把虚拟节点挂载到容器中
2、传递旧的和新的虚拟节点两个参数,会把新的虚拟节点替换掉旧的虚拟节点
Vue 中的 patch 方法如下,进行类型的判断,Switch 处理各种类型
处理元素 patch 函数的 switch 会走 processElement
processElement 又会调用 mountElement
mountElement 会接收虚拟节点
之后会调用 hostCreateElement,之后判断有没有子节点,如果有调用 mountChildren ,如果没有设置元素文本
之后调用 hostInsert 把元素插入到容器中。
其中 mountChildren 遍历子节点,调用 patch 函数,就又会执行之前这些逻辑
更新子树
effect 函数:会自动收集依赖,依赖改变,自动调用内部的回调函数。
当 data 改变,effect 就会重新执行这个 componentEffect 函数
之后进行判断当前是否已经挂载,如果没有挂载,创建子树,渲染根组件
之后继续调用 patch 函数,把 subtree 传入
多个根节点
在调用挂载组件方法中的 setupComponent 设置组件方法后会,调用 setupRenderEffect 方法
其结构如下
可以打印有根节点和无根节点的子树结构查看
<div id="app" class="container">
<div>
<div>我是div</div>
<span>我是span</span>
<p>我是p</p>
<h1>{{ count }}</h1>
<button @click="decrement">减一</button>
<button @click="increment">加一</button>
</div>
</div>
<div id="app" class="container">
<div>我是div</div>
<span>我是span</span>
<p>我是p</p>
<h1>{{ count }}</h1>
<button @click="decrement">减一</button>
<button @click="increment">加一</button>
</div>
对比之下可以看到
没有根节点时,是外面生成了根节点,<Fragment> 标签进行包裹。
为什么是 Fragment ?
因为普通元素会渲染为实际的 DOM 节点,而 Fragment 不会渲染任何实际节点,通过样式就可以对比出来,手写的根节点没有设置样式就是第一张图的样子。
虚拟节点是如何创建的
挂载组件调用这个 mountComponent 方法,内部创建了实例,然后调用 setupComponent(instance) 进行初始化
之后调用 setupRenderEffect 方法
setupRenderEffect 方法内部的第一次挂载会进行创建之前提到的 subTree
而 subTree 的创建是通过 renderComponentRoot 方法
内部创建了 result, result 调用了 render.call,最后返回了 result 给 instance.subTree
为了找到 render 方法的来源需要往回查看 instance 的来源,是 createComponentInstance
可以看到这里有 render ,但是为 null,所以需要继续寻找
之后是初始化组件
找到 setupStatefulComponent,顺着方法继续找
找到了初始化完成方法,里面找到了 render 方法
compile 把 template 通过 AST 抽象语法树 转为 JavaScript 代码,创建虚拟节点