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


    响应式原理

    🍤认识响应式逻辑

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

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

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

    let num = 50
    
    console.log(num + 50)
    console.log(num * num)
    
    num = 150
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

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

    // 对象的响应式
    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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

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

    // 对象的响应式
    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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

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

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

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

    🍤响应式依赖收集

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

    这个时候我们封装一个新的函数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())
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    🍤响应式依赖管理

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

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

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

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

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

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

    • 定义的类如下:
    // 使用一个类代替数组来进行依赖管理
    class Depend {
      constructor() {
        // 定义存放响应式函数的数组
        this.reactiveFns = []
      }
    
      // 定义实例方法, 用于收集需要响应式的函数
      addDepend(fn) {
        if (fn) {
          this.reactiveFns.push(fn)
        }
      }
    
      // 定义方法, 用于数据改变时, 执行数组中的响应式的函数
      notify() {
        this.reactiveFns.forEach(fn => fn())
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 使用类管理依赖
    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()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    🍤监听属性的变化

    目前存在的一个问题就是: 我们当某个属性发生变化, 我们想要它响应式, 就需要手动的调用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
        }
      }) 
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

    // 修改obj属性
    console.log("------------name属性发生改变------------")
    obj.name = "kaisa"
    
    console.log("------------age属性发生改变------------")
    obj.age = 38
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在这里插入图片描述


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

    目前我们是通过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[map]放到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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    接下来我们就需要正确的将依赖收集起来, 我们之前收集依赖的地方是在 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
        }
      }) 
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    当属性改变时, 会执行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
        }
      }) 
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

    // 使用一个类代替数组来进行依赖管理
    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[map]放到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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    🍤对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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    解决方式: 使用Set结构

    class Depend {
      constructor() {
        // 定义存放响应式函数换成Set
        this.reactiveFns = new Set()
      }
    
      addDepend(fn) {
        if (fn) {
          this.reactiveFns.add(fn)
        }
      }
    
      notify() {
        this.reactiveFns.forEach(fn => fn())
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    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())
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    get: function() {
      const dep = getDepend(obj, key)
    	 // 调用depend方法  
      dep.depend()
      return value
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    🍤创建响应式对象

    我们目前的响应式是针对于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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    当我们想要响应式时, 包裹一层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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    🍤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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
  • 相关阅读:
    css-定位position 理论
    线程状态及线程之间通信
    计组中的各种周期辨析
    【数据结构】---哈希表
    docker数据卷管理
    DotNetCore.CAP 基础应用
    2.1 CSS 简介特性
    git PR合并提交(rebase方式)
    【操作系统笔记十四】科普:POSIX 是什么
    DEFORMABLE DETR:用于端到端对象检测的可变形Transformer
  • 原文地址:https://blog.csdn.net/m0_71485750/article/details/126211115