深度剖析Vue2、Vue3响应式原理 | 逐步推敲手写响应式原理全过程

导读:本篇文章讲解 深度剖析Vue2、Vue3响应式原理 | 逐步推敲手写响应式原理全过程,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

响应式原理

🍤认识响应式逻辑

我们先来看一下响应式意味着什么?我们来看一段代码

num有一个初始化的值,有一段代码使用了这个值;

那么在num有一个新的值时,我们希望这段代码可以自动重新执行;

let num = 50

console.log(num + 50)
console.log(num * num)

num = 150

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

那么我们再来看一下对象的响应式

// 对象的响应式
const obj = {
  name: "chenyq",
  age: 18
}

// 当name属性变化时, 需要重新执行下面这段代码
console.log(obj.name)
console.log(obj.age)

// 当age变化时才需要执行这段代码, name变化不需要重新执行
console.log(obj.age + 100)

// 修改obj的name属性
obj.name = "kaisa"

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

那么我们的问题就变成了,当数据发生变化时,自动去执行某一个函数

// 对象的响应式
const obj = {
  name: "chenyq",
  age: 18
}

// 将两段代码分别封装成两个函数, 当数据变化需要重新执行时, 只需重新调用函数
function foo() {
  console.log(obj.name)
  console.log(obj.age)
}

function bar() {
  console.log(obj.age + 100)
}

// 修改obj的name属性
obj.name = "kaisa"

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

很明显,下面的函数中 foo 需要在obj的name发生变化时,重新执行,做出相应;

bar函数是一个完全独立于obj的函数,它不需要执行任何响应式的操作;

function foo() {
  let newName = obj.name
  console.log(obj.name)
}
function bar() {
  const result = 20 + 30 
  console.log(result)
}

🍤响应式依赖收集

我们如何区分一个函数是否需要响应式呢?

这个时候我们封装一个新的函数watchFn用来收集name属性变化时, 需要响应式的函数;

凡是传入到watchFn的函数,就是需要响应式的, 再由watchFn函数将他们存放在一个数组中;

其他默认定义的函数都是不需要响应式的;

const obj = {
  name: "chenyq",
  age: 18
}

// 将依赖对象的函数, 统一收集到一个数组中
const reactiveFns = []
// 设计一个专门收集响应式函数的函数
function watchFn(fn) {
  reactiveFns.push(fn)
  // 函数传进来时, 会自动执行一次
  fn()
}

// 调用函数, 将函数收集到数组中
watchFn(function foo() {
  console.log("foo:", obj.name)
  console.log("foo:", obj.age)
})

watchFn(function bar() {
  console.log("bar:", "hello " + obj.name)
  console.log("bar:", obj.age + 10)
})

// 修改obj属性
obj.name = "kaisa"
// 修改obj属性, 将收集响应式函数的数组遍历并且全部调用
reactiveFns.forEach( fn => fn())

🍤响应式依赖管理

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

我们在实际开发中需要监听很多对象的响应式;

这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数;

我们不可能在全局维护一大堆的数组来保存这些响应函数;

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

相当于替代了原来的简单 reactiveFns 的数组;

  • 定义的类如下:
// 使用一个类代替数组来进行依赖管理
class Depend {
  constructor() {
    // 定义存放响应式函数的数组
    this.reactiveFns = []
  }

  // 定义实例方法, 用于收集需要响应式的函数
  addDepend(fn) {
    if (fn) {
      this.reactiveFns.push(fn)
    }
  }

  // 定义方法, 用于数据改变时, 执行数组中的响应式的函数
  notify() {
    this.reactiveFns.forEach(fn => fn())
  }
}
  • 使用类管理依赖
const obj = {
  name: "chenyq",
  age: 18
}

// 创建一个实例
const dep = new Depend()
function watchFn(fn) {
  // 使用实例方法, 将响应式的函数添加到类中
  dep.addDepend(fn)
  // 函数传进来时, 会自动执行一次
  fn()
}

// 调用函数, 将函数收集到数类中
watchFn(function foo() {
  console.log("foo:", obj.name)
  console.log("foo:", obj.age)
})

watchFn(function bar() {
  console.log("bar:", "hello " + obj.name)
  console.log("bar:", obj.age + 10)
})

// 修改obj属性
obj.name = "kaisa"
// 当name数据改变时, 仅需调用类中的notify方法即可
dep.notify()

🍤监听属性的变化

目前存在的一个问题就是: 我们当某个属性发生变化, 我们想要它响应式, 就需要手动的调用notify方法, 如果没有调用, 那么数据就不会更新.

这样操作是非常繁琐的, 我们希望实现自动监听刷新数据的效果

那么我们接下来就可以通过之前学习的方式来监听对象的变量

方式一:通过 Object.defineProperty的方式(vue2采用的方式);

方式二:通过new Proxy的方式(vue3采用的方式);

我们这里先以Object.defineProperty的方式来监听:

