文章详情
vue数据响应式原理解析-对象篇
标签:
  • vue
日期:2021-5-01 18:55
摘要:vue里data中的数据只要被改变,视图就会更新,让我们来一探究竟...

要做到数据响应式,首先要对数据进行侦听,data返回的是一个对象,要侦听的就是这个对象...

那么,怎么侦听这个对象呢,其实在js里有一个方法Object.defineProperty(),这个方法可以设置对象的字段的get和set函数,如下:

const data = {}
let temp = 'hello world'
Object.defineProperty(data, 'title', { // 设置对象的属性可被枚举 enumerable: true,
get() {
console.log('data.title被获取')
return temp
},
set(value) {
console.log('data.title被设置')
temp = value
}
})

console.log(data.title)
data.title = 'good'

这段代码打印为:

这样,data.title就完成了侦听,当访问data.title时调用的是get,修改data.title时调用的是set。但由于get里是return的值,需要用到一个中转变量temp,这样很难维护,需要用闭包把它封在一起,我们稍作修改:

const data = {}

function defineReactive(value, key, val) {
Object.defineProperty(value, key, {
// 设置对象的属性可被枚举
enumerable: true,
get() {
console.log(`data.${key}被获取`)
return val
},
set(value) {
console.log(`data.${key}被设置`)
val = value
}
})
}

defineReactive(data, 'name', 'yzc')

console.log(data.name)
data.name = 'yyl'
console.log(data.name)

data里的属性很多,所以需要循环将每一个属性都转化为可侦听的,我们创建一个Observer类,专门负责干这个事情...

export default class Observer {
constructor(value) {
// 在构造器里调用walk遍历对象的每一个属性,转化为可侦听
this.walk(value)
}
walk(value) {
for (let key in value) {
defineReactive(value, key, value[key])
}
}
}

只要new Observer(data),这时整个对象的属性看似都能被侦听,但是别忘了,对象可以嵌套,我们需要一种类似递归的机制,一层一层的把所有嵌套的对象也new Observer()一下,需要创建一个函数observe,在observe里判断传入的对象属性还继续是不是一个对象,如果是普通值就什么也不做,并且Observer的构造器也要改造,在Observer的构造器内需要给每一个被转化的对象加上一个标记,vue源码里用的标记是__ob__标记,里面存的是Observer的实例,

defineReactive这个函数也要加上一些判断,判断进入的value[key]是否含有__ob__属性,如果没有,继续observe,至此这三个东西形成一个类似递归的循环引用:
observe --> Observer --> defineReactive --> observe......
直到对象的最后一层的属性是普通值,才会停止执行,实在是精妙...

方便理清思路,我们把这三个东西分成三个文件,改造后是这样的:

observe.js

import Observer from './Observer'

export default function observe(value) {
// 判断value是不是对象类型,如果不是什么也不做
if (typeof value !== 'object') {
return
}
// new Observer将对象的整层都转化为可侦听
return new Observer(value)
}

Observer.js

import defineReactive from './defineReactive'

export default class Observer {
constructor(value) {
// 给value加上__ob__属性,值为Observer的实例,带上__ob__的对象都是已经转化为可侦听的
Object.defineProperty(value, '__ob__', {
value: this
})
// 在构造器里调用walk遍历对象的每一个属性,转化为可侦听
this.walk(value)
}
walk(value) {
for (let key in value) {
defineReactive(value, key, value[key])
}
}
}

defineReactive.js

import observe from './observe'

export default function defineReactive(value, key, val) {
// 判断对象是否有__ob__, 如果没有就继续observe
if (!value[key].hasOwnProperty('__ob__')) {
observe(value[key])
}

Object.defineProperty(value, key, {
// 设置对象的属性可被枚举
enumerable: true,
get() {
console.log(`data.${key}被获取`)
return val
},
set(value) {
console.log(`data.${key}被设置`)
val = value // 新设置的值可能又是个对象,所以也要observe一下 observe(val)
}
})
}

至此,我们已经可以把无限层级的对象里的每一个属性全部变成可侦听的属性,来测试一下:

