-
- 响应式原理及实现
- 1.1. 何为响应式?
- 1.2. 如何区分需要响应式和非响应式
- 1.3. 依赖收集
- 1.4. 封装响应式函数
- 1.5. Proxy数据监听,自动触发
- 1.6. 依赖收集的自动管理
- 1.7. 依赖收集的时机
- 1.8. 解决依赖重复收集的问题
- 1.9. 将对象自动用Proxy代理
- 响应式原理及实现
如果我们有一个对象:
const obj = {
name: 'flten',
age: 16,
}
有一些函数使用了这个对象中的数据:
function fn1(){
console.log(obj.name)
}
function fn2(){
console.log(obj.name)
}
如何在obj对象的属性name
发生改变时,让fn1和fn2函数自动重新触发更新数据呢?
能够在数据更新时通知到所有使用到它的函数,使其重新触发,这就是响应式。
但是有些函数是使用了响应式的数据,而有些则没有,那我们如何判断呢?最简单的方式就是把所有使用到了某个数据的所有函数都保存在一个数组中,数据发生改变时直接全部遍历函数重新执行就可以了。
// 01.js
const reactiveArr = [];
const obj = {
name: 'flten',
age: 16,
}
function fn1(){
console.log(obj.name)
}
reactiveArr.push(fn1)
function fn2(){
console.log(obj.name)
}
reactiveArr.push(fn2)
reactiveArr.forEach(fn => {
fn()
})
等一下,难道每次我们都要手动push
吗?重复的操作当然是可以封装在一个函数里了
//02.js
const reactiveArr = [];
function addReactive(fn){
reactiveArr.push(fn)
}
const obj = {
name: 'flten',
age: 16,
}
function fn1(){
console.log(obj.name)
}
addReactive(fn1)
function fn2(){
console.log(obj.name)
}
addReactive(fn2)
reactiveArr.forEach(fn=>{
fn()
})
看起来还不错,但是这里我们只是收集了使用了name
属性的函数,如果我们还要收集age
属性的函数呢?这时我们就需要另外一个数组和另外一个将函数添加到数组中的方法
//03.js
const reactiveNameArr = [];
const reactiveAgeArr = [];
function addNameReactive(fn){
reactiveNameArr.push(fn)
}
function addAgeReactive(fn){
reactiveAgeArr.push(fn)
}
const obj = {
name: 'flten',
age: 16,
}
// name 属性的使用收集
function fn1(){
console.log(obj.name)
}
addNameReactive(fn1)
function fn2(){
console.log(obj.name)
}
addNameReactive(fn2)
reactiveNameArr.forEach(fn=>{
fn()
})
// age属性的使用收集
function fn3(){
console.log(obj.age)
}
addAgeReactive(fn3)
function fn4(){
console.log(obj.age)
}
addAgeReactive(fn4)
reactiveAgeArr.forEach(fn=>{
fn()
})
但是这样太麻烦了,每一个数据都要另外新建一个数组和添加函数吗?什么方式可以封装这些重复操作呢?它可以自动生成一个数组和对应的函数操作呢?当然是类了,我们可以通过定义一个类,为每个响应式数据都实例化一个对象,这样就把这些操作封装起来不需要每次都手动重建了。
我们将上面的操作封装为一个类:
//04.js
class Depend{
constructor(){
this.reactiveArr = []
}
addDepend(fn){
this.reactiveArr.push(fn)
}
notify(){
this.reactiveArr.forEach(fn=>fn())
}
}
const obj = {
name: 'flten',
age: 16,
}
const dependName = new Depend();
const dependAge = new Depend();
// name 属性的使用收集
function fn1(){
console.log(obj.name)
}
dependName.addDepend(fn1)
function fn2(){
console.log(obj.name)
}
dependName.addDepend(fn2)
dependName.notify()
// age属性的使用收集
function fn3(){
console.log(obj.age)
}
dependAge.addDepend(fn3)
function fn4(){
console.log(obj.age)
}
dependAge.addDepend(fn4)
dependAge.notify()
/*
flten
flten
16
16
*/
可是上面reactiveName.addDepend(fn1)
这样的操作也进行了很多次,这样的情况下我们将它封装为一个函数,专门将数据变为响应式
//05.js
class Depend{
constructor(){
this.reactiveArr = []
}
addDepend(fn){
this.reactiveArr.push(fn)
}
notify(){
this.reactiveArr.forEach(fn=>fn())
}
}
const obj = {
name: 'flten',
age: 16,
}
const dependName = new Depend();
const dependAge = new Depend();
function watch(depend, fn){
depend.addDepend(fn)
}
// name 属性的使用收集
function fn1(){
console.log(obj.name)
}
watch(dependName,fn1)
function fn2(){
console.log(obj.name)
}
watch(dependName,fn2)
dependName.notify()
// age属性的使用收集
function fn3(){
console.log(obj.age)
}
watch(dependAge,fn3)
function fn4(){
console.log(obj.age)
}
watch(dependAge,fn4)
dependAge.notify()
/*
flten
flten
16
16
*/
但是,数据更新以后我们每次都要手动调用notify
进行触发操作吗?这个动作可以自动执行吗?能够在数据发生变化时,去自动触发notify
?因此我们需要在数据变化时能够监听到数据的变化,什么东西可以实现数据监听呢?答案是 Proxy
数据代理。
// 06.js
class Depend{
constructor(){
this.reactiveArr = []
}
addDepend(fn){
this.reactiveArr.push(fn)
}
notify(){
this.reactiveArr.forEach(fn=>fn())
}
}
const obj = {
name: 'flten',
age: 16,
}
const dependName = new Depend();
const dependAge = new Depend();
// 将使用到监听数据的函数包裹为响应式
function watch(dependData, fn){
dependData.addDepend(fn)
}
// 创建代理对象进行数据监听
const proxy = new Proxy(obj, {
get: function(target, key, receiver){
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver){
Reflect.set(target, key, newValue, receiver)
if(Object.is(key, 'name')){
console.log(`属性${key}发生了变化,值变为${newValue}`)
dependName.notify
}
if(Object.is(key, 'age')){
console.log(`属性${key}发生了变化,值变为${newValue}`)
dependAge.notify
}
}
})
// name 属性的使用收集
function fn1(){
console.log(proxy.name)
}
watch(dependName,fn1)
function fn2(){
console.log(proxy.name)
}
watch(dependName,fn1)
// age 属性的使用收集
function fn3(){
console.log(proxy.age)
}
watch(dependAge,fn1)
function fn4(){
console.log(proxy.age)
}
watch(dependAge,fn1)
// age数据更新
proxy.age = 16
proxy.name = 'fltenwall'
proxy.age = 17
proxy.name = 'yj'
/*
属性age发生了变化,值变为16
属性name发生了变化,值变为fltenwall
属性age发生了变化,值变为17
属性name发生了变化,值变为yj
*/
我们看上面的代码,我们并不知道当前触发的是哪个数据,以及我们应该触发哪一个数据的notify
方法,即我们不知道每一个数据对应的depend
是什么,我们需要手动去判断,然后手动维护这种对应关系。
而且实际上我们开发中会用到多个对象,而多个对象又有多个属性,按照上面的 方案的话,我们需要手动为每一个对象的每一个属性都去手动实例化,让每一属性都对应一个depend对象,即数据依赖的数组及操作方法,可以这样实在太不方便了,我们可以用一个数据结构来保存这样所有的对应关系吗?即保存每一个属性和它对应的depend对象的关系?当然我们想到了映射,js中最好的描述映射关系的结果就是Map
。不过不同的对象可能存在相同名称的属性,因此我们需要为每一个对象创建一个Map
映射来关系其每一个属性和对应的depend对象关系。
这样就存在了多个Map
,但我们又如何管理每个对象和每个Map
的映射关系呢?我们可以使用Weakmap
。因此我们的目标是实现如下结构:
const obj1 = {name:'flten1',age:16}
const obj2 = {name:'flten2',age:26}
//假设 obj1NameDepend是obj1的name属性对应的depend对象
// map1保存obj1对象的所有属性和其depend对象的对应关系
const map1 = new Map()
map1.set('name', obj1NameDepend)
map1.set('age', obj1AgeDepend)
// map2保存obj2对象的所有属性和其depend对象的对应关系
const map2 = new Map()
map1.set('name', obj2NameDepend)
map1.set('age', obj2AgeDepend)
// 通过objMap保存每个对象和map的对应关系
const objMap = new Weakmap()
objMap.set(obj1, map1)
objMap.set(obj2, map2)
// 获取到每个对象的某个属性所对应的depend对象
objMap.get(obj1).get(name)
objMap.get(obj2).get(age)
按照上面的分析,我们可以封装一个getDepend
函数:
const objMap = new WeakMap()
// obj是对象,key是obj的属性
function getDepend(obj, key){
// 取出obj对应的map,即取出obj每个属性和其对应的depend的映射表
let map = objMap.get(obj)
// 如果还没有对象obj对应的map映射表,则创建映射表
// 并将其存入objMap
if(!map){
map = new Map()
objMap.set(obj, map)
}
// 从映射表中取出obj对象的key属性所对应的depend对象
let depend = map.get(key)
// 同样如果还没有key属性对应的depend对象,则创建depend对象
// 并将key与depend的对应关系存入映射表map
if(!depend){
depend = new Depend()
map.set(key, depend)
}
return depend
}
但上面的getDepend
函数应该在哪来执行呢?即我们在哪里能够知道数据被使用(访问或修改)了呢?因为我们使用了proxy代理,因此使用数据的操作会被proxy拦截监听,因此我们能够知道哪些数据被使用,能够得到正在使用的对象及其属性,因此在这里能够执行getDepend
,获取或新建属性对应的depend
对象,并将其添加到映射表map
里。
但又如何知道是哪个函数正在操作该属性呢?因为我们需要将该函数添加到depend
对象的依赖数组中,因此我们必须知道目前是哪个对象正在试图操作该属性。我们可以用一个全局变量来跟踪正在操作该属性的函数,这样我们就可以在proxy
的get
和set
监听中获取到该对象,并将该对象添加到depend
对象的依赖数组中。
后续每一次对某对象的某个属性的操作,都会被监听到,并且能够从map
取出对应的depend
对象,并且可以遍历执行所有已经添加到依赖数组中的函数,即发布更新数据的通知。
代码如下:
activeReactiveFn全局变量跟踪正在操作该属性的函数,因为我们可以用proxy知道目前正在操作的对象和属性,因此watch函数可以不传入对象作为第一个参数来区分不同对象
let activeReactiveFn = null
// 将使用到监听数据的函数包裹为响应式
function watch(fn){
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
在proxy中收集依赖和触发更新操作
const proxy = new Proxy(obj, {
get: function(target, key, receiver){
const depend = getDepend(target, key)
depend.addDepend()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver){
Reflect.set(target, key, newValue, receiver)
const depend = getDepend(target, key)
depend.notify()
}
})
而 Depend
类的addDepend
方法则需要判断activeReactiveFn
是否为null
addDepend(){
// 触发set操作时,依赖函数被执行
// 如果依赖函数有获取值的操作,那么就会同时触发get
// 而此时activeReactiveFn被重置为了null
// 因此需要判断activeReactiveFn是否为null来决定是否将其添加到依赖数组
if(activeReactiveFn){this.reactiveArr.push(activeReactiveFn)}
}
目前的整体代码为:
//07.js
class Depend{
constructor(){
this.reactiveArr = []
}
addDepend(){
if(activeReactiveFn){this.reactiveArr.push(activeReactiveFn)}
}
notify(){
this.reactiveArr.forEach(fn=>fn())
}
}
const obj = {
name: 'flten',
age: 16,
}
let activeReactiveFn = null
// 将使用到监听数据的函数包裹为响应式
function watch(fn){
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
const objMap = new WeakMap()
// obj是对象,key是obj的属性
function getDepend(obj, key){
// 取出obj对应的map,即取出obj每个属性和其对应的depend的映射表
let map = objMap.get(obj)
// 如果还没有对象obj对应的map映射表,则创建映射表
// 并将其存入objMap
if(!map){
map = new Map()
objMap.set(obj, map)
}
// 从映射表中取出obj对象的key属性所对应的depend对象
let depend = map.get(key)
// 同样如果还没有key属性对应的depend对象,则创建depend对象
// 并将key与depend的对应关系存入映射表map
if(!depend){
depend = new Depend()
map.set(key, depend)
}
return depend
}
// 创建代理对象进行数据监听
const proxy = new Proxy(obj, {
get: function(target, key, receiver){
const depend = getDepend(target, key)
// 触发set操作时,依赖函数被执行
// 如果依赖函数有获取值的操作,那么就会同时触发get
// 而此时activeReactiveFn被重置为了null
// 因此需要判断activeReactiveFn是否为null来决定是否将其添加到依赖数组
depend.addDepend()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver){
Reflect.set(target, key, newValue, receiver)
const depend = getDepend(target, key)
depend.notify()
}
})
// name 属性的使用收集
function fn1(){
console.log(proxy.age)
}
watch(fn1)
// 匿名函数
watch(function(){console.log(proxy.age)})
watch(function(){console.log(proxy.name)})
// age数据更新
proxy.age = 16
proxy.name = 'fltenwall'
proxy.age = 17
proxy.name = 'yj'
同一个函数可能会被添加到一个属性的依赖数组中多次
watch(function(){
console.log(proxy.age)
console.log(proxy.age)
})
例如在上面代码这种情况下,age
属性的依赖收集数组会将匿名函数添加进去两次,但实际我们只需要收集一次就可以了,因此我们要去重,而js里的Set()
可以帮助我们直接解决这个问题。
直接将依赖收集数组改为Set
类型即可。
this.reactiveArr = new Set()
在上面的代码中我们是手动对obj
对象进行了proxy
的代理监听,但是如果我们有多个对象,就需要对每一个都进行手动的代理监听,这显然是不合适的。
const obj1 = {name:'flten'},
const obj2 = {name:'flten2'},
const obj3 = {name:'flten3'},
const proxy1 = new Proxy(obj1, {...})
const proxy2 = new Proxy(obj2, {...})
const proxy3 = new Proxy(obj3, {...})
这样重复的创建过程我们可以将其封装为一个reactive
函数,将每一个对象都用这个函数进行包装,用proxy进行代理。
function reactive(obj){
return new Proxy(obj, {
get: function(target, key, receiver){
const depend = getDepend(target, key)
depend.depend()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver){
Reflect.set(target, key, newValue, receiver)
const depend = getDepend(target, key)
depend.notify()
}
})
}
const obj1 = {name:'flten'}
const obj2 = {name:'flten2'}
const obj3 = {name:'flten3'}
const proxy1 = reactive(obj1)
const proxy2 = reactive(obj2)
const proxy3 = reactive(obj3)
完整代码如下:
//08.js
class Depend{
constructor(){
this.reactiveArr = new Set()
}
depend() {
if (activeReactiveFn) {
this.reactiveArr.add(activeReactiveFn)
}
}
notify(){
this.reactiveArr.forEach(fn=>fn())
}
}
let activeReactiveFn = null
// 将使用到监听数据的函数包裹为响应式
function watch(fn){
activeReactiveFn = fn
fn()
activeReactiveFn = null
}
const objMap = new WeakMap()
// obj是对象,key是obj的属性
function getDepend(obj, key){
// 取出obj对应的map,即取出obj每个属性和其对应的depend的映射表
let map = objMap.get(obj)
// 如果还没有对象obj对应的map映射表,则创建映射表
// 并将其存入objMap
if(!map){
map = new Map()
objMap.set(obj, map)
}
// 从映射表中取出obj对象的key属性所对应的depend对象
let depend = map.get(key)
// 同样如果还没有key属性对应的depend对象,则创建depend对象
// 并将key与depend的对应关系存入映射表map
if(!depend){
depend = new Depend()
map.set(key, depend)
}
return depend
}
// 创建代理对象进行数据监听
function reactive(obj){
return new Proxy(obj, {
get: function(target, key, receiver){
const depend = getDepend(target, key)
depend.depend()
return Reflect.get(target, key, receiver)
},
set: function(target, key, newValue, receiver){
Reflect.set(target, key, newValue, receiver)
const depend = getDepend(target, key)
depend.notify()
}
})
}
const obj1 = {age:16}
const obj2 = {age:17}
const obj3 = {age:18}
const proxy1 = reactive(obj1)
const proxy2 = reactive(obj2)
const proxy3 = reactive(obj3)
watch(function(){console.log(proxy1.age)})
watch(function(){console.log(proxy2.age)})
watch(function(){console.log(proxy3.age)})
// age数据更新
proxy1.age = 20
proxy1.age = 21
proxy1.age = 22
/*
16
17
18
20
21
22
*/