数据响应式原理(JS版)

1,766 阅读7分钟

前言

在查询数据响应式原理相关资料时,发现绝大部分的都是在介绍Vue的数据响应式原理;
但是今天我想抛开Vue框架,单纯的用JS去一步步实现对象的数据响应式;
如果看完这篇文章,相信会对数据响应式有更加深刻的理解!

数据响应式的概念

在mdn中查阅了一下,没有找到相关的概念介绍;在这里我就谈谈我个人的理解。响应式这个词对于前端程序员来说并不陌生;早年间的响应式布局样式风靡一时;响应式布局就随着不同的设备尺寸变化,展示的样式也会随之改变;同理数据响应式也就是:某个数据发生变化时,能使与这数据相关的一些特定代码能自动重新执行的一种机制。

数据响应式步骤示意图

image.png 我们的数据响应式也就是实现:

  • 依赖自动收集
  • 数据变化劫持监听
  • 响应式代码随着数据变化而自动重新执行

数据劫持

先介绍一下数据劫持的方式,因为我们依赖收集也是基于数据劫持的基础上完成的;js有两种数据劫持监听的方式:

方式一:defineProperty

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
    this.address = "上海"
  }
  eat(){
    console.log(this.name + '吃东西')
  }
}
let xiaoming = new Person('xiaoming', 27)


Object.defineProperty(xiaoming,'age',{
  get:function(){
    console.log('get--------')
  },
  set: function(value) {
   console.log(value,'set---------')
  }
})
// xiaoming.eat()
xiaoming.age = 'xxxx' //xxxx set---------
xiaoming.age //get--------

方式二:Proxy

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
    this.address = "上海"
  }
  eat(){
    console.log(this.name + '吃东西')
  }
}
let xiaoming = new Person('xiaoming', 27)

const objProxy = new Proxy(xiaoming, {
  get: function(target, key, receiver) {
    console.log("get---------")
    return Reflect.get(target, key)
  },
  set: function(target, key, newValue, receiver) {
    console.log("set---------")
    // target[key] = newValue
    return Reflect.set(target, key, newValue)
  }
})
objProxy.name // get----
objProxy.name = 'xxxx' //xxxx set---------
console.log(xiaoming)

依赖收集

所谓依赖就是变化的数据和要响应式代码之间的关系;一段代码可能使用多个变量,就是使用这些变量时候建立依赖,代码依赖着变量,变量发生更改时,想办法自动根据收集的依赖关系自动通知执行,这就是今天要实现的响应式代码;所以可以先封装一个函数,来区分响应式代码和普通代码;当变量变化时,执行函数代码就可以了

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
    this.address = "上海"
  }
  eat() {
    console.log(this.name + '吃东西')
  }
}
let xiaoming = new Person('xiaoming', 27)


// 封装一个响应式的函数
let reactiveFns = []
function watchFn(fn) {
  reactiveFns.push(fn)
}

watchFn(() => {
  console.log(xiaoming.name)
  xiaoming.age += 1
  let newAddress = '我现在' + xiaoming.address
  console.log('我重新执行了')
})
//当对象属性发生变化时,执行reactiveFns里面的代码,实现响应式
xiaoming.name = '小明'
reactiveFns.forEach(fn => {
  fn()
})

目前我们的响应式是我们自己根据对象数据的变化手动调用响应式函数来执行,这显然不是我们想要的效果,所以我们可以定义一个类,它能帮我们收集依赖,并且自动执行代码

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
    this.address = "上海"
  }
  eat() {
    console.log(this.name + '吃东西')
  }
}
let xiaoming = new Person('xiaoming', 27)

// 依赖
class Depend {
  constructor() {
    this.reactiveFns = []
  }

  addDepend(reactiveFn) {
    this.reactiveFns.push(reactiveFn)
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}


// 封装一个响应式的函数
const depend = new Depend()
function watchFn(fn) {
  depend.addDepend(fn)
}

watchFn(() => {
  console.log(xiaoming.name)
  xiaoming.age += 1
  let newAddress = '我现在' + xiaoming.address
  console.log('我重新执行了')
})
//当对象属性发生变化时,执行reactiveFns里面的代码,实现响应式
xiaoming.name = '小明'
depend.notify()

现在我们能收集依赖,并且执行,但是这个过程还是我们手动调用执行,怎么让它自动呢?那么我们首先要知道数据变化了然后通知代码再执行,这就要使用到我们上面说到的数据劫持了,当数据变化时,再set当中拦截并执行响应式代码

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
    this.address = "上海"
  }
  eat() {
    console.log(this.name + '吃东西')
  }
}
let xiaoming = new Person('xiaoming', 27)

// 依赖
class Depend {
  constructor() {
    this.reactiveFns = []
  }

