Vue 数据双向响应机制
博主wx: -GuanEr-
,加博主进前端交流群
参考资料(感谢各位前辈的分享和资料)
尤雨溪讲解 Vue 源码
Vue 源码解析-Vue 中文社区
小马哥 Vue 源码解析
小马哥 Vue 源码解析代码示范
vue-cli 源码
MDN
Vue 的特点是数据驱动视图,也就是说,数据变化时视图随之变化, 所以要先监听到数据的变化,然后再去响应依赖该数据的视图,Vue 使用 Object 的 defineProperty 函数劫持数据的变化,用 Complite 源码解析器解析我们编写的 Vue 代码, 用 Dep 类收集数据依赖,使用 Watcher 类响应数据变化并更新视图。
一、Complite
源码解析器
因为要结合 Watcher
,所以 Complite
只在这里做简单的介绍,如果有时间单独写一篇.
我们在 .vue
的文件中编写的代码,或者创建 Vue
实例后编写的代码,部分如指令、函数,直接在 DOM
中渲染调用并计算变量等是不被浏览器直接识别和解析的,所以在代码正常渲染在浏览器并执行业务逻辑之前,Vue
要先将我们的代码进行编译。
编译流程如下(来自 Vue 中文社区 Vue 源码解析,全文链接在文章开头):
我们编写的 Vue
实例的结构代码其实是一个字符串,这个字符串被 Vue
的 Complite
源码解析器编译成抽象语法数AST
(DOM 节点及属性以对象嵌套的形式存在),然后根据 AST
为每个节点生成对应的 render
函数,调用某个节点的 render
函数就能形成对应的 VNode
。
Complite
除了要要编译源码之外,还要捕获数据变化,也就是绑定 Watcher
,然后根据 Watcher
类的,更新视图。
二、Observer
类
1. Object.defineProperty
函数 ( Object.defineProperty(obj, key, desc);
)
- 为一个对象添加属性,或者修改对象已有的属性,并返回这个对象。
- 由
Object
构造器直接调用,对象的实例不能调用。 - 接受三个参数,
obj
指要操作属性的源对象,key
指要操作的这个属性,desc
指要操作的属性的描述对象。 - 可以定义
Symbol
类型的数据作为key
。 - 对象属性的描述中,非常重要的两个函数
get
和set
。 - 更具体的内容,MDN 上有非常详细的说明,建议一看。
Object.defineProperty(obj, 'foo', {
get() {
// 每当 obj 访问 foo 属性时,get 函数会自动执行
// 不传入参数,会传入 this,但是 this 不一定指向 obj
// 该函数的返回值会被当做是 foo 的属性值
},
set(newVal) {
// 当 foo 属性值被修改时,set 函数会自动执行
// 接受一个参数 newVal,是为 foo 赋的新值
// 会传入 赋值时的 this 对象
}
});
2. Object.defineProperty
函数和 vue-cli
的关联
在 Vue
中,一个组件是一个 VueComponent
实例,每个实例有自己的 $data
对象,其中存储的是当前组件的数据列表,要让每个数据动态响应,就需要为每个数据添加 get
和 set
,来劫持数据值的改变。
// 为指定对象的每个键添加 setter 和 getter
function covert(obj) {
const keyList = Object.keys(obj);
keyList.forEach(key => {
// _val 如果该属性已经存在,就是上一次的赋值,不存在为undifined
// 每个 key 的操作都会形成一个闭包,所以这个闭包就成了单独存储对象某个属性值(_val)的位置
let _val = obj[key];
Object.defineProperty(obj, key, {
get() {
// 在这里可以捕获到属性的变化,并限制取值操作,如果 return 一个固定值,那么 obj 的某个键就永远是这个值,重新赋值也没用
return _val;
},
set(val) {
// 这里可以对比属性的新旧值,并限制赋值操作
_val = val;
}
});
});
}
// -----------------------------------------------
// 调用
const data = {
visible: true,
num: 10
};
covert(data); // 为 data 的两个属性绑定 get 和 set
data.visible = false; // 调用 set
console.log(data.visible); // 调用 get
3. vue-cli
源码中的 Observer
类简介(极简)
class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 数组的另一套劫持方式
} else {
this.walk(value)
}
}
walk (obj: Object) {
// 为每个键绑定 get 和 set
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
// 数组的特殊操作:由于数组数据类型的特殊性,数组的整体值的变更和劫持依旧在 set 和 get 中
// 但是 vue 为 Array 这个类的原型函数们添加了劫持,也就是说当数组的值发生改变时,要调用原型函数之前,先处理我们需要的业务操作
}
}
// ----------------------------------------------------------------
function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
...
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
...
},
set: function reactiveSetter (newVal) {
...
}
})
}
三、Dep
类
通过 Observer
类为数据们添加了 getter
和 setter
之后,就能观察到数据的变化,同时我们要改变跟这个数据有关系的一系列内容,可能是其他数据,也可能是页面渲染的内容。那我们就需要知道使用到这个数据的都有谁,才可以在当前数据变化时通知他们做响应。
1. 依赖、依赖收集和通知依赖
- 某个数据
A
直接控制的内容,或者通过A
计算得到的内容,都被称为依赖了A
。 - 都有谁依赖了
A
,需要一个准确的计量,才能在A
每次改变时,执行对应的操作,这种计量方式是一个数组,因为依赖A
的数据很可能有多个。 - 确定谁依赖了数据
A
,并将这些依赖收集起来:依赖数据A
一定要获取数据A
,所以在A
的getter
中,做依赖收集。 - 数据
A
每次发生值的改变时,setter
会执行,所以在A
的setter
中通知依赖。
2. 依赖的操作
依赖可能被添加,也有可能伴随着组件卸载、销毁而被删除,所以依赖除了要被声明之外,还要有其他操作,依赖类 Dep
就是用来为每个数据创建依赖并处理依赖的。
3. vue-cli
中的 Dep
类
...
let uid = 0
class Dep {
static target: ?Watcher; // 静态属性,Dep 构造器访问,Dep 的实例不能访问
id: number; // 每个 Dep 实例唯一的id
subs: Array<Watcher>;
constructor () {
this.id = uid++
// 某个数据的依赖列表
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
// 当某个数据的值发生变化时,要循环这个数据的依赖列表,并且让他们相关的所有操作都更新一次
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
...
4. Observer
中调用 Dep
(简写)
class Observer {
constructor() {
this.dep = new Dep();
}
...
}
function defineReactive() {
Object.defineProperty(obj, key, {
get() {
// get 被调用一次,都代表有一个数据依赖于当前数据,要向当前数据的依赖列表 subs 中添加一个依赖
dep.depend()
...
},
set() {
// set 被调用且值发生改变时,代表当前数据更新,那么依赖于当前数据的所有内容都要发生对应变化
dep.notify()
...
}
...
});
}
四、Watcher
类
上面的 Dep
类只是对数据的依赖进行管理的一套方式,从 Dep
的源码中我们可以捕捉到,真正的被添加到 subs
数组中的依赖是 Watcher
。
1. 什么是 Watcher
某个表达式 Express
,用到到了某个数据 A
,我们就说 Express
订阅了 A
,为了能让这个 Express
在 A
每次发生变化时,都能动态的重新计算自己,我们就为它编写一个 update
方法来更新自己并做后续的数据渲染。每次 A
变化,都通知 Express
让它 update
。
在 Vue
中,到处都是这样的订阅与响应,所以产生了 Watcher
类,专门处理数据变化之后其依赖们的响应动作。
2. Watcher
需要具备的功能
以下三点来自小马哥源码解析及总结
- 在自身实例化时往属性订阅器(
dep
)里面添加自己。 - 自身必须有一个
update()
方法。 - 待属性变动
dep.notify()
通知时,能调用自身的update()
方法,并触发Compile中绑定的回调,则功成身退。
3. 结合 Observer
和 Dep
,整个数据双向响应流程如下:
- 数据初始化,初始化了
dep
属性,继承了Dep
类的subs
属性,来承接依赖列表; - 为数据绑定了
getter
和setter
; - 数据每次被调用都代表被订阅,
getter
返回数据值的同时,向subs
数组中添加了一个Watcher
; - 数据值发生改变,调用
setter
,setter
设置数据值的同时,调用Dep
提供的notify
函数,来通知该数据的依赖做出响应; notify
函数内部遍历依赖列表(即订阅者Watcher
列表)subs
数组,调用每个订阅者的update
函数,让其根绝数据新的值重新计算自己。