import observe from './observe'

const data = {
name: 'yzc',
age: 29,
a: {
b: {
c: 10086
}
}
}

observe(data)

console.log(data.a.b.c)
console.log(data)

可以看到,data.a.b.c已经可以被侦听了,且每一层对象都有__ob__这个不可被枚举的属性。

光有数据的侦听还是不够的,当data内的某个属性被改变了,我们已经能够知道,但是我们怎么知道要去更改视图里的哪个位置呢,只能推倒重新渲染...

但是vue里不会这么做,尤大很巧妙的设计了两个类,一个是Dep类,一个是Watcher类。

每个数据都会搜集依赖,谁用了这个数据,谁就是依赖,在数据的get里搜集依赖,在数据的set里通知所有依赖。

那么依赖到底是什么,这是个很抽象的东西,谁用了数据谁就是依赖,那么这个“谁”又是什么东西,这个“谁”其实是Watcher类的实例,每个组件实例都对应一个Watcher实例,所谓的搜集依赖,就是在搜集Watcher的实例。

怎么去搜集依赖呢,Dep类就是干这个事情的,数据被读取的时候会触发get,在get中搜集依赖,数据被修改时会触发set,在set中通知依赖更新,从而使它关联的组件重新渲染。

搜集依赖和通知依赖更新的官方文档中的流程图:

Dep.js

export default class Dep {
constructor() {
// 实例的subs是依赖保存的地方
this.subs = []
}
// 搜集依赖
depend() {
if (window.target) {
this.subs.push(window.target)
}
}
// 通知所有依赖
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}

Watcher.js

export default class Watcher {
constructor(obj, deepkey, callback) {
// 在这里读取一次要观测的值,会触发值的get
// get里会搜集依赖
// 搜集依赖会去判断window.target存不存在,存在就把window.target放入依赖数组
// 所以我们在读取值之前把Watcher的实例设置为window.target
// 在读取完值之后,把window.target设置为null
// deepkey是要读取的值的路径,比如a.b.c,就是obj.a.b.c,需要编写一个工具函数来取值
this.callback = callback
window.target = this
parseDeepkey(deepkey)(obj)
window.target = null
}
update() {
this.callback()
}
}

// 这个函数是用来取对象嵌套层级内的值,deepkey为a.b.c之类的字符串
function parseDeepkey(deepkey) {
let keys = deepkey.split('.')
return function (obj) {
keys.forEach(key => {
obj = obj[key]
})
return obj
}
}

加上Dep类后,defineReactive也要加上相关的调用:

import Dep from './dep'
import observe from './observe'

export default function defineReactive(value, key, val) {
// 判断对象是否有__ob__, 如果没有就继续observe
if (!value[key].hasOwnProperty('__ob__')) {
observe(value[key])
}
let dep = new Dep()
Object.defineProperty(value, key, {
// 设置对象的属性可被枚举
enumerable: true,
get() {
console.log(`data.${key}被获取`)
// 在这里搜集依赖
dep.depend()
return val
},
set(value) {
console.log(`data.${key}被设置`)
val = value
// 新设置的值可能又是个对象,把这个值也observe一下
observe(val)
// 在这里通知依赖
dep.notify()
}
})
}

我们来测试一下能不能watch到数据并执行回调函数:

import observe from './observe'
import Watcher from './watcher'

const data = {
name: 'yzc',
age: 29,
a: {
b: {
c: 10086
}
}
}

observe(data)

new Watcher(data, 'age', function () {
console.log('我是依赖,data.age变化了, 需要do some...')
})
new Watcher(data, 'a.b.c', function () {
console.log('我是依赖,data.a.b.c变化了, 需要do some...')
})

data.age = 30
data.a.b.c = 9999


到这里,data内的所有值都已经完成了侦听,并且还能通知给使用这个值的依赖。

但是还是有不足的地方,当我们对对象数据直接添加或删除值时,无法通知依赖,例如:

this.myObject.newProperty = 'hi'

所以vue里还很贴心的准备了Vue.set和Vue.delete两个全局API...


发送
评论(0)