Object.keys(obj).forEach(key => {
  let value = obj[key]
  Object.defineProperty(obj, key, {
    set: function(newValue) {
      value = newValue
      dep.notify()
    },
    get: function() {
      return value
    }
  }) 
})

监听后就不需要再手动调用, 当属性发生变化时, 会自动调用notify方法, 实现数据更新

// 修改obj属性
console.log("------------name属性发生改变------------")
obj.name = "kaisa"

console.log("------------age属性发生改变------------")
obj.age = 38

在这里插入图片描述


🍤自动的收集依赖(核心难点)

目前我们是通过watchFn不管三七二十一的, 将函数添加到类中, 当属性改变时重新执行添加到类中的函数

如果向类中添加两个函数, 但是如果其中一个依赖name, 另一个没有依赖name属性, 这样的添加方法是有问题的, 没有依赖name属性的函数, 我们应该不去向类中添加

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

但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理;

我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?

在前面ES6新特性中我讲解过WeakMap,并且在讲WeakMap的时候我讲到了后面通过WeakMap如何管理这种响应式的数据依赖(我们会按照如下数据结构对响应式输入依赖进行管理)

dep对象数据结构的管理(最难理解)

  • 每一个对象的每一个属性都会对应一个dep对象

  • 同一个对象的多个属性的dep对象是存放一个map对象中

  • 多个对象分别对应的map对象, 又会被存放到一个objMap的对象中

依赖收集: 当执行get函数, 自动的添加fn函数; 当执行set函数, 自动执行Depend对象的notify方法

在这里插入图片描述

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

这样我们调用getDepend函数, 一定会返回一个depend对象, 并且在get和set方法中, 可以根据obj和key拿到正确的depend对象

// 封装一个函数: 负责通过obj的key获取对应的Depend对象
const objMap = new WeakMap()
function getDepend(obj, key) {
  // 1.根据obj对象, 找到obj对应的map对象
  let map = objMap.get(obj)
  // 当map对象不存在时, 创建一个map对象, 再将obj放到objMap中
  if (!map) {
    map = new Map()
    objMap.set(obj, map)
  }

  // 2.根据key, 找到map对应的depend对象
  let dep = map.get(key)
  // dep没有值时, 创建一个depend对象, 存入对应的map对象中
  if (!dep) {
    dep = new Depend()
    map.set(key, dep)
  }
  return dep
}

接下来我们就需要正确的将依赖收集起来, 我们之前收集依赖的地方是在 watchFn 中

但是之前这种收集依赖的方式我们根本不知道是哪一个key的哪一个depend需要收集依赖, 只能针对一个单独的depend对象来添加你的依赖对象;

那么正确的应该是在哪里收集呢?应该在我们调用了get捕获器时, 因为如果一个函数中使用了某个对象的key,那么它应该被收集依赖, 而又当一个函数中使用了某个对象的key, 那么就会执行该对象的get方法, 我们可以在get捕获器中, 将正确的依赖收集进来;

// 封装函数: 用于收集依赖
// 定义一个变量, 临时保存传入的fn函数, 方便在get中添加到depend对象
let reactiveFn = null
function watchFn(fn) {
  reactiveFn = fn
  // 函数传进来时,会 自动执行一次
  fn()
  reactiveFn = null
}

Object.keys(obj).forEach(key => {
  let value = obj[key]
  Object.defineProperty(obj, key, {
    get: function() {
      // 获取正确的dep, 将函数添加进去
      const dep = getDepend(obj, key)
      dep.addDepend(reactiveFn)
      return value
    }
  }) 
})

当属性改变时, 会执行set方法, 我们在set方法中可以拿到当前dep对象, 并执行当前dep对象的notify方法

Object.keys(obj).forEach(key => {
  let value = obj[key]
  Object.defineProperty(obj, key, {
    set: function(newValue) {
      value = newValue
      // 获取obj.key对应的depend对象
      const dep = getDepend(obj, key)
      dep.notify()
    },
    get: function() {
      // 找到正确的dep, 将函数添加进去
      const dep = getDepend(obj, key)
      dep.addDepend(reactiveFn)
      return value
    }
  }) 
})

按照上诉步骤, 就完成了自动收集依赖的实现, 完整代码如下

// 使用一个类代替数组来进行依赖管理
class Depend {
  constructor() {
    // 定义存放响应式函数的数组
    this.reactiveFns = []
  }

  // 定义实例方法, 用于收集需要响应式的函数
  addDepend(fn) {
    if (fn) {
      this.reactiveFns.push(fn)
    }
  }

  // 定义方法, 用于数据改变时, 执行数组中的响应式的函数
  notify() {
    this.reactiveFns.forEach(fn => fn())
  }
}

const obj = {
  name: "chenyq",
  age: 18
}

// 封装函数: 用于收集依赖
// 定义一个变量, 临时保存传入的fn函数, 方便在get中添加到depend对象
let reactiveFn = null
function watchFn(fn) {
  reactiveFn = fn
  // 函数传进来时,会 自动执行一次
  fn()
  reactiveFn = null
}

