Vue3/Vu2响应式原理的实现

375 阅读8分钟

1.什么是响应式?

可以自动响应数据变量的代码机制,我们就称之为是响应式的

2.响应式函数设计

执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中

const obj = {
  name: "张三",
  age: 18
}

// 2.1 将需要执行的代码封装成函数
function foo() {
  const newName = obj.name
  console.log(newName)
  console.log(obj.name)
}

// 2.2 当对象属性改变时,执行foo()
obj.name = "李四"
foo()
// 李四
// 李四

在开发中我们是有很多的函数的,我们如何区分一个函数需要响应式,还是不需要响应式呢?

3.响应式函数的封装

这个时候我们封装一个新的函数watchFn;凡是传入到watchFn的函数,就是需要响应式的;其他默认定义的函数都是不需要响应式的;

const obj = {
  name: "张三",
  age: 18
}
// 3.1 声明一个数组
let reactiveFns = []
// 3.2 将需要响应式的函数保存进数组
function watchFn(fn) {
  reactiveFns.push(fn)
}

watchFn(function foo() {
  console.log(obj.name)
})

watchFn(function bar() {
  console.log(obj.age)
})

console.log(reactiveFns) // [foo(),bar()]

obj.name = "王五"
obj.age = 20

// 3.3 遍历响应式的函数,执行需要响应的函数
reactiveFns.forEach((fn) => {
  fn()
})

// [foo(),bar()]
// 王五
// 王五
// 20
// 20

4.响应式依赖的收集

目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:

  • 我们在实际开发中需要监听很多对象的响应式;
  • 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数;
  • 我们不可能在全局维护一大堆的数组来保存这些响应函数;

所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数:

好处:

  1. 每个对象属性对应一个new Depend对象,每个new Depend对象对应一个reactiveFns数组,这样就不会发生改变一个属性,所有属性的reactiveFns都会执行的问题
  2. notify()封装进class,这样我们不需要遍历,只需要调用notify()
const obj = {
  name: "张三",
  age: 18
}

// 4.1 数组重写成类
class Depend {
  constructor() {
    this.reactiveFns = []
  }

  addDepend(reactiveFn) {
    this.reactiveFns.push(reactiveFn)
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn()
    })
  }
}
// 4.2 重写响应式的函数
const depend = new Depend()
function watchFn(fn) {
  depend.addDepend(fn)
}

watchFn(function () {
  console.log(obj.name)
  
})
watchFn(function () {
  console.log(obj.age)
})
obj.name = "赵二"
obj.age = 25
depend.notify()

问题:当属性改变时,我们只能手动调用notify()方法来实现响应式

5. 监听对象的变化

那么我们接下来就可以通过new Proxy的方式(vue3采用的方式)来监听对象的变量

const obj = {
  name: "张三",
  age: 18
}

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)
}
// 5.1 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = 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)
        // 5.2 每次修改都要调用notify()
        depend.notify()
    }
})

watchFn(function () {
  console.log(objProxy.name)
})
watchFn(function () {
  console.log(objProxy.age)
})

objProxy.name = "赵二"
objProxy.age = 25

// 赵二
// 18
// 赵二
// 25

问题:只有一个depend对象,改变某个对象属性,所有响应式函数都会执行

6.对象的依赖管理

我们目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数:

  • 但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;
  • 所以我们可以用Map来管理一个对象的依赖,用WeakMap来管理不同对象的依赖;

也就是 objMap(WeakMap) -> obj依赖(Map)->name依赖(Depend对象)

7.对象依赖管理的实现

我们可以写一个getDepend函数专门来管理这种依赖关系

const obj = {
  name: "张三",
  age: 18
}

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)
}

// 7.1 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepends(target, key) {
  // 7.2 根据对象获取对应的Map对象
  let objMap = targetMap.get(target)
  if (!objMap) {
    objMap = new Map()
    targetMap.set(target, objMap)
  }

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

const objProxy = 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)
    // 7.4 重写depend调用方式
    const depend = getDepends(target, key)
    console.log(depend)
    depend.notify()
  }
})

watchFn(function () {
  console.log(objProxy.name)
})
watchFn(function () {
  console.log(objProxy.age)
})

objProxy.name = "赵二"
objProxy.age = 25
//[]
//[]

问题:这时候获取的依赖是空的

8.正确的依赖收集

  • 我们之前收集依赖的地方是在 watchFn 中:
    • 但是这种收集依赖的方式我们根本不知道是哪一个key的哪一个depend需要收集依赖;
    • 你只能针对一个单独的depend对象来添加你的依赖对象;
  • 那么正确的应该是在哪里收集呢?应该在我们调用了Proxyget捕获器时
    • 因为如果一个函数中使用了某个对象的key,那么它应该被收集依赖;
const obj = {
  name: "张三",
  age: 18
}

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

  addDepend(reactiveFn) {
    this.reactiveFns.push(reactiveFn)
  }
  notify() {
    this.reactiveFns.forEach((fn) => {
      fn()
    })
  }
}
// 保存当前需要收集的响应式函数,在objProxy的get中使用
let activeReactiveFn = null
function watchFn(fn) {
  activeReactiveFn = fn
  fn()
  // 防止watchFn不传fn时被乱添加
  activeReactiveFn = null
}

