interview-answe
interview-answe copied to clipboard
165.[vue]Vue 的数据双向绑定原理 - 响应式系统的基本原理
[vue]
Vue 的数据双向绑定原理 - 响应式系统的基本原理
我是看了掘金上的小册《剖析 Vue.js 内部运行机制》,然后理解梳理了一遍,记录一下 ~ 99%是从小册上搬运下来的,请叫我搬运侠 ~
Object.defineProperty
// Vue.js就是基于Object.defineProperty实现「 响应式系统 」的
/*
obj: 目标对象
prop: 需要操作的目标对象的属性名
descriptor: 描述符
return value 传入对象
*/
Object.defineProperty(obj, prop, descriptor)
下边是descriptor的一些简单属性 :
· enumerable,属性是否可枚举,默认 false。
· configurable,属性是否可以被修改或者删除,默认 false。
· get,获取属性的方法。
· set,设置属性的方法。
例子测试 ( 下边代码可直接新建 index.js,运行测试 )
我们知道 Object.defineProperty 之后,将使用它将对象变成可观察的, 下面只对对象进行处理
// 定义一个函数 @ChangeView
function ChangeView(val) {
console.log('视图更新啦 ~ 新值为 : ' + val);
}
我们定义一个函数 defineReactiv,这个函数通过 Object.defineProperty 来实现对对象的「响应式」化。
经过 defineReactive ,obj 的 key 属性在「读」的时候会触发 reactiveGetter 方法,而在该属性被「写」的时候则会触发 reactiveSetter 方法
/*
* obj : 需要绑定的对象
* key : 对象的某一属性
* val : 具体的值
*/
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true /* 属性可枚举 */,
configurable: true /* 属性可被修改或删除 */,
get: function reactiveGetter() {
// 依赖收集
return val;
},
set: function reactiveSetter(newVal) {
if (val === newVal) {
return;
}
ChangeView(newVal); // 调用函数,通知视图更新
}
});
}
// 但这是不够的,我们的对象的某些属性可能还是对象,所以封装一层observer
// 这个函数传入一个 obj(需要「响应式」化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理。
//(注:源码中实际是 observer 会进行递归调用,为了便于理解去掉了递归的过程)
function observer(obj) {
if (!obj || typeof obj != 'object') {
return;
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
// 这样我们就大功告成了
class Vue {
constructor(options) {
this.data = options.data;
observer(this.data);
}
}
let t_vue = new Vue({
data: {
text: '这是一个测试'
}
});
t_vue.data.text = '改变测试值';
依赖收集
为什么要依赖收集
比如我们现在有是这种情况
new Vue({
template :
`<div>
<p>{{ message1 }}</p>
<p>{{ message2 }}</p>
</div>`,
data() : {
return {
message1: '我是1',
message2: '我是2',
message3: '我是3'
}
}
})
// 然后我们将修改了message3
this.message3 = '修改message3'
但是我们实际在 template 中并未使用到 message3 的值,所以我们不需要调用上边说的 ChangeView()来通知视图更新。还有一种情况就是这样的 :
let globaldata = {
message: '我是全局的数据'
};
let v1 = new Vue({
template: `<p>{{ message }}</p>`,
data: globaldata
});
let v2 = new Vue({
template: `<p>{{ message }}</p>`,
data: globaldata
});
// 这时候我们将修改message的值
globaldata.mesage = '修改全局的值';
我们应该需要通知 v1 以及 v2 两个 vm 实例进行视图的更新,「依赖收集」会让 message 这个数据知道“哦~有两个地方依赖我的数据,我变化的时候需要通知它们~”。
如何收集依赖
订阅者 Dep,它的主要作用是用来存放 Watcher 观察者对象。
class Dep {
constructor() {
// 用来存放所有Watcher对象的数组
this.subs = [];
}
// 添加一个 watcher对象
addSub(sub) {
this.subs.push(sub); //
}
// 通知所有视图更新
notify() {
this.subs.forEach(sub => {
sub.update();
});
}
}
观察者 Watcher
class Watcher {
constructor() {
/* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
Dep.target = this;
}
// 更新视图
update() {
console.log('试图更新啦 ~');
}
}
开始依赖收集
在闭包中添加 Dep 类的对象,用来收集 Watcher 对象。在对象被「读」的时候,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果当该对象被「写」的时候,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图
function defineReactive(obj, key, val) {
// 一个 Dep 类对象
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true /* 属性可枚举 */,
configurable: true /* 属性可被修改或删除 */,
get: function reactiveGetter() {
// 依赖收集,将Dep.target(即当前的Watcher对象存入dep的subs中)
dep.addSub(Dep.target);
return val;
},
set: function reactiveSetter(newVal) {
if (val === newVal) {
return;
}
/* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
dep.notify();
}
});
}
class Vue {
constructor(options) {
this.data = options.data;
observer(this.data);
/* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象 */
new Watcher();
/* 在这里模拟render的过程,为了触发test属性的get函数 */
console.log('render~', this.data.test);
}
}
讲得有点模糊,总结一下
1 : 首先我们实现一个 class Vue,在其构造函数中,进行初始化以及调用 observer 方法监测数据,目的就是通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理。接着 new Watcher,这一步就是告诉订阅者,我是谁,但是真正的订阅是在数据被引用的时候
new Vue {
constructor (options) {
this.data = options.data
observer(this.data)
new Watcher()
}
}
2 : 在 observer 中,递归遍历对每个属性都通过 defineReactive 处理,在 defineReactive 中注册 get 方法,用来收集依赖,在它的闭包中会有一个 Dep 对象,这个对象用来存放 Watcher 对象的实例。
class Dep {
constructor() {
this.subs = []; // 存放所有watcher对象的数组
}
addSub(sub) {
this.subs.push(sub);
}
notify() {
this.subs.forEach(sub => {
sub.update();
});
}
}
class Watcher {
constructor() {
Dep.target = this;
}
update() {
console.log('视图更新啦');
}
}
function observer(obj) {
if (!obj || typeof obj != 'object') {
return;
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
function defineReactvie(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true /* 属性可枚举 */,
configurable: true /* 属性可被修改或删除 */,
get: function reactiveGetter() {
// 依赖收集,将Dep.target(即当前的Watcher对象存入dep的subs中)
dep.addSub(Dep.target);
return val;
},
set: function reactiveSetter(newVal) {
if (val === newVal) {
return;
}
/* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
dep.notify();
}
});
}
其实「依赖收集」的过程就是把 Watcher 实例存放到对应的 Dep 对象中去。新建一个 Watcher 对象只需要 new 出来,这时候 Dep.target 已经指向了这个 new 出来的 Watcher 对象。get 方法可以让当前的 Watcher 对象(Dep.target)存放到它的 subs 中(addSub)方法,在数据变化时,set 会调用 Dep 对象的 notify 方法通知它内部所有的 Watcher 对象进行视图更新。
再多说一句
可能你们会发现 99%跟染陌大佬说的一摸一样,没错,我就是跟着他的一步一步来的,他已经讲得很简单啦,我就只能当大自然的搬运工啦 ~
相关连接
染陌 VueDemo : https://github.com/answershuto/VueDemo
掘金小册 : https://juejin.im/book/5a36661851882538e2259c0f