一、什么是响应式?
在理解什么是响应式之前咱们现来看一段代码演示
let x;
let y;
let f = n => n * 100
x = 1;
y = f(x);
console.log(y); // 100
x = 2;
y = f(x);
console.log(y); // 200
x = 3;
y = f(x);
console.log(y); // 300
代码示例中,变量y依赖变量x进行求值,然而咱们会发现每一次变量x从新赋值时都要手动对y进行求值,存在大量的反复模板,因而,领导咱们进行程序设计的DRY准则就施展价值了
DRY 全称:Don’t Repeat Yourself (摘自wikipedia),是指编程过程中不写反复代码,将可能公共的局部形象进去,封装成工具类或者用“abstraction”类来形象私有的货色,升高代码的耦合性,这样不仅进步代码的灵活性、健壮性以及可读性,也不便前期的保护或者批改。
那么咱们须要有一个办法,实现主动监听x的变动并且主动对y进行求值,以缩小反复代码
假如咱们有一个onXChange函数,使得每次x从新赋值时都会触发onXChange中的回调函数,你的代码看起来应该像上面这样:
let x;
let y;
let onXChange = function(cb) {
// ...
}
onXChange(() => {
y = f(x);
console.log(y);
})
x = 1; // 100
x = 2; // 200
x = 3; // 300
如果将y换成dom模板,依据x的变动主动渲染不同的模板也是同理。
当初咱们能够来解释什么是响应式了(其实都不必我解释,看到这你本人也有答案了),响应式只是一种编程形式,它的目标是为了简化编程,特点是主动对变动进行响应。
以下是摘自wikipedia的解释:
响应式(Reactive Programming)
是一种面向数据流和变动流传的编程范式。这意味着能够在编程语言中很不便的表白动态或动静的数据流,而相干的计算模型会主动将变动的值通过数据流进行流传。
二、Vue中的响应式剖析
咱们来看看Vue中是怎么实现响应式的
首先第一步须要监听数据变动,晓得变量什么时候进行了批改,JS提供的可能监听数据变动的API有Object.defineProperty以及ES6新增的Proxy,本节咱们只探讨Object.defineProperty
对于Object.defineProperty的应用如果你还不理解的话请浏览如下文档:
https://developer.mozilla.org…
Vue2.0中应用了Object.defineProperty来遍历data中的数据,在getter中将应用到这个数据的上下文进行收集,这个过程称之为【依赖收集】,在setter中批改这个数据时则会触发【告诉依赖更新】的操作,如下图所示:
什么是依赖收集?
所谓依赖收集,就是把一个数据用到的中央收集起来,在这个数据产生扭转的时候,对立去告诉各个中央做对应的操作。
为什么这里须要依赖收集?
思考到一个变量的批改可能会引起多处变动,因而须要将依赖这个变量的所有中央都收集起来,等到变量更新时再进行批量操作。
Vue对于响应式原理的介绍官网曾经说的很分明,这里贴出链接不再赘述:
https://cn.vuejs.org/v2/guide…
三、实现一个简略的数据响应
以下面的代码为例,咱们须要通过onXChange函数来监听x的批改,因为下面代码中x的值是根底类型,咱们须要将x更改为援用类型才能够应用Object.defineProperty,因而咱们能够创立一个函数来做这件事件,假如这个函数为ref
ref函数接管一个初始值,函数内闭包一个value变量赋值为传入的初始值,通过Object.defineProperty返回一个带有value属性的对象,在get中返回value,并在set中将value赋值,这样在咱们批改了x.value之后会主动触发set来更新闭包的value变量
let ref = initValue => {
let value = initValue;
return Object.defineProperty({}, 'value', {
get() {
return value;
},
set(newValue) {
value = newValue;
}
})
}
对应的代码也须要批改一下:
- 批改x为ref函数调用的返回值
- 将对应的x赋值更改为x.value赋值
let x;
let y;
let f = n => n * 100
let onXChange = function(cb) {
// ...
}
let ref = initValue => {
let value = initValue;
return Object.defineProperty({}, 'value', {
get() {
return value;
},
set(newValue) {
value = newValue;
}
})
}
// 创立x对象,初始value传入1
x = ref(1);
// 监听x
onXChange(() => {
y = f(x.value);
console.log(y);
})
x.value = 2;
x.value = 3;
到这一步曾经能够主动获取到x.value扭转后的值了,咱们能够在set办法中打印newValue
set(newValue) {
console.log('x: ', newValue)
value = newValue;
}
既然曾经监听到x.value的批改了,接下来咱们只须要拿到onXChange中的回调函数,在set办法中调用它就能够同步批改y的值
怎么拿这个回调函数?
咱们能够创立一个变量将这个回调函数存起来,假如变量名为active,而后在set办法中调用active函数即可
// 省略局部代码...
// 创立active变量
let active;
let onXChange = function(cb) {
// 将回调赋值给active
active = cb;
}
let ref = initValue => {
let value = initValue;
return Object.defineProperty({}, 'value', {
get() {
return value;
},
set(newValue) {
value = newValue;
active(); // 调用active函数
}
})
}
能够看到y的打印后果进去了,但少了x.value初始为1时的后果,咱们还须要在初始的时候调用一次active
// 执行onXChange时就调用一次回调函数
let onXChange = function(cb) {
active = cb;
active();
active = null; // 销毁active,防止批改x.value时反复增加依赖
}
四、联合Vue源码来看响应式
到这里一个简略的响应式其实曾经实现了,但还不够,如果咱们不仅有onXChange,还有onYChange、onZChange呢,这些函数都依赖了x变量怎么办?
Vue的解决办法是通过一个Dep对象将这些依赖都收集起来,在变量产生扭转时进行批量告诉更新。
那么Dep对象至多应该具备一个存储依赖的列表、一个增加依赖的办法和一个告诉依赖更新的办法
咱们先来简略实现一下,再联合Vue源码验证
Dep代码如下:
class Dep {
deps = new Set();
// 收集依赖
depend() {
if (active) {
this.deps.add(active);
}
}
// 批量更新
// 将所有的依赖都执行一遍
notify() {
this.deps.forEach(dep => dep());
}
}
而后咱们须要在ref函数中获取dep实例,在get时调用dep.depend()增加依赖,在set时调用dep.notify来告诉依赖更新
let ref = (initValue) => {
let value = initValue;
// 获取dep实例
let dep = new Dep();
return Object.defineProperty({}, "value", {
get() {
// 增加依赖
dep.depend();
return value;
},
set(newValue) {
value = newValue;
// 告诉依赖更新
dep.notify();
},
});
};
当初来验证一下,咱们增加任意个依赖x变量的函数:
let onYChange = function(cb) {
active = cb;
active();
}
onYChange(() => {
console.log('onYChange', f(x.value));
})
// ......
能够看到所有依赖x变量的中央都打印了后果,所有都没有问题。
那么Vue的源码是不是这么实现的呢?
以vue2.6.11版本为例:
defineReactive$$1函数的作用就是通过Object.defineProperty来将一般的数据处理成响应式数据,残缺代码如下:
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
去除源码中影响浏览的代码:
function defineReactive$$1 (
obj,
key,
val,
) {
var dep = new Dep();
// 省略局部代码...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
if (Dep.target) {
dep.depend();
}
return value
},
set: function reactiveSetter (newVal) {
val = newVal;
dep.notify();
}
});
}
与咱们本人实现的ref函数比照一下,你Get到了吗?
再看源码中的Dep是怎么实现的:
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
Dep.prototype.removeSub = function removeSub (sub) {
remove(this.subs, sub);
};
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (!config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
先不必管别的代码,至多咱们在Dep源码中找到了一个存储依赖的列表subs、增加依赖的办法depend、告诉依赖更新的办法notify,看到这里我想你对Vue的响应式原理曾经有本人的了解了。
那么咱们再来总结一下Vue的响应式原理:
- 将data中的数据通过Object.defineProperty解决成响应式数据
- 数据被【读】的时候会触发getter,将应用到这个数据的上下文进行依赖收集,寄存到Dep类中
- 数据被【写】的时候会触发setter,调用Dep.notify办法告诉依赖更新
不对的中央请斧正,但不要批评我,不听哈哈哈!以上演示代码已上传github:
https://github.com/Mr-Jemp/Vu…
前面要学习的内容在这里:
Vue—对于响应式(二、异步更新队列原理剖析)
Vue—对于响应式(三、Diff Patch原理剖析)
Vue—对于响应式(四、深刻学习Vue响应式源码)
本文由博客一文多发平台 OpenWrite 公布!
发表回复