defineReactive
为什么
为什么要定义一个defineReactive函数?原因是JS原生的defineProperty不好用。
如下代码所示:直接对obj.a赋值是没有意义的,因为getter一直返回的是7。
let obj = {}
Object.defineProperty(obj, 'a', {
get() {
console.log('你试图访问obj的a属性');
return 7 //等于不管怎么样,得到的obj.a一直是7
},
set(n) {
console.log('你试图改变obj的a属性', n);
}
})
console.log(obj.a);
obj.a = 9 //赋值无效,因为getter一直返回的是7
console.log(obj.a);
为了让obj.a=9生效,需要一个临时变量周转getter和setter
let obj = {}
let temp
Object.defineProperty(obj, 'a', {
get() {
console.log('你试图访问obj的a属性');
return temp //等于不管怎么样,得到的obj.a一直是7
},
set(n) {
console.log('你试图改变obj的a属性', n);
temp = n
}
})
console.log(obj.a);
obj.a = 9 //赋值无效,因为getter一直返回的是7
console.log(obj.a);
为了使代码更加简洁清晰、模块化,将上述代码封装为一个闭包环境defineReactive,这样可以不用单独设置一个temp临时变量,闭包中的val变量可以起到同样的效果,因为闭包中访问到父级函数的变量不会被销毁。
关于闭包的知识需要参考此文:juejin.cn/post/739870…
let obj = {}
function defineReactive(data, key, val) {
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 可配置
configurable: true,
// getter
get() {
console.log('你试图访问obj的' + key + '属性', val);
return val
},
// setter
set(newValue) {
console.log('你改变obj的' + key + '属性', newValue);
if (val === newValue) return
val = newValue
}
})
}
defineReactive(obj, 'a', 7)
obj.a = 9
console.log(obj.a);
defineReactive(obj, 'b', 11)
obj.b = 33
console.log(obj.b);
递归侦测对象全部属性
上一节写的defineReactive函数有缺陷,它没有识别.语法的能力,即不会遍历侦测对象属性
// 目前没有识别.语法的能力,即不会遍历侦测对象属性
defineRactive(obj, 'a.m.n', 5)
console.log(obj.a.m.n);
所以这一节,我们就需要解决这个问题
首先,先将defineReactive函数抽离为一个文件
defineReactive.js
export default function defineReactive(data, key, val) {
if (arguments.length == 2) {
val = data[key]
}
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 可配置
configurable: true,
// getter
get() {
console.log('你试图访问obj的' + key + '属性', val);
return val
},
// setter
set(newValue) {
console.log('你改变obj的' + key + '属性', newValue);
if (val === newValue) return
val = newValue
}
})
}
main.js使用
import defineReactive from './defineReactive'
let obj = {}
// 目前没有识别.语法的能力,即不会遍历侦测对象属性
defineReactive(obj, 'a.m.n', 5)
console.log(obj.a.m.n);
同时新增一个observer类,这个类的作用是将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的object)
新建一个Observer.js文件
export default class Observer {
constructor() {
}
}
然后在main.js引入,并且创建一个新函数observe,注意函数的名字没有r,这个函数起到辅助判别的作用
import defineReactive from './defineReactive'
import Observer from './Observer';
let obj = {}
// 创建Observe函数,注意函数的名字没有r,这个函数起到辅助判别的作用
function observe(value) {
// 如果value不是对象什么都不做,这个函数只为对象服务
if (typeof value !== 'object') return
// 定义ob
let ob
if (typeof value.__ob__ !== 'undefined') {
// __ob__ 用来存储一个Observer实例
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
目前我已经很混乱了,上面这些是什么鬼东西!!!为了帮助理解,总的来说,递归侦测对象的流程图是这个
按照这个流程图我们继续开发
Observer.js
import { def } from './utils.js'
export default class Observer {
constructor(value) {
console.log('我是Observer构造器', value);
// 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)
// 加了__ob__属性,值是这次new的实例
// __ob__属性是一个不可枚举属性,因为我们不希望遍历这个对象的时候可以遍历到这个__ob__
def(value, '__ob__', this, false)
console.log('我是Observer构造器', value);
}
}
注意上面的代码:
__ob__属性是一个不可枚举属性,因为我们不希望遍历这个对象的时候可以遍历到这个__ob__
为了方便定义__ob__属性,创建了一个def函数
utils.js
export const def = function (obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
}
完成了上述准备工作,开始整体完成递归侦测对象属性,总而言之,实现递归侦测对象属性的递归策略是:
- observe(obj)将一个对象作为参数传入observe方法
- 检测这个obj有没有__ob__属性,如果没有需要
new Observer一个Observer实例,添加到这个obj的__ob__属性上. - 在Observer构造函数会执行这个Observer类的walk方法,在这个walk方法中遍历obj的属性,然后使用defineReactive函数将这些属性全部转化为响应式属性
- 并且在defineReactive方法中也会observe每个属性,从而实现递归、深层次将对象的属性都转换为响应式的。
具体代码:
observe.js
import Observer from './Observer'
// 创建Observe函数,注意函数的名字没有r,这个函数起到辅助判别的作用
export default function observe(value) {
// 如果value不是对象什么都不做,这个函数只为对象服务
if (typeof value !== 'object') return
// 定义ob
let ob
if (typeof value.__ob__ !== 'undefined') {
// __ob__ 用来存储一个Observer实例
ob = value.__ob__
} else {
ob = new Observer(value)
}
return ob
}
utils.js
export const def = function (obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
}
Observer.js
import { def } from './utils.js'
import defineReactive from './defineReactive.js'
export default class Observer {
constructor(value) {
// 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)
// 加了__ob__属性,值是这次new的实例
// __ob__属性是一个不可枚举属性,因为我们不希望遍历这个对象的时候可以遍历到这个__ob__
def(value, '__ob__', this, false)
console.log('我是Observer构造器', value);
// 不要忘记初心,Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
this.walk(value)
}
// 遍历
walk(value) {
console.log('walk---value', value);
for (let key in value) {
defineReactive(value, key)
}
}
}
defineReactive.js
import observe from './observe'
export default function defineReactive(data, key, val) {
console.log('我是defineReactive', key);
if (arguments.length == 2) {
val = data[key]
}
// 子元素要进行observe,至此形成了递归,这个递归不是函数自己调用自己,而是多个函数、类循环调用
let childOb = observe(val)
console.log('childOb', childOb);
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 可配置
configurable: true,
// getter
get() {
console.log('你试图访问' + key + '属性', val);
return val
},
// setter
set(newValue) {
console.log('你改变' + key + '属性', newValue);
if (val === newValue) return
val = newValue
// 当设置了新值,这个新值也需要转换为响应式的
childOb = observe(newValue)
}
})
}
main.js
import observe from './observe';
let obj = {
a: {
m: {
n: 5
}
},
b: 10,
c: {
d: {
e: {
f: 22
}
}
}
}
observe(obj)
数组响应式原理
目前的代码依然不完善,当obj有一个属性是数组时,你会发现defineReactive的get可以触发,但是set不会触发。
为什么不完善
对于数组,直接使用 Object.defineProperty 是不够的,原因如下:
- 数组的索引和
length属性:数组的索引和length属性是特殊的,不能通过Object.defineProperty来劫持。例如,直接修改arr[0]或arr.length不会触发 getter 和 setter。 - 数组方法的原生行为:数组的原生方法(如
push、pop等)在修改数组时不会触发依赖更新。这是因为这些方法内部并没有调用属性的 getter 和 setter。
如何实现数组响应式原理
改写数组的七个方法
要想改写这七个方法,首先要对JS原型有特别清醒的认知。
首先,这七个方法都挂载在Array.prototype上
大致操作流程图如下:
把数组的原型指向arrayMethods
array.js
import { def } from './utils'
//得到Array.prototype
const arrayPrototype = Array.prototype
// 以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrayPrototype)
const methodsNeedChange = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsNeedChange.forEach(methodName => {
// 备份原来的方法,因为push等7个函数的功能不能被剥夺
const original = arrayPrototype[methodName]
// 定义新的方法
def(arrayMethods, methodName, function () {
// 恢复原来的功能
original.apply(this, arguments)
}, false)
})
然后observer构造函数增加对value值是否是数组的判断,是的话就将value,也就是数组的原型指向我们自己造的arrayMethods
// 检查它是数组还是对象
if (Array.isArray(value)) {
// 如果是数组,要非常强行的将这个数组的原型指向arrayMethods
Object.setPrototypeOf(value, arrayMethods)
} else {
this.walk(value)
}
完整代码:
Observer.js
import { def } from './utils.js'
import defineReactive from './defineReactive.js'
import { arrayMethods } from './array.js'
console.log('arrayMethods', arrayMethods);
export default class Observer {
constructor(value) {
// 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)
// 加了__ob__属性,值是这次new的实例
// __ob__属性是一个不可枚举属性,因为我们不希望遍历这个对象的时候可以遍历到这个__ob__
def(value, '__ob__', this, false)
console.log('我是Observer构造器', value);
// 不要忘记初心,Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
// 检查它是数组还是对象
if (Array.isArray(value)) {
// 如果是数组,要非常强行的将这个数组的原型指向arrayMethods
Object.setPrototypeOf(value, arrayMethods)
} else {
this.walk(value)
}
}
// 遍历
walk(value) {
for (let key in value) {
defineReactive(value, key)
}
}
}
让数组变得observe(可侦测的)
接下来我们要想办法让这个数组变得observe,首先我们需要对这个数组进行特殊的遍历
import { def } from './utils.js'
import defineReactive from './defineReactive.js'
import { arrayMethods } from './array.js'
import observe from './observe.js'
export default class Observer {
constructor(value)
def(value, '__ob__', this, false)
if (Array.isArray(value)) {
Object.setPrototypeOf(value, arrayMethods)
// 让这个数组变得observe
this.observeArray(value)
} else {
this.walk(value)
}
}
// 遍历
walk(value) {
for (let key in value) {
defineReactive(value, key)
}
}
// 数组的特殊遍历
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
// 逐项进行observe
observe(arr[i])
}
}
}
因为splice、push、unshift这三个方法比较特殊,会往数组内部插入项,但是新插入的项也得让它observe。为此我们需要在array.js方法也使用我们新增的observeArray方法
import { def } from './utils'
//得到Array.prototype
const arrayPrototype = Array.prototype
// 以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrayPrototype)
const methodsNeedChange = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsNeedChange.forEach(methodName => {
// 备份原来的方法,因为push等7个函数的功能不能被剥夺
const original = arrayPrototype[methodName]
// 定义新的方法
def(arrayMethods, methodName, function () {
// 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么被添加了?因为数组肯定不是最高层,第一次遍历obj对象的第一层的时候,已经给这个数组属性添加了__ob__属性
const ob = this.__ob__
// 把类数组对象变为数组
const args = [...arguments]
// 有三种方法push/unshift/splice能够插入新项,现在要把插入的新项也要变为observe的
let inserted = []
switch (methodName) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
// splice格式是splice(下标,数量,插入的新项)
inserted = args.slice(2)
break
}
// 判断有没有要插入的新项,让新项也变为响应的
if (inserted) {
ob.observeArray(inserted)
}
// 恢复原来的功能
const result = original.apply(this, arguments)
return result
}, false)
})
这样数组响应式就基本完成了,但是目前还不完善,是个低配版,只能通过调用那7个数组方法才能变为响应式。通过数组下标array[2]=1这样修改还不行
依赖收集
什么是依赖
用到数据的地方就是依赖,比如vue2组件的data属性就是依赖。
在getter中收集依赖,在setter中触发依赖
Dep、Watcher、Observer、数据属性之间的关系
响应式原理示意图如下:
结合代码辅以理解Watcher和Dep是什么:
<template>
<div>
<p>{{ message }}</p>
<p>{{ computedMessage }}</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello, Vue!'
};
},
computed: {
computedMessage() {
return this.message + ' (Computed)';
}
}
};
</script>
在这个例子中:
-
message属性:message属性有一个唯一的Dep实例与之关联。
-
Watcher 实例:
- 模板中的
{{ message }}会创建一个Watcher实例来监听message属性的变化。 - 计算属性
computedMessage也会创建一个Watcher实例来监听message属性的变化。 - 因此,
message属性的Dep实例会包含两个Watcher实例:一个用于模板中的{{ message }},另一个用于计算属性computedMessage。
- 模板中的
总结
- 每个数据属性有一个
Dep实例:负责收集所有依赖于该数据属性的Watcher实例。 - 每个依赖关系有一个
Watcher实例:负责监听数据属性的变化并执行相应的更新逻辑。 - 一个数据属性可以有多个
Watcher实例:因为多个组件或计算属性可能依赖于同一个数据属性。
这种设计使得 Vue 的响应式系统能够高效地管理和更新视图,确保数据变化时能够准确地触发相关的更新操作。
所以我们得出了结论,也就是数据属性、watcher、observer、dep这四者之间的关系:
Observer使得数据对象中的每个属性都能被监听,当这些属性被访问或修改时,可以触发相应的行为。Dep收集所有依赖于特定属性的Watcher,这样当属性发生变化时,Dep可以通知所有相关的Watcher。Watcher监听数据的变化,并在变化时执行相应的更新逻辑,比如更新 DOM 或重新渲染组件。
注意事项:
- 依赖就是Watcher,只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。
- Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有Watcher都通知一遍
- 代码实现的巧妙之处:
Watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter。在getter中就能得到当前正在读取数据的Watcher,并把这个Watcher收集到Dep中。
按照上文的依赖收集逻辑,先实例化Dep
新增Dep.js
export default class Dep {
constructor() {
console.log('我是Dep类的构造器');
}
notify() {
console.log('我是notify方法');
}
}
新增Watcher.js
export default class Watcher {
constructor() {
console.log('我是Watcher类的构造器');
}
}
给Observer类的构造函数添加Dep属性
defineReactive.js
setter中触发依赖
为什么Observer与defineReactive都实例化了一个dep
-
Observer类的作用:Observer类的主要职责是将一个对象的所有属性变成响应式的。它会遍历对象的所有属性,并调用defineReactive方法来定义这些属性的 getter 和 setter。Observer类中的dep实例通常用于管理整个对象的依赖,但它并不是直接用来管理具体属性的依赖。
-
defineReactive的作用:defineReactive方法用于将对象的单个属性变成响应式的。它会在每个属性上定义 getter 和 setter,并为每个属性创建一个独立的Dep实例来管理该属性的依赖。defineReactive确保每个属性都有自己的依赖收集和通知机制,从而实现细粒度的依赖管理。
比如在这个例子中
import observe from './observe';
let obj = {
a: {
m: {
n: 5
}
},
b: 10,
c: {
d: {
e: {
f: 22
}
}
},
g: [1, 2, 3, 4, 5]
}
observe(obj)
obj.a.m.n = 88
obj.g.push(100)
console.log('obj', obj);
-
obj.a.m:Observer会为obj.a.m创建一个Dep实例,管理m对象的依赖。defineReactive会为m对象的n属性创建一个独立的Dep实例,管理n属性的依赖。
-
obj.a.m.n:defineReactive会为n属性创建一个独立的Dep实例,管理n属性的依赖。
Watcher类和Dep类的具体代码
继续来处理Watcher和Dep的具体代码,目前要做的就是什么时候把Watcher放到Dep当中。
具体代码,我就不太注重研究了,因为面试的时候不可能问得那么细
Dep.js
let uid = 0
export default class Dep {
constructor() {
console.log('我是Dep类的构造器');
this.id = uid++
// 用数组存储自己的订阅者
// 这个数组里面放的是Watcher的实例
this.subs = []
}
// 添加订阅
addSub(sub) {
this.subs.push(sub)
}
// 添加依赖
depend() {
// Dep.target就是当前正在被watcher观察的watcher对象
if (Dep.target) {
// 把watcher观察者对象push到dep中
this.addSub(Dep.target)
}
}
// 通知更新
notify() {
console.log('我是notify方法');
//浅克隆一份
const subs = this.subs.slice()
// 遍历
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
watcher.js
import Dep from './Dep'
let uid = 0
export default class Watcher {
constructor(target, experssion, callback) {
console.log('我是Watcher类的构造器');
this.id = uid++
this.target = target
this.getter = parsePath(experssion)
this.callback = callback
this.value = this.get()
}
update() {
this.run()
}
get() {
// 进入依赖收集阶段。让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
Dep.target = this
const obj = this.target
let value
// 只要能找,就一直找
try {
value = this.getter(obj)
} finally {
Dep.target = null
}
return value
}
run() {
this.getAndInvoke(this.callback)
}
getAndInvoke(cb) {
const value = this.get()
if (value !== this.value || typeof value == 'object') {
const oldValue = this.value
this.value = value
cb.call(this.target, value, oldValue)
}
}
}
function parsePath(str) {
let segments = str.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
defineReactive.js
main.js使用Watcher
结语
目前我春招复习的Vue响应式原理就告一段落了,本文的知识积累足以解决面试。