JS Advance --- 响应式

278 阅读5分钟

什么是响应式

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

当逻辑中有一个变量X,有某些逻辑代码在运行的时候,需要使用变量X

那么我们就认为这些需要使用变量X的代码为变量X的依赖

而当变量X发生改变的时候,可以自动去执行变量X的所有依赖的过程,就被称之为响应式

模拟实现

响应式函数

const user = {
  name: 'Klaus',
  age: 23
}

const reactiveNameFns = []
const reactiveAgeFns = []

// 定义name的响应式函数
function watchNameEffect(fn) {
  reactiveNameFns.push(fn)
}

// 定义age的响应式函数
function watchAgeEffect(fn) {
  reactiveAgeFns.push(fn)
}

watchNameEffect(function() {
  console.log('name需要响应的代码 - 1')
})

watchNameEffect(function() {
  console.log('name需要响应的代码 - 2')
})

function foo() {
  console.log('其它业务逻辑,不需要被响应')
}

watchAgeEffect(() => console.log('age需要响应的代码'))

user.name = 'Alex'

// user的name属性发生改变了,需要name需要响应的代码
reactiveNameFns.forEach(fn => fn())

console.log('----------------')

// user的age属性发生改变了,执行age需要响应的代码
reactiveAgeFns.forEach(fn => fn())

可以看到使用响应式函数后,当某一个属性发生改变的时候

对应的需要执行的代码都被正确的执行了

但是单单使用响应式函数是存在很多问题的

  1. 依赖并不是自动收集的,对应的更新也不是自动更新的,都是需要手动执行的
  2. 一个需要响应的属性 对应一个响应式函数和一个收集依赖的数组,存在很多的可抽取的重复代码

依赖响应类

const user = {
  name: 'Klaus',
  age: 23
}

// 定义响应类 --- 重复代码的抽离
class Dep {
  constructor() {
    this.depFns = []
  }

  depend(fn) {
    this.depFns.push(fn)
  }

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

// 一个属性 对应 一个Dep实例
const nameDep = new Dep()
const ageDep = new Dep()

nameDep.depend(() => console.log('name依赖 - 1'))
nameDep.depend(() => console.log('name依赖 - 2'))
nameDep.notify()

ageDep.depend(() => console.log('age依赖'))
ageDep.notify()

收集依赖的数据结构

之前的代码最大的问题是每一个属性会存在一个实例,随着项目越来越大,对应的dep实例也会越来越多

所以我们需要使用一个数据结构来管理所有的dep实例,这就是WeakMap

IfgTWR.png

const user = {
  name: 'Klaus',
  age: 23
}

// activeReactiveFn 是为了在watchEffect 和 Dep实例的set方法 传递 响应函数
let activeReactiveFn = null

// 收集响应类
class Dep {
  constructor() {
    // 使用set的原因是
    // 如果一个响应函数中使用了同一个属性多次,如果使用的是数组,那么同一个响应函数会被多次添加
    // 此时如果执行notify方法,那么多个相同的响应函数会被依次重复执行
    // 这是没有意义的,所以使用set来存储响应函数的目的是为了对应响应函数进行去重操作
    this.depFns = new Set()
  }

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

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

// 对所有的dep实例进行统一管理
const deps = new WeakMap()

// 根据对象和对象的属性名 去获取对应的 dep实例
function getDep(target, key) {
  let map = deps.get(target)

  if (!map) {
    map = new Map()
    deps.set(target, map)
  }

  let depends = map.get(key)

  if (!depends) {
    depends = new Dep()
    map.set(key, depends)
  }

  return depends
}

// 监听对象的属性的set和get
const userProxy = new Proxy(user, {
  get(target, key, receiver) {
     // 收集依赖
     const dependency = getDep(target, key)
     dependency.depend()

    return Reflect.get(target, key, receiver)
  },

  set(target, key, newValue, receiver) {
    // 需要先更新值后再去修改依赖
    // 不然执行的响应式函数中使用的值依旧是更新前的值
    Reflect.set(target, key, newValue, receiver)

    const dependency = getDep(target, key)
    dependency.notify()
  }
})

function watchEffect(fn) {
  activeReactiveFn = fn

  // 收集依赖的时候,需要先执行一遍响应函数
  // 只有这样,对应属性的get方法才会被执行
  fn()

  activeReactiveFn = null
}

// 只有在收集和使用的时候,全部使用代理后的对象,其属性对应的响应函数才会被正常收集和执行
// 如果使用原对象,其对应的属性的set和get方法并没有被修改,所以无法触发对应的响应式

// 响应依赖函数
watchEffect(() => console.log(`user的name发生了改变 - 1 --- ${userProxy.name}`))
watchEffect(() => console.log(`user的name发生了改变 - 2 --- ${userProxy.name}`))
watchEffect(() => console.log(`user的age发生了改变 ---- ${userProxy.age} --- ${userProxy.age}`))

console.log('-------------------------')

// userProxy.name = 'Alex'
userProxy.age = 18

但是,此时的代码实现依旧是存在弊端的,因为此时我们需要手动的将一个函数转换为对应的代理对象

对此,我们可以将此操作进行一个简单的封装,将对象转换为代理对象的过程封装成一个名为reactive的函数

reactive函数

let activeReactiveFn = null

class Dep {
  constructor() {
    this.depFns = new Set()
  }

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

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

const deps = new WeakMap()

function getDep(target, key) {
  let map = deps.get(target)

  if (!map) {
    map = new Map()
    deps.set(target, map)
  }

  let depends = map.get(key)

  if (!depends) {
    depends = new Dep()
    map.set(key, depends)
  }

  return depends
}

// 对象 ---> 代理对象
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
       const dependency = getDep(target, key)
       dependency.depend()

