该文章以vue2框架为主,记录学习笔记。如有错误或需要改进代码的地方,欢迎各位大佬指点,共同学习进步。
该文章源码git地址:vue源码解析: vue响应式源码解析
前言
vue的响应式数据是通过Object.defineProperty()将属性转换成getter/setter的形式来追踪变化。结合消息订阅和发布者模式,读取数据时会触发getter,修改数据时会触发setter。
- Observer:它的作用是把一个Object中的所有数据(包括子数据)都转换成响应式的
- Watcher:订阅一个数据,并读取数据内容,数据变化时执行回调函数,更新视图
- Dep:将读取的数据内容收集到dep中,数据改变时给回调函数传参
1.如何追踪变化
我们封装一个函数defineReactive函数,其作用是定义一个响应式数据,也就是说在这个函数中进行变化追踪,封装后只需要传递data,key和val就行了。每当从data的key中读取数据时,get函数被触发;每当设置data中的key属性时,set被触发。
// 监听数据变化
function defineReactive(data, key, val = data[key]) {
Object.defineProperty(data, key, {
get() {
console.log('读取数据触发get-->', val);
return val
},
set(newVal) {
if (val === newVal) return
console.log('修改数据触发set-->', newVal);
val = newVal
},
})
}
2.如何收集依赖
只对Object.defineProperty进行封装,其实没什么用,真正有用的是如何收集依赖,观察下面代码:
<template>
<h1>
{{name}}
</h1>
</template>
该模板使用了数据name,先把用到的数据name收集起来,然后当它发生变化时,要向使用了它的地方发送通知。总结起来就是在getter中收集依赖,在setter中更新依赖。代码如下:
// 创建Dep类,用来收集依赖,发布依赖(dep收集的是windows.target)
class Dep {
constructor() {
this.subs = []
}
// 添加依赖
addSubs(val) {
this.subs.push(val)
}
// 收集依赖
collect() {
// 为什么收集的是Window中target的值呢?后续Watcher类中会解释
if (Window.target) {
this.addSubs(Window.target)
}
}
// 发布依赖
notify() {
const subs = [...this.subs]
// 数据变化时循环数组,执行元素watcher实例的undata方法通知更新
subs.forEach(item => item.undata())
}
}
创建好Dep类后,如何把dep和监听的数据关联起来呢?我们改造一下之前的defineReactive函数,如下:
// 监听数据变化
function defineReactive(data, key, val = data[key]) {
let dep = new Dep() //新增
Object.defineProperty(data, key, {
get() {
// 收集
dep.collect() //新增
return val
},
set(newVal) {
if (val === newVal) return
val = newVal
// 发布
dep.notify() //新增
}
})
}
3.什么是watcher
watcher是一个中介的角色,数据发生变化时通知它,然后它在通知其他地方。
关于watcher,先看一个经典的使用方式:
vm.$watch('a.b.c', function(newVal, oldVal){
//做点什么
})
思考一下,怎们实现这个功能?好像只要把这个watcher实例添加到data.a.b.c属性的Dep中就行了。然后当data.a.b.c的发生变化时,通知watcher执行参数中的回调函数。代码如下:
// 订阅数据,把自己放在一个全局的位置(例:Window.target)
class Watcher {
constructor(vm, expOrFn, cb) {
// vm: 数据对象
// expOrFn,根据data和expression就可以获取watcher依赖的数据
// cb:依赖变化时触发的回调函数
this.vm = vm
this.expOrFn = expOrFn
this.cb = cb
this.value = this.get()
}
// 读取属性的内容
get() {
/**
* 为什么把this(Watcher实例)放在Window.target下面呢?
* 因为需要把读取的数据值放在Dep中,我们需要Watcher和Dep进行通讯
* 所以在全局对象Window下创建一个target属性,解决了一个问题
*/
Window.target = this
// 执行path方法,读取对象中属性的值后会触发getter,收集到dep中
const value = path(this.vm, this.expOrFn)
// 收集后需要对Window.target进行重置
Window.target = null
return value
}
// 当收到数据变化的消息时执行该方法,从而调用cb
undata() {
// 之前存下来旧的值
const oldVal = this.value
// 读取新的值
this.value = this.get()
// 执行回调函数,并更改this执行,让this指向对象data而不是Watcher实例
this.cb.call(this.vm, this.value, oldVal)
}
}
// 辅助函数:用来读取对象中key的值
function path(obj, path = '') {
// 分割路径,例如:'a.b.c',以data[a][b][c]的方式取值
const keys = path.split('.')
return obj ? keys.reduce((newObj, item) => newObj[item], obj) : obj
}
这段代码可以主动把自己主动添加data.a.b.c的Dep中去,我在get方法中先把window.target设置成this,也就是当前的watcher实例,然后再读取一下data.a.b.c的值,就会触发getter。
触发了getter,就会触发收集依赖的逻辑。收集依赖上面已经介绍过了,会从window.target中读取一下依赖并添加到Dep中。
依赖注入到Dep后,每当data.a.b.c的值发生变化时,就会让依赖列表中所有的依赖循环触发updata方法,也就是watcher中的updata方法。而updata方法会执行参数中的回调函数,将value和oldValue传到参数中。
4.递归侦测所有key
现在可以实现变化侦测功能了,但是前面的代码只能侦测数据中的某一个属性,我们需要把数据中的所有属性(包括子属性)都侦测,所以封装一个Observer类。代码如下:
// 监听对象的每个属性
class Observe {
constructor(data) {
this.value = data
// 数据的监听方式不太一样,当前只考虑对象的监听方式
if (!Array.isArray(data)) {
this.walk(data)
}
}
walk(data) {
// 这里和defineReactive函数新增的代码形成递归,直到子属性的值为基本数据类型
Object.keys(data).forEach(key => defineReactive(data, key))
}
}
// 改造一下之前的defineReactive函数
function defineReactive(data, key, val = data[key]) {
// 新增代码
if (typeof val === 'object' && val !== null) {
new Observe(val)
}
let dep = new Dep()
Object.defineProperty(data, key, {
get() {
// 收集
dep.collect()
return val
},
set(newVal) {
if (val === newVal) return
val = newVal
// 发布
dep.notify()
},
})
}
5.总结
根据上面封装的几个类,以下面代码obj对象的执行做一个总结。我们创建一个Observe类通过递归把obj中所有的子属性都变成响应式的。接着我们创建一个Watcher实例,并订阅属性a.b.c。watcher内部会先把自己放在全局变量Window.target下,然后读取obj.a.b.c的值,读取后会触发getter中collect方法把它收集到dep中。当后续更改obj.a.b.c的值时,会触发setter中notify方法,它会把之前收集到的依赖循环调用实例上undata方法,最后执行回调函数。
const obj = {
a: {
b: {
c: 1
}
}
}
new Observe(obj)
const watcher1 = new Watcher(obj, 'a.b.c', function (newVal, oldVal) {
console.log('订阅1-->', newVal)
console.log('订阅1-->', oldVal)
})
const watcher2 = new Watcher(obj, 'a.b.c', function (newVal, oldVal) {
console.log('订阅2-->', newVal)
console.log('订阅2-->', oldVal)
})
obj.a.b.c = 66