// 8.1 封装一个获取depend函数
const targetMap = new WeakMap()
function getDepends(target, key) {
  // 根据对象获取对应的Map对象
  let objMap = targetMap.get(target)
  if (!objMap) {
    objMap = new Map()
    targetMap.set(target, objMap)
  }

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

const objProxy = new Proxy(obj, {
  get: function (target, key, receiver) {
    // 8.2 根据target.key获取对应的depend
    const depend = getDepends(target, key)
    // 给depend对象中添加响应函数
    depend.addDepend(activeReactiveFn)
    return Reflect.get(target, key, receiver)
  },
  set: function (target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 8.2 根据target.key获取对应的depend
    const depend = getDepends(target, key)
    depend.notify()
  }
})

watchFn(function () {
  console.log(objProxy.name)
})
watchFn(function () {
  console.log(objProxy.age)
})

objProxy.name = "赵二"
objProxy.age = 25
// 张三
// 18
// 赵二
// 25

9.对Depend重构

但是这里有两个问题:

  • 问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次;
  • 问题二:我们并不希望将添加reactiveFn放到get中,因为它是属于Depend的行为;

所以我们需要对Depend类进行重构:

  • 解决问题一的方法:不使用数组,而是使用Set
  • 解决问题二的方法:添加一个新的方法,用于收集依赖;
// 定义全局变量是为了获取需要响应的fn在objProxy的get中使用
let activeReactiveFn = null

const obj = {
  name: "张三",
  age: 18
}

class Depend {
  constructor() {
    // 9.2 将数组重写成Set防止多次添加同一依赖函数
    this.reactiveFns = new Set()
  }

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

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

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

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

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

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

const objProxy = new Proxy(obj, {
  get: function (target, key, receiver) {
    // 根据target.key获取对应的depend
    const depend = getDepends(target, key)
    depend.depend()
    return Reflect.get(target, key, receiver)
  },
  set: function (target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 重写depend获取方式
    const depend = getDepends(target, key)
    depend.notify()
  }
})

watchFn(function () {
  console.log(objProxy.name, "--------------")
  console.log(objProxy.name, "++++++++++++++")
})
watchFn(function () {
  console.log(objProxy.age)
})
// 第一次执行还是会添加两次依赖函数
objProxy.name = "赵二"
objProxy.age = 25
// 第二次执行只会添加一次
objProxy.name = "王五"

10.对象的响应式操作

我们目前的响应式是针对于objProxy一个对象的,我们可以创建出来一个函数,针对所有的对象都可以变成响应式对象

// 定义全局变量是为了获取需要响应的fn在objProxy的get中使用
let activeReactiveFn = null

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

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

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

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

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

const targetMap = new WeakMap()
function getDepends(target, key) {
  // 根据对象获取对应的Map对象
  let objMap = targetMap.get(target)
  if (!objMap) {
    objMap = new Map()
    targetMap.set(target, objMap)
  }

  // 根据key获取Depend对象
  let depend = objMap.get(key)
  if (!depend) {
    depend = new Depend()
    objMap.set(key, depend)
  }
  return depend
}
// 10.1 将objProxy 封装成一个reactive函数
function reactive(obj) {
  return new Proxy(obj, {
    get: function (target, key, receiver) {
      // 根据target.key获取对应的depend
      const depend = getDepends(target, key)
      depend.depend()
      return Reflect.get(target, key, receiver)
    },
    set: function (target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)
      // 重写depend获取方式
      const depend = getDepends(target, key)
      depend.notify()
    }
  })
}

const obj = reactive({
  name: "张三",
  age: 18
})

watchFn(() => {
  console.log(obj.name, "--------------")
  console.log(obj.name, "++++++++++++++")
})
watchFn(() => console.log(obj.age))
// 第一次执行还是会添加两次依赖函数
obj.name = "赵二"
obj.age = 25
// 第二次执行只会添加一次
obj.name = "王五"

11.Vue2响应式原理

Vue2中通过我们前面学习过的Object.defineProerty的方式来实现对象属性的监听;

我们可以将reactive函数进行如下的重构:

  • 在传入对象时,我们可以遍历所有的key,并且通过属性存储描述符来监听属性的获取和修改;
  • settergetter方法中的逻辑和前面的Proxy是一致的;
// 定义全局变量是为了获取需要响应的fn在objProxy的get中使用
let activeReactiveFn = null

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

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

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

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

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

const targetMap = new WeakMap()
function getDepends(target, key) {
  // 根据对象获取对应的Map对象
  let objMap = targetMap.get(target)
  if (!objMap) {
    objMap = new Map()
    targetMap.set(target, objMap)
  }

  // 根据key获取Depend对象
  let depend = objMap.get(key)
  if (!depend) {
    depend = new Depend()
    objMap.set(key, depend)
  }
  return depend
}
// 11.1 将 objProxy 封装成一个reactive函数,使用Object.defineProperty
function reactive(obj) {
  // ES6之前, 使用Object.defineProperty
  Object.keys(obj).forEach((key) => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get: function () {
        const depend = getDepends(obj, key)
        depend.depend()
        return value
      },
      set: function (newValue) {
        value = newValue
        const depend = getDepends(obj, key)
        depend.notify()
      }
    })
  })
  return obj
}

const obj = reactive({
  name: "张三",
  age: 18
})

watchFn(() => {
  console.log(obj.name, "--------------")
  console.log(obj.name, "++++++++++++++")
})
watchFn(() => console.log(obj.age))
// 第一次执行还是会添加两次依赖函数
obj.name = "赵二"
obj.age = 25
// 第二次执行只会添加一次
obj.name = "王五"