响应式-数据驱动
所谓数据驱动就是开发者只关心数据的变更,而数据的变更将自动触发相应的视图更新,这样就大大降低了开发者需要关注的点,有利于形成统一的开发模式,提升效率。
而js恰好为我们提供了工具:Object.defineProperty
侦听属性变更的方式
我们的需求是当我们更改了对象的某个属性的值时,可以监听到这个变更去做一些事情,前文提到的Object.defineProperty可以帮我们做到:
var obj = {
name: 'jack'
}
function defineReactive(target, key, val) {
Object.defineProperty(target, key, {
enumerable: true, // 是否可以被枚举
configurable: true,
get() {
console.log(`${key}属性被读取了`)
return val
}, // getter
set(newVal) {
console.log(`${key}属性被重新设置了`)
val = newVal
}, // setter
})
}
defineReactive(obj, 'name', obj.name)
obj.name // 打印:'obj.name属性被读取了'
obj.name = 'tom' // 打印:'obj.name属性被重新设置了'
可以把上面的例子放到浏览器的console里面运行一下
上面的代码初步实现了我们的需求:监听对象属性的读取/变更,做一些事情。
可以实现以下代码来监听一个对象上面所有的属性:
function def(target, key, val, enumerable = true) {
Object.defineProperty(target, key, {
value: val,
enumerable: enumerable,
writable: true,
configurable: true
})
}
class Observer {
constructor(value) {
this.value = value
def(value, '__ob__', this) // 在value上面添加一个'__ob__'属性避免重复Observer,值指向this
this.walk(value)
}
walk(value) {
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
defineReactive(value, keys[i], value[keys[i]])
}
}
}
// 测试一下
var obj = {name: 'jack', age: 18, sex: 'male'}
var ob = new Observer(obj)
现在我们实现了监听对象的所有属性的读写,虽然有一些瑕疵(如果属性为引用类型,目前的方式还不能覆盖,后面会进行改进)
接下来考虑:如果对象的某个属性发生了变更,我们也监听到了,但是要通知谁去做相应的动作呢?是更新视图?还是...?问题点就在于目前我们还不知道对象的某个属性x的依赖者是谁,当属性x发生了变更我们去通知谁进行状态更新,由此就引入了一个概念:依赖收集
依赖收集
Object.defineProperty不只是能监听属性的写,也能监听对象的读,我们假设某个对象(并非js的对象)读取了对象的某个属性x,我们就有理由认为该对象依赖于属性x,以后属性x一旦更新我们就需要通知该对象进行相应的状态更新,可以实现如下代码
window.Deptarget = null
function remove(target, list) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
let uid = 0
class Dep {
constructor() {
this.id = uid++ // dep实例的唯一标识
this.subs = [] // 订阅者列表,保存订阅者对象
}
addSub(sub) { // 添加订阅者
this.subs.push(sub)
}
removeSub(sub) { // 移除订阅者
remove(sub, this.subs)
}
notify() {} // 通知订阅者(暂时先不实现)
}
function defineReactive(target, key, val) {
const dep = new Dep()
Object.defineProperty(target, key, {
enumerable: true, // 是否可以被枚举
configurable: true,
get() {
// 这里引入一个全局变量用于保存当前的订阅者
if (window.Deptarget) {
dep.addSub(window.Deptarget) // 将当前的订阅者加入dep.subs列表
}
return val
}, // getter
set(newVal) {
console.log(`${key}属性被重新设置了`)
val = newVal
}, // setter
})
}
上面代码扩展了defineReactive方法,引入类Dep,怎么理解Dep呢?
其实每一个实例dep和对象属性一一对应,可以把实例dep理解为对象属性的代理,上面保存了很多与该属性相关的内容。
依赖收集的过程:
- 对象的每一个属性通过
defineReactive处理设置了getter/setter - 订阅者读取对象的属性
- 触发属性getter,属性对应的dep将订阅者添加到dep.subs列表
思考:为什么要引入Dep?
因为我们要记录属性的订阅者,就需要对应一个抽象的对象用以保存这些,同时该对象上面实现了添加订阅者、移除订阅者、通知订阅者等方法
变更通知
现在我们已经做了:
- 遍历对象的每一个属性使其响应化
- 完成依赖收集 接下来实现对象属性变更后通知订阅者进行更新:
class Dep {
constructor() {
this.id = uid++ // dep实例的唯一标识
this.subs = [] // 订阅者列表,保存订阅者对象
}
addSub(sub) { // 添加订阅者
this.subs.push(sub)
}
removeSub(sub) { // 移除订阅者
remove(sub, this.subs)
}
notify() { // 通知订阅者
for (let i = 0; i< this.subs.length; i++) {
this.subs[i].update()
}
}
}
function defineReactive(target, key, val) {
const dep = new Dep()
Object.defineProperty(target, key, {
enumerable: true, // 是否可以被枚举
configurable: true,
get() {
// 这里引入一个全局变量用于保存当前的订阅者
if (Deptarget) {
dep.addSub(Deptarget) // 将当前的订阅者加入dep.subs列表
}
return val
}, // getter
set(newVal) {
if (newVal === val) return;
val = newVal
dep.notify() // 通知dep.subs列表中所有的订阅者
}, // setter
})
}
上述代码实现了Dep的实例方法notify,扩展了setter。当对象属性重新赋值的时候将触发setter执行dep.notify()通知订阅者更新
注意:这里的订阅者对象要实现update方法进行更新操作
订阅者
前面的小节基本已经理顺了从对象响应化到依赖收集再到变更通知的流程,但是还缺少一个重要角色:订阅者,那具体到代码中,订阅者应该是怎样的呢?
let uid = 0
class Watcher {
constructor(getter) {
this.id = uid++
this.getter = getter
this.value = this.get()
}
get() { // 首次执行逻辑触发依赖收集
window.Deptarget = this // 将当前Watcher赋值给全局变量,标识当前依赖收集的订阅者为this
return this.getter()
window.Deptarget = null
}
update() { // 实现dep要求的update方法
this.run()
}
run() { // 订阅者在依赖项变更所需要做的动作
this.value = this.get()
}
}
整理一下完整版代码:
let uid = 0
function remove(target, list) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
function def(target, key, val, enumerable = true) {
Object.defineProperty(target, key, {
value: val,
enumerable: enumerable,
writable: true,
configurable: true
})
}
function defineReactive(target, key, val) {
const dep = new Dep()
Object.defineProperty(target, key, {
enumerable: true, // 是否可以被枚举
configurable: true,
get() {
// 这里引入一个全局变量用于保存当前的订阅者
if (Deptarget) {
dep.addSub(Deptarget) // 将当前的订阅者加入dep.subs列表
}
return val
}, // getter
set(newVal) {
if (newVal === val) return;
val = newVal
dep.notify() // 通知dep.subs列表中所有的订阅者
}, // setter
})
}
class Dep {
constructor() {
this.id = uid++ // dep实例的唯一标识
this.subs = [] // 订阅者列表,保存订阅者对象
}
addSub(sub) { // 添加订阅者
this.subs.push(sub)
}
removeSub(sub) { // 移除订阅者
remove(sub, this.subs)
}
notify() { // 通知订阅者
for (let i = 0; i< this.subs.length; i++) {
this.subs[i].update()
}
}
}
class Observer {
constructor(value) {
this.value = value
def(value, '__ob__', this) // 在value上面添加一个'__ob__'属性避免重复Observer,值指向this
this.walk(value)
}
walk(value) {
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
defineReactive(value, keys[i], value[keys[i]])
}
}
}
class Watcher {
constructor(getter) {
this.id = uid++
this.getter = getter
this.value = this.get()
}
get() { // 首次执行逻辑触发依赖收集
window.Deptarget = this // 将当前Watcher赋值给全局变量,标识当前依赖收集的订阅者为this
this.value = this.getter()
window.Deptarget = null // 依赖收集完成后置空
}
update() { // 实现dep要求的update方法
this.run()
}
run() { // 订阅者在依赖项变更所需要做的动作
this.value = this.get()
}
}
现在来测试一下
var obj = {name: 'jack'}
function getter() {
console.log(obj.name)
}
var ob = new Observer(obj)
// 订阅者执行
new Watcher(getter)
// 更新obj.name
obj.name = 'tom'
结果很不幸,上面的代码测试结果将是一直打印'tom',也就是说getter一直被执行,我们来找一下问题:
- 首先Observer(obj)响应化obj
- 将getter传入Watcher创建订阅者实例
- Watcher实例化过程中将执行传入的getter
- getter中发生对obj.name的引用触发对应的属性getter
- 属性getter执行dep.addSub(window.Deptarget)将watcher加入dep.subs列表
- 重新赋值obj.name触发对应的属性setter,执行dep.notify()通知订阅者update
- 执行watcher.update,从而执行getter(),将触发依赖收集即步骤3 问题出在步骤7,即更新对象obj后触发订阅者更新又一次触发依赖收集从而重复得将当前观察者添加到dep.subs列表中,而1-7步骤是同步执行的,所以dep.subs一直在增长永远无法遍历完成
找到了问题根源,接下来就是解决问题
解决前文响应化依赖收集变更通知的缺陷
经过前一小节后面的分析,定位到问题在依赖收集上面,确切得说是在对象属性更新后订阅者update的第二次依赖收集上面。其实应该等到getter真正执行完毕之后才真正将订阅者加入dep.subs中。 同时前面的代码我们实际上只是简单得把订阅者添加到dep.subs列表中,设想一下如果对象更新触发订阅者update后订阅者不再依赖该属性了怎么办,我们没有考虑,也就是说对于订阅者来说dep是不可见的,这是不合适的,我们进行改进:
class Dep {
constructor() {
this.id = uid++ // dep实例的唯一标识
this.subs = [] // 订阅者列表,保存订阅者对象
}
addSub(sub) { // 添加订阅者
this.subs.push(sub)
}
removeSub(sub) { // 移除订阅者
remove(sub, this.subs)
}
notify() { // 通知订阅者
for (let i = 0; i< this.subs.length; i++) {
this.subs[i].update()
}
}
// 新增depend方法用于依赖收集
depend() {
if (window.Deptarget) {
window.Deptarget.addDep(this) // 将当前dep传给订阅者,让订阅者决定是否需要依赖该dep
}
}
}
function defineReactive(target, key, val) {
const dep = new Dep()
Object.defineProperty(target, key, {
enumerable: true, // 是否可以被枚举
configurable: true,
get() {
// 这里引入一个全局变量用于保存当前的订阅者
if (window.Deptarget) {
dep.depend() // 这里就不能简单得将订阅者直接添加到dep.subs列表
}
return val
}
})
}
class Watcher {
constructor(getter) {
this.id = uid++
this.getter = getter
// 扩展几个变量用于存储dep
this.newDeps = [] // 存储新一轮的dep
this.newDepIds = new Set()
this.deps = [] // 存储老一轮的dep
this.depIds = new Set()
this.value = this.get()
}
get() { // 首次执行逻辑触发依赖收集
window.Deptarget = this // 将当前Watcher赋值给全局变量,标识当前依赖收集的订阅者为this
this.value = this.getter()
window.Deptarget = null // 依赖收集完成后置空
this.cleanUpDeps()
}
// 新增cleanUpDeps方法,用于在getter执行完毕之后对依赖进行一次更新整理,解除不再依赖的dep
cleanUpDeps() {
// 清除不再依赖的dep,将newDeps赋值给deps,清空newDeps
const i = this.deps.length
while(i--) {
if (!this.newDepIds.has(this.deps[i].id)) {
this.deps[i].removeSub(this)
}
}
let temp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = temp
this.newDepIds.clear()
temp = this.deps
this.deps = this.newDeps
this.newDeps = temp
this.newDeps.length = 0
}
// 新增addDep方法,进行依赖收集
addDep(dep) {
// 如果新依赖列表不包含dep,则将其加入新dep列表
if (!newDepIds.has(dep.id)) {
this.newDepIds.add(dep.id)
this.newDeps.push(dep)
// 如果老dep列表中也不包含dep,则立即将当前订阅者加入dep.subs列表中
if (!this.depIds.has(dep.id)) {
dep.addSub(this)
}
}
}
...
}
完整代码
let uid = 0
function remove(target, list) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
function def(target, key, val, enumerable = true) {
Object.defineProperty(target, key, {
value: val,
enumerable: enumerable,
writable: true,
configurable: true
})
}
function defineReactive(target, key, val) {
const dep = new Dep()
Object.defineProperty(target, key, {
enumerable: true, // 是否可以被枚举
configurable: true,
get() {
// 这里引入一个全局变量用于保存当前的订阅者
if (Deptarget) {
dep.depend() // 将当前的订阅者加入dep.subs列表
}
return val
}, // getter
set(newVal) {
if (newVal === val) return;
val = newVal
dep.notify() // 通知dep.subs列表中所有的订阅者
}, // setter
})
}
class Dep {
constructor() {
this.id = uid++ // dep实例的唯一标识
this.subs = [] // 订阅者列表,保存订阅者对象
}
addSub(sub) { // 添加订阅者
this.subs.push(sub)
}
removeSub(sub) { // 移除订阅者
remove(sub, this.subs)
}
notify() { // 通知订阅者
for (let i = 0; i< this.subs.length; i++) {
this.subs[i].update()
}
}
depend() {
if (window.Deptarget) {
window.Deptarget.addDep(this) // 将当前dep传给订阅者,让订阅者决定是否需要依赖该dep
}
}
}
class Observer {
constructor(value) {
this.value = value
def(value, '__ob__', this)
this.walk(value)
}
walk(value) {
const keys = Object.keys(value)
for (let i = 0; i < keys.length; i++) {
defineReactive(value, keys[i], value[keys[i]])
}
}
}
class Watcher {
constructor(getter) {
this.id = uid++
this.getter = getter
this.newDeps = [] // 存储新一轮的dep
this.newDepIds = new Set()
this.deps = [] // 存储老一轮的dep
this.depIds = new Set()
this.value = this.get()
}
get() {
window.Deptarget = this
this.value = this.getter()
window.Deptarget = null
this.cleanUpDeps()
}
update() {
this.run()
}
run() {
this.get()
}
cleanUpDeps() {
// 清除不再依赖的dep,将newDeps赋值给deps,清空newDeps
let i = this.deps.length
while(i--) {
if (!this.newDepIds.has(this.deps[i].id)) {
this.deps[i].removeSub(this)
}
}
let temp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = temp
this.newDepIds.clear()
temp = this.deps
this.deps = this.newDeps
this.newDeps = temp
this.newDeps.length = 0
}
// 新增addDep方法,进行依赖收集
addDep(dep) {
// 如果新依赖列表不包含dep,则将其加入新dep列表
if (!this.newDepIds.has(dep.id)) {
this.newDepIds.add(dep.id)
this.newDeps.push(dep)
// 如果老dep列表中也不包含dep,则立即将当前订阅者加入dep.subs列表中
if (!this.depIds.has(dep.id)) {
dep.addSub(this)
}
}
}
}
测试一下:
var obj = {name: 'jack'}
function getter() {
console.log(`my name is ${obj.name}`)
}
var ob = new Observer(obj)
// 订阅者执行
new Watcher(getter)
// 更新obj.name
obj.name = 'tom' // 将输出'my name is tom'
总结
vue实现数据驱动的过程:
- 首先Observer(obj)响应化obj
- 将getter传入Watcher创建订阅者实例
- Watcher实例化过程中将执行传入的getter
- getter中发生对obj.name的引用触发对应的属性getter
- 属性getter执行dep.depend通知处于全局Deptarget的订阅者进行依赖收集
- 订阅者实现了addDep方法,携带deps和newDeps属性分别标识旧生代依赖列表和新生代依赖列表,首先将dep加入新生代依赖列表,如果旧生代依赖列表中未包含当前dep,则立即执行dep.addSub()通知将订阅者加入dep.subs
- 当getter执行完毕,结束了一轮依赖收集将执行watcher.cleanupDeps()方法将新生代依赖列表中不存在的旧生代依赖移除,同时将新生代依赖赋值给旧生代依赖,清空新生代依赖列表等待下一轮依赖收集
- 重新赋值obj.name触发对应的属性setter,执行dep.notify()通知订阅者update
- 执行watcher.update,从而执行getter(),将触发依赖收集即步骤3
遗留问题
思考:
- 如何响应化嵌套对象
- 在一轮变更中订阅者依赖的多个属性都发生了变更或者依赖的某个属性发生了多次变更怎么解决订阅者多次update的问题
- 如果在一个订阅者的getter函数中嵌套另一个订阅者,此时的依赖收集过程是怎样的