  addDepend(reactiveFn) {
    this.reactiveFns.push(reactiveFn)
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}


// 封装一个响应式的函数
const depend = new Depend()
function watchFn(fn) {
  depend.addDepend(fn)
}

watchFn(() => {
  console.log(xiaoming.name)
  xiaoming.age += 1
  let newAddress = '我现在' + xiaoming.address
  console.log('我重新执行了')
})
// 监听对象的属性变量
const xiaomingProxy = new Proxy(xiaoming, {
  get: function(target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    depend.notify()
  }
})
//当对象属性发生变化时,执行reactiveFns里面的代码,实现响应式
xiaomingProxy.name = '小明'

目前我们实现了响应式的部分功能,但是有些比较致命的问题

  1. 现在根本没区分是哪个属性变化引起哪些特定的代码重新执行
  2. 假设有多个响应式对象,根本没法区分特定对象的特定依赖 所以要用一个特殊的数据结构能一一存储下面这种依赖关系

image.png

解释一下上图就是用weakMap,以Oject为key,存储Object对于的map,而map中则存储该对象key和其对应响应式代码。 另外,依赖其实提前收集好,当我更改值时,直接取出对应的依赖更为合理;所以可以执行一遍代码并在拦截器get当中收集相关依赖

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
    this.address = "上海"
  }
  eat() {
    console.log(this.name + '吃东西')
  }
}
let xiaoming = new Person('xiaoming', 27)

// 依赖
class Depend {
  constructor() {
    this.reactiveFns = []
  }

  addDepend(reactiveFn) {
    this.reactiveFns.push(reactiveFn)
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}


// 封装一个响应式的函数
// 定义一个全局变量,以便当get添加依赖获取相关的响应式代码
let activeReactiveFn = null
function watchFn(fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = 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)
  }

  // 根据key获取depend对象
  let depend = map.get(key)
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}

// 监听对象的属性变量
const xiaomingProxy = new Proxy(xiaoming, {
  get: function (target, key, receiver) {
    // 根据target.key获取对应的depend
    const depend = getDepend(target, key)
    // 给depend对象中添加响应函数
    depend.addDepend(activeReactiveFn)
    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()
  }
})
//当对象属性发生变化时,执行reactiveFns里面的代码,实现响应式
watchFn(function () {
  console.log("-----这是关于name的响应式函数------")
  console.log("我是小明")
  console.log("开始执行响应式代码")
  console.log("假设我这里有很多很多代码")
  console.log(xiaomingProxy.name)
  console.log("-----这是关于name的响应式函数------")
})
watchFn(function () {
  console.log("-----这是关于age的响应式函数------")
  console.log("开始执行响应式代码")
  console.log("假设我这里有很多很多代码")
  console.log(xiaomingProxy.age)
  console.log("-----这是关于age的响应式函数------")
})
// xiaomingProxy.name
xiaomingProxy.name = 'xxxx'
xiaomingProxy.age

到这我们的响应式基本已经实现了;但当我测试时发现还是有几个小小的问题:

  1. 当我一段响应式代码中重复出现相同变量时,我们现在的依赖也会重复收集,导致代码重复执行
  2. 我们拦截器中直接使用全局变量activeReactiveFn;存在耦合

你一个问题是因为我们把依赖存放在数组中,当重复使用变量时我们又没有给他去重,导致代码重新执行;所以我们可以考虑使用set来保存数据;这样就不会存在重复数据 第二个问题其实我们可以把整个收集封装在我们的depend类当中,在get中只需调用相关方法即可;

所以最终优化后代码如下:

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
    this.address = "上海"
  }
  eat() {
    console.log(this.name + '吃东西')
  }
}
let xiaoming = new Person('xiaoming', 27)

class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }


  depend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}


// 封装一个响应式的函数
// 定义一个全局变量,以便当get添加依赖获取相关的响应式代码
let activeReactiveFn = null
function watchFn(fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = 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)
  }

  // 根据key获取depend对象
  let depend = map.get(key)
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}

// 监听对象的属性变量
const xiaomingProxy = new Proxy(xiaoming, {
  get: function (target, key, receiver) {
    // 根据target.key获取对应的depend
    const depend = getDepend(target, key)
    // 给depend对象中添加响应函数
    // depend.addDepend(activeReactiveFn)
    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()
  }
})
//当对象属性发生变化时,执行reactiveFns里面的代码,实现响应式
watchFn(function () {
  console.log("-----这是关于name的响应式函数------")
  console.log("我是小明")
  console.log("开始执行响应式代码")
  console.log("假设我这里有很多很多代码")
  console.log(xiaomingProxy.name)
  console.log(xiaomingProxy.name)
  console.log("-----这是关于name的响应式函数------")
})
watchFn(function () {
  console.log("-----这是关于age的响应式函数------")
  console.log("开始执行响应式代码")
  console.log("假设我这里有很多很多代码")
  console.log(xiaomingProxy.age)
  console.log("-----这是关于age的响应式函数------")
})
// xiaomingProxy.name
xiaomingProxy.name = 'xxxx'
xiaomingProxy.age

好了,大功告成!

总结

数据响应式实现原理其实就分为数据拦截、依赖收集、代码响应这几个步骤,跟着文章一步一步下来,相信会有一个比较深刻的理解!