JavaScript|监听对象与响应式原理

很多前端 JavaScript 框架,包含但不限于(Angular,React,Vue)都拥有自己的响应式方法,而这些方法其实都是基于监听对象来实现的,这里我们来看看监听对象实现响应式的原理。

Object.defineProperty 监听对象

如果需要监听对象的属性变化,可以使用Object.defineProperty的存储属性描述符的方法来监听。

const obj = {
  a: 1,
  b: 2,
  c: 3
}

// 监听一个属性的变化
Object.defineProperty(obj, 'a', {
  set(val) {
    console.log('a set', val)
  },
  get() {
    console.log('a get')
    return this.a
  }
})
<!-- more -->
// 监听所有属性的变化
Object.keys(obj).forEach(key => {
    let val = obj[key]
    Object.defineProperty(obj, key, {
        get() {
            console.log(key, 'get')
            return val
        },
        set(newVal) {
            console.log(key, 'set', newVal)
            val = newVal
        },
    })
});

console.log(obj.a) // a get 1
obj.a = 2 // a set 2

但通过Object.defineProperty会带来很多问题:

  1. 如果需要监听对象的增删操作,这个方法就会失效
  2. 定义属性时,所有的普通属性都会强行变成属性描述符

存储数据描述符设计的初衷并不是为了去监听一个完整的对象。

Proxy的基本使用

在ES6中,新增了Proxy类,用于为对象创建一个代理:
如果我们希望监听一个对象的相关操作,可以先创建一个代理对象(Proxy),然后将相关操作都在代理对象上完成。

const obj = {
  a: 1,
  b: 2,
  c: 3
}

const proxy = new Proxy(obj, {
    get(target, key) {
        console.log('get', key)
        return target[key]
    },
    set(target, key, val) {
    console.log('set', key, val)
    target[key] = val
    },
    has(target, key) {
        console.log('has', key)
        return key in target
    },
    deleteProperty(target, key) {
        console.log('delete', key)
        delete target[key]
    }
})

console.log(proxy.a); // get a 1
proxy.a = 2; // set a 2
console.log('b' in proxy); // has b true
delete proxy.c // delete c

Proxy除了可以代理对象外,还能代理函数。

function foo() {
  
}

const proxy = new Proxy(foo, {
    apply(target, thisArg, argArray) {
        console.log('apply')
        return target.apply(thisArg, argArray)
    },
    construct(target, argArray) {
        console.log('construct')
        return new target(...argArray)
    },
})

proxy.apply({}, [1, 2]) // apply
new proxy('1','2') // construct

Proxy有很多捕获器方法,具体可以查阅MDN文档proxy

Reflect基本使用

Reflect也是ES6新增的一个API,它是一个对象,它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法:比如Reflect.getPrototypeOf(target)类似于 Object.getPrototypeOf(); Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty()……

为什么要有Reflect呢?早期ECMA规范中没有考虑到这种对对象本身的操作如何设计会更加规范,所以将这些API放到了Object上面。但是Object作为一个构造函数,这些操作实际上放到它身上并不合适。另外还包含一些类似于 in、delete操作符,让JS看起来是会有一些奇怪的。所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上。

比较Reflect和Object

Reflect API

// 将之前代理转换成Reflect使用
const obj = {
  a: 1,
  b: 2,
  c: 3
}

const proxy = new Proxy(obj, {
    get(target, key, receiver) {
        console.log('get', key)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, val, receiver) {
        console.log('set', key, val)
        return Reflect.set(target, key, val, receiver)
    },
    has(target, key) {
        console.log('has', key)
        return Reflect.has(target, key)
    },
    deleteProperty(target, key) {
        console.log('delete', key)
        return Reflect.deleteProperty(target, key)
    }
})

Receiver的作用

receiver的参数,它的作用是什么呢?如果我们的源对象(obj)有setter、getter的访问器属性,那么可以通过receiver来改变里面的this。

function Student(name, age) {
    this.name = name
    this.age = age
}

function Teacher() {}

const teacher = Reflect.construct(Student, ['Tom', 18], Teacher)
console.log(teacher); // Teacher { name: 'Tom', age: 18 }
console.log(teacher instanceof Teacher) // true

响应式原理

响应式的定义

响应式是指,当一个对象的属性发生变化时,会自动触发对应的方法。

变量响应式

let a = 10
// 需要执行的代码
console.log(a)
console.log(a * 2)
// 当a发生变化时上面代码需要再次执行
a = 20

对象响应式

let obj = {
    a: 1,
    b: 2,
    c: 3
}

console.log(obj.a); // obj.a 改变时需要再次执行
console.log(obj.b); // obj.b 改变时需要再次执行

响应式函数的封装

通过创建一个数组收集变量触发需要执行的方法,然后在每次变量发生变化时,遍历数组,调用所有变量的响应式方法。

let fns = []
function watchFn(fn) {
    fns.push(fn)
}

const obj = {
    a: 1,
    b: 2,
    c: 3
}

watchFn(function() {
    console.log(obj.a)
})

