响应式或数据双向绑定的基本概念,这里我就不再多说了,这里我们直接来探究其行为:直观上,数据在变化时不需要开发者去手动更新视图,视图会根据变化的数据“进行”更新。而想要完成这个过程,需要做到以下几点:
- 知道收集视图依赖了哪些数据
- 感知被依赖数据的变化
- 数据变化时,自动“通知”需要更新的视图变化,并进行更新。
这个过程对应的技术概念如下:
- 依赖收集
- 数据劫持/数据代理
- 发布/订阅模式 这里我们先来学习其中的一个技术点——数据劫持/数据代理。
感知数据变化的方法很直接, 就是进行数据劫持或数据代理. 我们往往会通过Object.defineProperty这个方法来实现:
let data = {
stage: 'GitChat',
course: {
title: '前端开发进阶',
author: 'Lucas',
publishTime: '2022-01-03'
}
}
Object.keys(data).forEach(key => {
let currentVal = data[key]
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get () {
console.log(`getting ${key} value now, getting value is: ${currentVal}`)
return currentVal
},
set (newVal) {
currentVal = newVal
console.log(`setting ${key} value now, setting value is: ${currentVal}`)
}
})
})
这段代码对data数据的getter和setter方法进行了定义拦截, 当我们读取或改变data的值时, 便可以监听到这种响应.
但是这种实现有一个问题, 例如执行以下代码后的结果并没有打印出来data.course.title的信息
data.course.title = '前端开发进阶2'
// getting course value now, getting value is: [object Object]
出现这个问题的原因是, 我们的代码只实现了一层Object.defineProperty, 或者说只对data的第一层属性执行了Object.defineProperty, 对于嵌套的引用类型数据结构data.course, 我们同样应该进行拦截.
为了达到深层拦截的目的, 可以将Object.defineProperty的逻辑抽象为observe函数, 并改用递归实现:
// tool.js
export const observe = data => {
if (!data || typeof data !== 'object') {
return
}
Object.keys(data).forEach(key => {
let currentVal = data[key]
observe(currentVal)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get () {
console.log(`getting ${key} value now, getting value is: ${currentVal}`)
return currentVal
},
set (newVal) {
currentVal = newVal
console.log(`setting ${key} value now, setting value is: ${currentVal}`)
}
})
})
}
import { observe } from './tool.js'
let data = {
stage: 'GitChat',
course: {
title: '前端开发进阶',
author: 'Lucas',
publishTime: '2022-01-03'
}
}
observe(data)
注意: 我们在代理set行为时, 并没有对newValue为复杂类型的情况再次递归调用observe(newVal)方法. 如果给data.course.title赋值一个引用类型, 则无法实现对data.couse.title数据的观察.
监听数组的变化
我们将上述数据中的某一项变为数组:
import { observe } from './tool.js'
let data = {
stage: 'GitChat',
course: {
title: '前端开发进阶',
author: ['Lucas'],
publishTime: '2022-01-03'
}
}
observe(data)
data.course.author.push('jacky')
此时,我们只监听到了对data.course和data.course.author的读取,而数组push所产生的行为并没用被拦截。这是因为Array.prototype上挂在的方法并不能触发data.course.author属性值的settter,这并不属于做赋值操作,而是调用数组的push方法。
Vue同样存在这样的问题,它的解决方法是:将数组的常用方法进行重写。
实现逻辑如下:
const arrExtend = Object.create(Array.prototype)
const arrMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
arrMethods.forEach(method => {
const oldMethod = Array.prototype[method]
const newMethod = function (...args) {
oldMethod.apply(this, args)
console.log(`${method}方法被执行了`)
}
arrExtend[method] = newMethod
})
对数组的7个原生方法进行重写,核心操作还是调用原生方法oldMethod.apply(this, args),在此之外,还可以在调用oldMethod.apply(this, args)的前后加入我们需要的任何逻辑。
Object.defineProperty和Proxy
使用Proxy重构代码:
let data = {
stage: 'GitChat',
course: {
title: '前端开发进阶',
author: ['Lucas'],
publishTime: '2022-01-03'
}
}
const observe = data => {
if (!data || Object.ptototype.toString.call(data) !== '[object Object]') {
return
}
Object.keys(data).forEach(key => {
let currentVal = data[key]
if (typeof currentVal === 'object') {
observe(currentVal)
data[key] = new Proxy(currentVal, {
set(target, property, value, receiver) { // 因为使用数组的push方法时会引起length属性的方法,所以调用push之后会触发两次set操作 保留一次即可
if (property !== 'length') {
console.log(`setting ${key} value now, setting value is: ${currentVal}`)
}
return Reflect.set(target, property, value, receiver)
}
})
} else {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get () {
console.log(`getting ${key} value now, getting value is: ${currentVal}`)
return currentVal
},
set (newVal) {
currentVal = newVal console.log(`setting ${key} value now, setting value is: ${currentVal}`)
}
})
}
})
}
observe(data)
这里对于数据键值为基本类型的情况,使用Object.defineProperty;对于键值为对象类型的情况,可以继续递归调用observe方法,并通过Proxy返回的新对象对data[key]重新赋值,这个新值的getter和setter已经被添加了代理。
Proxy实现数据代理和Object.defineProperty实现数据拦截对比:
- Object.defineProperty不能监听数组的变化,需要对数组方法进行重写
- Object.defineProperty必须遍历对象的每个属性,且需要对嵌套结构进行深层遍历
- Proxy的代理是针对整个对象的,而不是针对象的某个属性的,因此不用遍历对象的每个属性,Proxy只需要做一层代理就可以监听同级结构下的所有属性变化,对于深层结构,还是需要递归
- Proxy支持代理数组的变化
- Proxy 的第二个参数除了可以使用set和get,还可以使用13种拦截方法
- Poxy性能会被底层持续优化