看源码的思路:
先看整体架构,搞清楚每个目录都是干嘛的,不要一上来就盯着代码一行一行看。
可以结合测试用例看,测试用例能够让我们快速了解到某个方法的作用。比如如果有某行代码看不懂,可以把它先注释掉,然后跑一下测试用例,看看哪个用例报错了,就能知道这行代码是干嘛用的了。
在 package.json 中的构建命令里加上
-s
,重新打包,然后打开 google 浏览器的Enable JavaScript source maps
,就可以在浏览器中直接调试 vue 源码了。
# 架构概览
compiler ⽬录是编译模板;
core ⽬录是 vue 的核⼼;
entries ⽬录是⽣产打包的⼊口;
platforms 目录是针对核⼼模块的 ”平台“ 模块,该目录下有 web 和 weex 两个目录,web 目录下有对应的 compiler、runtime、server 和 util 目录;
server 目录是处理服务端渲染;
sfc ⽬录处理单⽂件 .vue;
shared ⽬录提供全局用到的⼯具函数。
vue 是由 core + 对应的 “平台” 补充代码构成的(独立构建和运行时构建只是 platforms 下 web 平台的两种选择)。
vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。
components 模板编译的代码
global-api 最上层的⽂件接⼝(全局 api)
instance ⽣命周期 -> init.js
observer 数据收集与订阅
util 常⽤⼯具⽅法类
vdom 虚拟 dom
# 双向数据绑定
# 涉及的技术
Object.defineProperty
Observer
Watcher
Dep
Directive
# 整体流程分析
vue 采用数据劫持 & 发布-订阅模式的方式,通过 es5 提供的 Object.defineProperty 方法来劫持(监听)对象各属性的 getter 和 setter,并在数据发生变化时通知订阅者,触发相应的监听回调。
并且,由于是一个 watcher 对应一个组件,某个组件的数据变更只会引起对应组件的重新渲染,并不会影响其他组件。
vue 在初始化的时候,会通过 Object.defineProperty
方法去劫持 data 里的数据,并将它们的读写转化成 getter 和 setter,然后通过订阅者 watcher 的 get()
方法去触发 observer 里的 getter
。同时,触发 getter 的过程中会去调用 dep.depend()
方法,通过判断是否有 Dep.target
(Dep.target 就是 watcher
),也就是说判断是否是通过 watcher 初始化调用的,如果是,depend() 方法里就会通过 Dep.target.addDep()
去收集依赖,依赖收集完成后,再把 Dep.target 置为 null,这样下次再获取同样的数据时,就不会重复收集依赖了。接着,再把获取到的数据通过 watcher.update()
重新渲染视图完成更新。到此,第一次初始化的过程就完成了。
当我们修改数据的时候,就会触发 setter
,setter 通过调用 dep.notify()
方法(notify -> update -> run)去通知对应的 watcher,watcher 再去告诉对应的 Directive 更新视图。
因此,总的来说,要实现 vue 中的双向数据绑定,主要可以分为三个模块:Observer、Compiler、Watcher。
Observer 是数据监听器,负责对数据对象的所有属性进行监听,监听到数据变化后通知订阅者。
Compiler 是指令解析器,扫描 vue 模板,并对指令进行解析,然后绑定事件。
Watcher 是订阅者,用于关联 Observer 和 Compiler,能够订阅并收到数据变化的通知,执行指令绑定的相应操作,更新视图。通过自身的 update 方法去执行 Compiler 中绑定的回调,更新视图。
# Object.defineProperty
Object.defineProperty (opens new window) 可以用来修改一个对象的属性,或者在对象上定义一个新增的属性,并返回该对象。
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function defineGet() {
console.log(`get key:${key},value:${value}`);
return value;
},
set: function defineSet(newValue) {
console.log(`set key:${key},value:${newValue}`);
value = newValue;
}
});
}
function observe(data) {
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
}
let arr = ["abs", 2, 3];
observe(arr);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Object.defineProperty 的缺点就是不能监听数组或对象新增的 key。
this.$set() 和 Vue.set()
受 es5 的限制,vue 不能检测到对象属性的添加或删除。因为 vue 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 vue 转换它,才能让它是响应的。
当需要新增属性时,正确写法如下:
this.$set(this.data, key, value);
Vue.set() 方法用于向嵌套对象添加响应式属性。
const vm = new Vue({
el:'#test',
data:{
user:{
name:'小明';
}
}
});
Vue.set(vm.user, 'sex', '男');
2
3
4
5
6
7
8
9
vue 不允许动态添加根级响应式属性。比如以下代码会报错:
const app = new Vue({
data: {
a: 1
}
// render: h => h(Suduko)
}).$mount('#app1');
Vue.set(app.data, 'b', 2);
2
3
4
5
6
7
8
# 重写数组的方法
Object.defineProperty 在监听对象的时候,会先遍历它所有的 key,然后设置 getter 和 setter,进行监听。对象的 key 就是它的属性,数组的 key 就是它的索引。而在监听数组的时候,则会重写数组的原型方法。
为什么 vue 要重写数组的方法而不重写对象的呢?
这是因为数组的变更(插入、删除元素)会导致频繁移位,从而触发多次 getter 和 setter,最终会导致视图频繁更新。
vue 源码中重写数组的代码在 core/observer/array.js
中,可以看到,重写的都是数组中会造成频繁移位的方法。并且,在重写数组方法的过程中都没有触发 getter 和 setter,而是在最后手动去调用 notify 方法通知视图更新。
重写数组方法的方式就是,如果是数组,并且调用的方法会改变数组长度,则会重新增加索引之后更新数组,重新进行监听。
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from "../util/index";
const arrayProto = Array.prototype; // Array 构造函数的原型对象
export const arrayMethods = Object.create(arrayProto); // 新对象继承了数组构造方法的原型对象
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function(method) {
// cache original method
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
let inserted; // 新增项
switch (method) {
// push、unshift、splice 都会新增索引
case "push":
case "unshift":
inserted = args;
break;
case "splice":
inserted = args.slice(2);
break;
}
if (inserted) ob.observeArray(inserted); // 新增索引,才会重新处理成响应数据
// notify change
ob.dep.notify(); // 触发视图更新
return result;
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 模板编译
# 整体流程
.vue 文件 => 打包编译(词法分析、语法分析、构建 ast、转义 js)=> 输出 render() 函数
# 细节分析
vue2 主要是用正则表达式去匹配模板的,当页面资源多的时候效率会很低。这也是 vue2 为什么改版的一个主要原因。
什么是运行时?
当一段程序要运行的时候,它必须先装载到内存中,然后 cpu 再去运行它,在这个运行的过程中,会保存当前程序运行的数据。这些保存的数据就是运行时,当前保留的状态就是运行时状态。
运行时(Runtime)就是当前程序运行过程中,保留的数据和状态。
- .vue 文件是在上线前就在本地被编译成了 js,属于离线编译;但是平时用下面这种方法写的 template 是在上线之后才会被编译成 js 的,属于在线编译。
离线编译和在线编译都是采用正则的方式。
new Vue({
data: {
a: 1
},
template: "<div>{a}</div>"
});
2
3
4
5
6
因为本地分析的时候是不会分析 js 代码的,只会分析 .vue 文件的内容。只有在运行的时候才会去分析 js 代码。
因此,推荐使用 .vue 文件的方式来书写 vue 代码,这样上线的时候就已经是编译好的 js 代码了,可以直接构建运行时,运行效率更快。这是使用 vue 时的一个优化点。
vue 的编译打包方式有
Runtime
和Runtime + compiler
两种,这个 complier 就是用来在线编译解析 vue 模板的。使用后者打包后的 vendor 文件还会更大,更占用资源。因此,推荐直接使用 Runtime 进行编译打包。这也是使用 vue 时的一个优化点。vue 代码编译打包后的内容是包裹在 with 里面的。
<div id="app">{{ msg }}</div>
编译后的代码如下:
function render() {
with (this) {
return _c(
"div",
{
attrs: {
id: "app"
}
},
[_v(_s(msg))]
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
vue 源码中使用 with 的场景和原因
在 vue 的源码中,with 语句主要用于简化对对象属性的访问,以减少代码中的重复性,提高代码的可读性和简洁性。with 语句的使用场景主要集中在编译模板的过程中,特别是在处理模板字符串时。
- 简化属性访问:使用 with 语句可以省略对对象的重复访问,提高代码的可读性。例如,如果模板中的大部分内容都在某个对象下,可以使用 with 语句将对象引入到作用域链中,从而简化对属性的访问。
with (obj) {
console.log(prop1); // 等同于 console.log(obj.prop1);
console.log(prop2); // 等同于 console.log(obj.prop2);
}
2
3
4
- 模板字符串的编译:在编译模板字符串为渲染函数时,with 语句用于引入组件实例的作用域,使得在模板中直接访问组件实例的数据和方法变得更加方便。
// 简化前
function render() {
_c('div', { class: vm.someClass, on: { click: vm.handleClick } }, [
_v(vm.message)
])
}
// 简化后
// 在这里,with 语句将组件实例 vm 引入到作用域链中,使得在模板中直接访问 vm 下的属性和方法更加简洁
function render() {
with (vm) {
_c('div', { class: someClass, on: { click: handleClick } }, [
_v(message)
])
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
然而,需要注意的是,with (opens new window) 语句在 JavaScript 中有一些潜在的问题,可能导致作用域链的不确定性,影响代码的可维护性。因此,在一般的 JavaScript 开发中,并不推荐过多地使用 with 语句。在 vue 的源码中,with 的使用是在编译模板的特定环境下,为了简化模板编译过程而进行的优化。
# 虚拟 dom
vue2 的虚拟 dom 是基于 snabbdom (opens new window) 实现的,并不是它自己独立实现的。