watchFn(function() {
    console.log(obj.b)
}
// 变量改变时触发该函数
fns.forEach(fn => fn())

依赖收集封装

上述的方法在对多个对象做监听的时候,每创建一个对象都需要创建一个数组和方法,这样会比较麻烦。可以通过将该方法封装成一个类去实现:

class Dep {
    constructor() {
        this.subs = []
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    notify() {
        this.subs.forEach(sub => sub.update())
    }
}

const obj = {
    a: 1,
    b: 2,
    c: 3
}

const dep = new Dep()

dep.addSub(function() {
    console.log(obj.a)
})

dep.addSub(function() {
    console.log(obj.b)
})

// 变量改变时触发该函数
dep.notify()

自动监听对象的变化

收集好依赖函数后,对象变化时如何触发相关依赖?我们可以通过proxy来实现:

// 沿用上个例子的代码
const proxy = new Proxy(obj, {
    get(target, key, receiver) {
        return Reflect.get(target, key, receiver)
    },
    set(target, key, val, receiver) {
        Reflect.set(target, key, val, receiver)
        dep.notify()
    }
})

proxy.a = 10 // 触发dep.notify() -> 10 2

依赖的管理

我们目前是创建了一个Depend对象,用来管理对于obj变化需要监听的响应函数:但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?

在前面关于ES6的文章中我提到过WeakMap,并且在学习WeakMap的时候我讲到了后面通过WeakMap如何管理这种响应式的数据依赖:

// 依赖封装
class Depend {
  constructor() {
      this.reactiveFns = []
  }
  addDepend(reactiveFn) {
      this.reactiveFns.push(reactiveFn)
  }
  notify() {
      this.reactiveFns.forEach(reactiveFn => reactiveFn())
  }
}

// 响应式函数封装
let activeReactFn = null
function watchFn(reactiveFn) {
  activeReactFn = reactiveFn
  reactiveFn()
  activeReactFn = null
}

// 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepend(target, key) {
  // 根据target获取map
  let map = targetMap.get(target)
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }
  // 根据target获取map中的depend对象
  let depend = map.get(key) 
  if (!depend) {
      depend = new Depend()
      map.set(key, depend)
  }
  return depend
}

// 对象案例
const obj = {
  name: '张三',
  age: 18
}

// 监听代理
const objProxy = new Proxy(obj, {
  get: function (target, key, receiver) {
      // 获取depend对象
      const depend = getDepend(target, key)
      // 添加响应函数
      depend.addDepend(activeReactFn)
      // 返回值
      return Reflect.get(target, key, receiver)
  },
  set: function (target, key, val, receiver) {
      Reflect.set(target, key, val, receiver)
      // 获取depend对象
      const depend = getDepend(target, key)
      // 响应函数
      depend.notify()
  }
})

watchFn(function() {
  console.log('React Name')
  console.log(objProxy.name)
})

watchFn(function() {
  console.log('React Age')
  console.log(objProxy.age)
})

objProxy.name = '李四' // 只会触发 React Name 李四,不会触发 React Age 18

依赖管理重构

上述的方法可以针对对象的某个变量完成响应式,但是如果响应式函数中变量被调用两次,那么这个函数会被收集两次,可以通过Set来解决这个问题,还可以将get的收集依赖方法放在Dep内部,避免混淆:

// 对上述两个方法进行改进
// 依赖封装
class Depend {
  constructor() {
        this.reactiveFns = new Set()
  }
  addDepend() {
        if (activeReactiveFn)
        this.reactiveFns.add(activeReactiveFn)
  }
  notify() {
        this.reactiveFns.forEach(reactiveFn => reactiveFn())
  }
}

// 监听代理
const objProxy = new Proxy(obj, {
  get: function (target, key, receiver) {
      // 获取depend对象
      const depend = getDepend(target, key)
      // 添加响应函数
      depend.addDepend()
      // 返回值
      return Reflect.get(target, key, receiver)
  },
  set: function (target, key, val, receiver) {
      Reflect.set(target, key, val, receiver)
      // 获取depend对象
      const depend = getDepend(target, key)
      // 响应函数
      depend.notify()
  }
})

实现Vue3/Vue2响应式

通过上面的原理,再利用闭包的特性,可以实现Vue的响应式方法:

Vue3是通过Proxy实现响应式:

// vue3 reactive
function reactive(obj) {
    return new Proxy(obj, {
        get: function (target, key, receiver) {
            // 获取depend对象
            const depend = getDepend(target, key)
            // 添加响应函数
            depend.addDepend()
            // 返回值
            return Reflect.get(target, key, receiver)
        },
        set: function (target, key, val, receiver) {
            Reflect.set(target, key, val, receiver)
            // 获取depend对象
            const depend = getDepend(target, key)
            // 响应函数
            depend.notify()
        }
    })
}

const info = reactive({
    name: '张三',
    age: 18
})

watchFn(function() {
    console.log('React Name')
    console.log(info.name)
})

info.name = '李四' // React Name 李四

Vue2是通过Object.defineProperty实现响应式:

// vue2 reactive
function reactive(obj) {
    Object.keys(obj).forEach(key => {
        let value = obj[key]
        Object.defineProperty(obj, key, {
        get: function (target, key, receiver) {
            // 获取depend对象
            const depend = getDepend(target, key)
            // 添加响应函数
            depend.addDepend()
            // 返回值
            return Reflect.get(target, key, receiver)
            },
        set: function (target, key, val, receiver) {
            Reflect.set(target, key, val, receiver)
            // 获取depend对象
            const depend = getDepend(target, key)
            // 响应函数
            depend.notify()
            }
        })
    })
    return obj
}

const info = reactive({
    name: '张三',
    age: 18
})

watchFn(function() {
    console.log('React Name')
    console.log(info.name)
})

info.name = '李四' // React Name 李四