      return Reflect.get(target, key, receiver)
    },

    set(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)

      const dependency = getDep(target, key)
      dependency.notify()
    }
  })
}

const user = reactive({
  name: 'Klaus',
  age: 23
})

function watchEffect(fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}

watchEffect(() => console.log(`user的name发生了改变 - 1 --- ${user.name}`))
watchEffect(() => console.log(`user的name发生了改变 - 2 --- ${user.name}`))
watchEffect(() => console.log(`user的age发生了改变 ---- ${user.age} --- ${user.age}`))

console.log('-------------------------')

// user.name = 'Alex'
user.age = 18

而这就是 Vue3中 watchEffect函数 和 reactive函数的简单实现

vue2响应式 vs vue3响应式

vue最早发布于2014年,也就是ES6发布之前, 所以在vue2中,并不是使用proxy进行数据代理的,而是使用了Object.defineProrperty

function reactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]

    Object.defineProperty(obj, key, {
      set(newValue) {
        value = newValue

        const dependency = getDep(obj, key)
        dependency.notify()
      },

      get() {
        const dependency = getDep(obj, key)
        dependency.depend()
        return value
      }
    })
  })

  return obj
}

Object.defineProrperty只能对对象的属性的get和set进行拦截,并不能对对象的其它操作,例如添加新属性,删除属性进行监听

所以在vue2定义好data后,如果希望为data赋值新属性的时候,这个属性并不是响应式的,因为此时并没有监听其get和set方法

因此vue2提供了$setAPI 来专门解决这个问题

另外,本质上,使用Object.defineProrperty修改对象的属性的时候,修改的是原数据,这可能导致数据的不可控,不利于后期的维护和调试

因此自ES6提出Proxy后,vue3在重构vue代码的时候,使用proxy对数据进行代理,此时是对整个对象的所有操作都进行了拦截,而不是只能对get和set方法进行拦截操作

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
       const dependency = getDep(target, key)
       dependency.depend()

      return Reflect.get(target, key, receiver)
    },

    set(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)

      const dependency = getDep(target, key)
      dependency.notify()
    }
  })
}