依赖追踪机制是 Vue 的核心之一,那么依赖追踪算法如何工做呢?在 30 行内咱们就能实现它🤓前端
提及依赖追踪,就不能不提数据绑定的概念。前端最多见的重复劳动之一就是把数据绑定到 HTML 模板上,这时数据绑定可以实现数据更新时模板的自动更新。简单的三行伪代码就能描述出这个流程的实际使用场景:react
const data = { foo: 123 }
magic(data, dom) // 定义 Reactive 并绑定数据到 dom
data.foo = 456 // 数据更新时 dom 自动更新复制代码
这里的 magic
可以把普通的 JS 对象转换为支持数据绑定的 Reactive 对象,在 Reactive 对象数据更新时,被绑定的模板也会进行更新。做为依赖追踪的基础,咱们仍是先用几行实现一个最简单的 Reactive 示例吧:git
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
get () {
return val
},
set (newValue) {
// 在此添加更新绑定数据相关代码
val = newValue
}
})
}复制代码
能够看到,Reactive 自己其实就是经过 Object.defineProperperty
添加了自定义 getter / setter 后的对象。这类对象可以在读写其属性值时,执行用户自定的代码,从而在此实现被绑定数据的更新。有了这个能力后,咱们就能够开始编写依赖追踪器了。github
咱们能够用 Excel 来理解依赖追踪:Reactive 就是普通单元格中的原始数据,而 Computed 就是插入了公式的单元格。Reactive 的格子更新时,Computed 的格子会根据为它设定的求和公式(即依赖)来自动更新出相应的值。算法
因此,对 Computed 最朴素的定义,就是一个简单的函数,形如这样:数组
// 这里的 data 是一个 Reactive
const isEmpty = () => data.values.length === 0复制代码
这初看之下没有任何 Magic,不过这里关键的细节区别在于:在指定 Excel 的公式时,咱们须要手动选择公式所依赖的单元格,但在一个 Computed 函数中,咱们没有传入 Computed 的依赖!既然没有传入依赖,那么这个 Computed 函数是怎么在它所使用的 Reactive 更新时去更新自身的呢?这就是依赖追踪算法所须要解决的问题了。前端框架
咱们知道,Reactive 的数据绑定,本质上是在 set Reactive 时去执行更新。而依赖追踪则相反,须要在 Computed 中 get Reactive 时,去标记 Computed 对 Reactive 的依赖。mvc
为了理解这个算法,咱们不妨先假设一个简单的执行场景:假设 Computed 函数 C 依赖了 Reactive 对象 R1 和 R2,这时咱们添加一个全局的辅助对象 D 来为当前 Computed 函数收集依赖。从而,咱们能够用文字描述出这个算法的执行流程:框架
这个算法的核心,就是为 Reactive 添加【依赖者】数组,从而在 Computed 触发 Reactive 时,添加该 Computed 至 Reactive 的依赖者中。这样,在 Reactive 下次更新时,就可以主动地触发 Computed 的更新了。下面咱们使用代码来实现这个文字流程。dom
动手实现 Computed 前,咱们不妨设计出实际使用场景下一个简单的 API,而后从 API 接口出发来进行编码实现。假设咱们有一个 elder
对象,他具备 now
这个 Reactive 来标记当前年份,那么咱们能够定义出一个 Computed 来计算出他的年龄:
const elder = {}
defineReactive(elder, 'now', null)
defineComputed(elder, 'age', () => elder.now - 1926,
() => console.log('Now his age is', elder.age)
)
elder.now = 2016
console.log(elder.age)
elder.now = 2017
console.log(elder.age)复制代码
在使用方式上,能够发现咱们先是定义 Reactive,再定义从 Reactive 衍生出的 Computed 函数。
接下来就是代码实现了。咱们在前文的 defineReactive 函数基础上,拓展出新的 defineComputed 函数。去除掉啰嗦的注释后,是能够控制在 30 行内的😅
// 标记当前正在求值的 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, val) {
// 在此标记哪些 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 函数外,还在 defineReactive 函数中进行了必定的修改。这主要体如今,咱们在 Reactive 中:
这个在 getter 中收集依赖,然后在 setter 中触发的模式,实际就是本系列中第一篇 MVC 框架介绍中,所涉及的 PubSub 发布订阅模式了。不一样之处在于,在依赖收集器中,咱们经过 Object.defineProperty 这一高级特性将 PubSub 模式进行了封装,PubSub 中须要用户显式操做的【订阅】过程被平滑地优化为了【经过 getter 自动化进行的依赖收集】。依赖收集完成后,就能在 Reactive 更新时实现依赖追踪了。
OK,这就是依赖追踪器的基础实现了,本文的源码亦托管在 Github 上,能够拉取或直接复制到 Node 中运行🙃但愿对感兴趣的同窗有所帮助。
本系列后续会继续专一用简单的代码解释前端框架各种 Magic 的实现机制,安利往期文章: