彻底理解前端依赖收集
依赖收集是 Vue.js 和 Mobx.js 核心的之一,那么依赖追踪算法如何工作呢?本文将带读者自己动手实现一个依赖收集的库。
早起很多双向绑定的框架,Model和Dom同步更新,或者现在较多场景Model更新时,Dom自动更新。
例子1:
data = {
label: '深圳欢迎你'
};
bind(dom, data); //实现一个bind函数,让dom和data绑定起来
data.label = "北京欢迎你"; // dom 上显示的label就是"北京欢迎你"
例子2:
计算属性 sum
data = {
num: 3,
price: 20,
sum: 60 // sum = num * price
}
那么每次data.num和data.price改变,自动更新data.sum
这两个例子用Object.defineProperty很好实现。但是耦合度很高。
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
get () { return val },
set (newValue) {
// 例子1 这里要写dom更新的代码
// 例子2 这里要写sum更新数据的代码
val = newValue
}
})
}
也就是被依赖者要知道依赖者的存在。 那如果一个源数据被很多方依赖,那么在set部分的代码将越来越多。
那现在 mobx 和 vuejs 的依赖收集是怎么做的呢?
我们先看个题目,计算某个同学的岁数?
那么如何实现呢?首先,依赖于当前的年份,其次,依赖于这个同学的出生年份。
let current = {
year: 2018
};
let student = {
born: 1995
};
我们需要两个函数,defineReactive(obj, key) 和 defineComputed(obj, key, computeFn, updateCallback) 来实现整个过程。
let agedObj = {
age: 0 // 初始的岁数
};
defineReactive(current, 'year');
defineReactive(student, 'born');
defineComputed(agedObj, age, () => current.year - student.born, value => {
console.log(`this student's aged is ${value}`);
});
// 标记当前正在求值的 computed 函数
let Dep = null
// 定义 computed,需传入求值函数与 computed 更新时触发的回调
function defineComputed (obj, key, computeFn, updateCallback) {
// 封装供 reactive 收集的更新回调,以触发 computed 的更新事件
const onDependencyUpdated = function () {
// 在此调用 computeFn 计算出的值用于触发 computed 的更新事件
// 供后续可能的 watch 等模块使用
const value = computeFn()
updateCallback(value)
}
Object.defineProperty(obj, key, {
get () {
// 标记当前依赖,供 reactive 收集
Dep = onDependencyUpdated
// 调用求值函数,中途收集依赖
const value = computeFn()
// 完成求值后,清空标记
Dep = null
// 最终返回的 getter 结果
return value
},
// 计算属性无法 set
set () {}
})
}
// 通过 getter 与 setter 定义出一个 reactive
function defineReactive (obj, key) {
let val = obj[key];
// 在此标记哪些 computed 依赖了该 reactive
const deps = []
Object.defineProperty(obj, key, {
// 为 reactive 求值时,收集其依赖
get () {
if (Dep) deps.push(Dep)
// 返回 val 值作为 getter 求值结果
return val;
},
// 为 reactive 赋值时,更新所有依赖它的计算属性
set (newValue) {
// 在 setter 中更新值
val = newValue
// 更新值后触发所有 computed 依赖更新
deps.forEach(changeFn => changeFn())
}
})
}
其实原理很简单,就是defineComputed(obj, key, computeFn, updateCallback) 在调用的时候,先把computeFn和updateCallback组成的一个新的函数赋给全局的Dep函数,因为computeFn在执行的时候回去调用依赖对象的字段的get方法。 get 方法在调用的时候,会把Dep函数作为依赖收集起来,在set的时候会把所有依赖函数调用更新。