// 封装一个函数: 负责通过obj的key获取对应的Depend对象
const objMap = new WeakMap()
function getDepend(obj, key) {
  // 1.根据obj对象, 找到obj对应的map对象
  let map = objMap.get(obj)
  // 当map对象不存在时, 创建一个map对象, 再将obj放到objMap中
  if (!map) {
    map = new Map()
    objMap.set(obj, map)
  }

  // 2.根据key, 找到map对应的depend对象
  let dep = map.get(key)
  // dep没有值时, 创建一个depend对象, 存入对应的map对象中
  if (!dep) {
    dep = new Depend()
    map.set(key, dep)
  }
  return dep
}

Object.keys(obj).forEach(key => {
  let value = obj[key]
  Object.defineProperty(obj, key, {
    set: function(newValue) {
      value = newValue
      // 获取obj.key对应的depend对象
      const dep = getDepend(obj, key)
      dep.notify()
    },
    get: function() {
      // 获取正确的dep, 将函数添加进去
      const dep = getDepend(obj, key)
      dep.addDepend(reactiveFn)
      return value
    }
  }) 
})

// 调用函数, 将函数收集到数类中
watchFn(function foo() {
  console.log("foo:", obj.name)
  console.log("foo:", obj.age)
})

watchFn(function bar() {
  console.log("bar:", obj.age + 10)
})

// 测试: 
// 修改obj属性
console.log("------------name属性发生改变------------")
obj.name = "kaisa"

console.log("------------age属性发生改变------------")
obj.age = 38

🍤对Depend重构

自动收集依赖已经实现, 但是这里有两个小问题:

问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次;

问题二:我们并不希望将添加reactiveFn放到get中,以为它是属于Dep的行为;

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

解决问题一的方法:不使用数组,而是使用Set;

如下代码, 例如当函数中有使用两次或多次name属性时, name属性也会被多次添加到dep.reactiveFns数组中, 导致函数被多次执行

// 调用函数, 将函数收集到数类中
watchFn(function foo() {
  console.log("foo:", obj.name)
  console.log("foo:", obj.name)
  console.log("foo:", obj.name)
  console.log("foo:", obj.age)
})

watchFn(function bar() {
  console.log("bar:", obj.age + 10)
})

// 测试: 
// 修改obj属性
console.log("------------name属性发生改变------------")
obj.name = "kaisa"

在这里插入图片描述

解决方式: 使用Set结构

class Depend {
  constructor() {
    // 定义存放响应式函数换成Set
    this.reactiveFns = new Set()
  }

  addDepend(fn) {
    if (fn) {
      this.reactiveFns.add(fn)
    }
  }

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

解决问题二的方法:添加一个新的方法,用于收集依赖;

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

  addDepend(fn) {
    if (fn) {
      this.reactiveFns.add(fn)
    }
  }

  // 添加一个新的方法,用于收集依赖
  depend() {
    if (reactiveFn) {
      this.reactiveFns.add(reactiveFn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => fn())
  }
}
get: function() {
  const dep = getDepend(obj, key)
	 // 调用depend方法  
  dep.depend()
  return value
}

🍤创建响应式对象

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

function reactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      set: function(newValue) {
        value = newValue
        // 获取obj.key对应的depend对象
        const dep = getDepend(obj, key)
        dep.notify()
      },
      get: function() {
        // 获取正确的dep, 将函数添加进去
        const dep = getDepend(obj, key)
        // dep.addDepend(reactiveFn)
        dep.depend()
        return value
      }
    }) 
  })
  return obj
}

当我们想要响应式时, 包裹一层reactive即可

const obj = reactive({
  name: "chenyq",
  age: 18,
  address: "成都市"
})

watchFn(function foo() {
  console.log("foo:", obj.name)
  console.log("foo:", obj.age)
})

const user = reactive({
  userName: 'aaabbbccc',
  passWorld: "abc123"
})

watchFn(function bar() {
  console.log("bar:", user.userName)
  console.log("bar:", user.passWorld)
})

// 测试: 
obj.name = "kaisa"

user.passWorld = "123456"

🍤Vue3响应式原理

我们前面所实现的响应式的代码,其实就是Vue2中的响应式原理

Vue3主要是通过Proxy来监听数据的变化以及收集相关 的依赖的;

Vue2中通过Object.defineProerty 的方式来实现对象属性的监听;

Vue2和Vue3的原理思路是一样的, 我们只需要将reactive函数中的Object.defineProerty重构成Proxy来监听数据, 就是Vue3的响应式原理

function reactive(obj) {
  const objProxy = new Proxy(obj, {
    set: function(target, key, newValue) {
      // 设置新值
      Reflect.set(target, key, newValue)
      const dep = getDepend(target, key)
      dep.notify()
    },
    get: function(target, key) {
      const dep = getDepend(target, key)
      dep.depend()
      return Reflect.get(target, key)
    }
  })
  return objProxy
}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/120066.html

(0)
seven_的头像seven_bm

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!