是什么
是指在数据状态发生变化时,让对应的重新DOM渲染。这里我们首先了解对象(Object)变化时,怎么通知到外界的。
(1)简单来看,第一次渲染的时候数据传递就是这样一个过程
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLiR2N1YmMyQTZkNmYzQDNhJ2M1QzY0MDOxgjZhZTMihzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
这里的外界,指的是要渲染的模板,或者要暴露出去的watch监听方法等。
(2)当数据发生变化时,要让外界知道数据的变化。所以数据和外界的关系应当如下。
(3)然后就是怎么通知的问题了,这就是我们这次要将的Object变化侦测,就是要监听数据的变化,并且通知到外界。这个监听器我们暂且定义为watcher。watcher监听着数据的变化,并负责通知外界。
(4)接下来,就是研究怎么去监听数据的变化了。
Vue2.0中使用的监听方法是,使用Object.defineProperty()给对象设置setter和getter方法,getter和setter方法监听数据的变化。
简单实现简单监听的Demo:
function defindReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log('get data')
return val;
},
set: function (newVal) {
console.log('set data')
if (val === newVal) {
return
}
val = newVal
}
});
}
let numData = {num: 1};
let key = 'num';
// 设置监听
defindReactive(numData, key, 2);
numData.num = 3
console.log(numData.num)
// 输出
set data
get data
3
Object.defineProperty(obj, prop, descriptor);
具体参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
可以看出在设置好监听后,我们再对对象设置或读取时,都可以触发自定义的操作。
(5)能监听到数据变化后,就要在数据变化时通知到使用该数据的地方,如某个对象的name字段值被修改了,就需要通知页面上展示字段name的地方修改值。这里展示name的地方也可以称为该数据的依赖。所以我们在监听到数据变化后,要做的主要工作就有:1. 收集依赖。 2. 通知依赖变化。
为了减少耦合,我们单独定义一个依赖收集器Dep。
我们首先定义一个依赖收集器:
// 依赖收集器
class Dep {
// 创建对象时,初始化收集器的数组
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
remove(this.subs, sub);
}
// 依赖
depend() {
if (window.target) {
this.addSub(window.target)
}
}
// 通知
notify() {
// 复制数组
const subs = this.subs.slice()
for (let i = 0; i < subs.length; i++) {
subs[i].update()
}
}
}
function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if(index > -1) {
arr.splice(index, 1)
}
}
}
然后在数据getter时收集依赖,在setter时触发依赖:
// 数据监听和依赖收集器结合
function defindReactive(data, key, val) {
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
// 收集依赖
dep.depend()
return val;
},
set: function (newVal) {
console.log('set data')
if (val === newVal) {
return
}
val = newVal
// 通知依赖发生变化
dep.notify();
}
});
}
(6)对于Object,我们希望把对象所有的属性以及后代属性都监听到,所以我们还需要定义一个类,把数据内所有的属性的都转换成getter和setter形式。我们把它定义为Observer。
class Observer {
constructor(value) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value);
}
}
walk(obj) {
for (let key in obj) {
defineReactive(obj, key, obj[key]);
console.log('value', obj[key]);
console.log('key', key);
}
}
}
简单测试一下:
let user = {
name: 'hello',
age: 223
}
let ob = new Observer(user)
// 打印输出
value hello
key name
value 223
key age
再改造一下defineReactive:
// 数据监听和依赖收集器结合
function defindReactive(data, key, val) {
// 新增点
if (typeof val === 'object) {
new Observer(val)
}
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
// 收集依赖
dep.depend()
return val;
},
set: function (newVal) {
console.log('set data')
if (val === newVal) {
return
}
val = newVal
// 通知依赖发生变化
dep.notify();
}
});
}
(7)最后实现Watcher的工作:
const bailRE = /[^\w.$]/
// 匹配path是否满足 a.b.c这样的格式。返回一个函数
export default function parsePath(path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function(obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
}
}
class watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
get() {
window.target = this
let value = this.getter.call(this.vm, this.vm);
window.target = undefined
return value
}
set() {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
代码中的window.target用来表示当前依赖。
Object侦测的简单逻辑图