Skip to content

1.什么是响应式?

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

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

    • 那么在m有一个新的值时,这段代码可以自动重新执行

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

    • 那么我们再来看一下对象的响应式:
image-20220817192022063

2.响应式函数设计

  • 首先,执行的代码中可能不止一行代码,所以我们可以将这些代码放到一个函数中:
    • 那么我们的问题就变成了,当数据发生变化时,自动去执行某一个函数
image-20220817192126986
  • 但是有一个问题:在开发中我们是有很多的函数的,我们如何区分一个函数需要响应式,还是不需要响应式呢?
    • 很明显,下面的函数中foo需要在objname发生变化时,重新执行,做出相应
    • bar函数是一个完全独立于obj的函数,它不需要执行任何响应式的操作
image-20220817192137883

3.响应式函数的实现watchFn

  • 但是我们怎么区分呢?
    • 这个时候我们封装一个新的函数watchFn
    • 凡是传入到watchFn的函数,就是需要响应式的
    • 其他默认定义的函数都是不需要响应式的
image-20220817192222713

4.响应式依赖的收集

  • 目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:
    • 我们在实际开发中需要监听很多对象的响应式
    • 这些对象需要监听的不只是一个属性,它们很多属性的变化,都会有对应的响应式函数
    • 我们不可能在全局维护一大堆的数组来保存这些响应函数
  • 所以我们要设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数:
    • 相当于替代了原来的简单reactiveFns的数组
image-20220817192313139

5.监听对象的变化

  • 那么我们接下来就可以通过之前学习的方式来监听对象的变量:
    • 方式一:通过 Object.defineProperty的方式(vue2采用的方式)
    • 方式二:通过new Proxy的方式(vue3采用的方式)
  • 我们这里先以Proxy的方式来监听:
image-20220817192458958

6.对象的依赖管理

  • 我们目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数:
    • 但是实际开发中我们会有不同的对象,另外会有不同的属性需要管理
    • 我们如何可以使用一种数据结构来管理不同对象的不同依赖关系呢?
  • 在前面我们刚刚学习过WeakMap,并且在学习WeakMap的时候我讲到了后面通过WeakMap如何管理这种响应式的数据依赖:

image-20220817192552487

  • 为什么外面一层用weakMap对象呢?
    • 如果用map,某个对象置为null时,map对象中的key这里还有个强引用指向之前的对象,就不会被销毁
  • 为什么里面的不用WeakMap对象呢?
    • 因为WeakMapkey只能是对象

7.对象依赖管理的实现

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

8.正确的依赖收集

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

9.对Depend重构

  • 但是这里有两个问题:
    • 问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次
    • 问题二:我们并不希望将添加reactiveFn放到get中,以为它是属于Dep的行为
  • 所以我们需要对Depend类进行重构:
    • 解决问题一的方法:不使用数组,而是使用Set
    • 解决问题二的方法:添加一个新的方法,用于收集依赖
image-20220817193207793

10.创建响应式对象

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

11.Vue响应式原理

  • 我们前面所实现的响应式的代码,其实就是Vue3中的响应式原理:
    • Vue3主要是通过Proxy来监听数据的变化以及收集相关的依赖的
    • Vue2中通过我们前面学习过的Object.defineProerty的方式来实现对象属性的监听
  • 我们可以将reactive函数进行如下的重构:
    • 在传入对象时,我们可以遍历所有的key,并且通过属性存储描述符来监听属性的获取和修改
    • settergetter方法中的逻辑和前面的Proxy是一致的

12.代码实现

  • Vue2

    js
        // const obj = {
        //   name: 'later',
        //   age: 18
        // }
    
        // const obj2 = {
        //   name: 'later',
        //   age: 22
        // }
    
        let dependFn = null // 用来记录当前传入的函数,方便get操作的时候,对其进行收集
        function watch(fn) {
          dependFn = fn
          fn() // 执行一次,把对应的依赖添加到对应的依赖收集对象中
          dependFn = null
        }
    
        class Deped {
          constructor() {
            this.dependFns = new Set() // 使用Set 减少某个依赖函数中因为多次访问某个属性,而导致添加多个相同的依赖函数
          }
          // addDepend(fn) {
          //   if(fn) this.dependFns.push(fn)
          // }
          addDepend() {
            // if(dependFn) this.dependFns.push(dependFn)
            if(dependFn) this.dependFns.add(dependFn) // Set 使用 add
          }
          notify() {
            this.dependFns.forEach(fn => fn())
          }
        }
    
        /**
          * 1.dep对象数据结构的管理(最难理解)
            * 每一个对象的每一个属性都会对应一个dep对象
            * 同一个对象的多个属性的dep对象是存放一个map对象中
            * 多个对象的map对象, 会被存放到一个objMap的对象中
          * 2.依赖收集: 当执行get函数, 自动的添加fn函数
        */
    
        // 为对象的每一个属性都建立一个单独的依赖收集数组(depend对象),方便获取该对象中的某个属性所对应的依赖数组
        // 对象中每个属性和其所对应的依赖收集数组,这种映射关系通过一个map对象来管理
        // 然后所有对象的map对象由一个objMap来管理
        const objMap = new WeakMap() // 通过弱引用,后续某个对象置为null时,不会对之前的对象保持强引用,会自动销毁掉
        function getDepend(obj, key) {
          // 1.根据对象obj, 找到对应的map对象
          let map = objMap.get(obj)
          // 判断某个属性在objMap中是否有其对应的map,没有则创建
          if (!map) {
            map = new Map()
            objMap.set(obj, map)
          }
    
          // 2.根据key, 找到对应的depend对象
          let dep = map.get(key)
          // 判断某个属性在map中是否有其对应的depend对象,没有则创建
          if (!dep) {
            dep = new Deped()
            map.set(key, dep)
          }
          return dep
        }
    
        function reactive(obj) {
          Object.keys(obj).forEach(key => {
            let value = obj[key]
            Object.defineProperty(obj, key, {
              get: () => { // 获取属性的时候,不需要调用该属性所对应的依赖函数,只需要在访问的时候,执行添加依赖的处理
                const deps = getDepend(obj, key)
                // deps.addDepend(dependFn)
                deps.addDepend() // 减少deps对外部变量的引用 上面写法也是欧克的
                return value
              },
              set: newValue => { // 只有设置属性的时候,才需要调用该属性所对应的依赖函数 进行更新处理
                value = newValue
                const deps = getDepend(obj, key)
                deps.notify()
              }
            })
          })
          return obj
        }
    
        const obj = reactive({
          name: 'later',
          age: 18
        })
        const obj2 = reactive({
          name: 'later',
          age: 22
        })
        function foo1() {
          console.log('foo1------')
          console.log(obj.name)
        }
        function foo2() {
          console.log('foo2------')
          console.log(obj.age)
        }
        function foo3() {
          console.log('foo3------')
          console.log(obj.name, obj.age)
          console.log(obj2.age)
        }
        watch(foo1)
        watch(foo2)
        watch(foo3)
    
        console.log('----------------------')
        // obj.name = 'hehe'
        // obj.name
        obj2.age = 20
    • Vue3
    js
      // const obj = {
      //   name: 'later',
      //   age: 18
      // }
    
      // const obj2 = {
      //   name: 'later',
      //   age: 22
      // }
    
      let dependFn = null // 用来记录当前传入的函数,方便get操作的时候,对其进行收集
      function watch(fn) {
        dependFn = fn
        fn() // 执行一次,把对应的依赖添加到对应的依赖收集对象中
        dependFn = null
      }
    
      class Deped {
        constructor() {
          this.dependFns = new Set() // 使用Set 减少某个依赖函数中因为多次访问某个属性,而导致添加多个相同的依赖函数
        }
        // addDepend(fn) {
        //   if(fn) this.dependFns.push(fn)
        // }
        addDepend() {
          // if(dependFn) this.dependFns.push(dependFn)
          if(dependFn) this.dependFns.add(dependFn) // Set 使用 add
        }
        notify() {
          this.dependFns.forEach(fn => fn())
        }
      }
    
      /**
          * 1.dep对象数据结构的管理(最难理解)
            * 每一个对象的每一个属性都会对应一个dep对象
            * 同一个对象的多个属性的dep对象是存放一个map对象中
            * 多个对象的map对象, 会被存放到一个objMap的对象中
          * 2.依赖收集: 当执行get函数, 自动的添加fn函数
        */
    
      // 为对象的每一个属性都建立一个单独的依赖收集数组(depend对象),方便获取该对象中的某个属性所对应的依赖数组
      // 对象中每个属性和其所对应的依赖收集数组,这种映射关系通过一个map对象来管理
      // 然后所有对象的map对象由一个objMap来管理
      const objMap = new WeakMap() // 通过弱引用,后续某个对象置为null时,不会对之前的对象保持强引用,会自动销毁掉
      function getDepend(obj, key) {
        // 1.根据对象obj, 找到对应的map对象
        let map = objMap.get(obj)
        // 判断某个属性在objMap中是否有其对应的map,没有则创建
        if (!map) {
          map = new Map()
          objMap.set(obj, map)
        }
    
        // 2.根据key, 找到对应的depend对象
        let dep = map.get(key)
        // 判断某个属性在map中是否有其对应的depend对象,没有则创建
        if (!dep) {
          dep = new Deped()
          map.set(key, dep)
        }
        return dep
      }
    
      function reactive(obj) {
        const objProxy = new Proxy(obj, {
          get: (target, key, receiver) => {
            const deps = getDepend(obj, key)
            deps.addDepend()
            return Reflect.get(target, key, receiver)
          },
          set: (target, key, newValue, receiver) => {
            // target[key] = newValue // 外部操作的是proxy对象,内部设置的是源对象,所以不会产生递归
            Reflect.set(target, key, newValue, receiver)
            const deps = getDepend(obj, key)
            deps.notify()
          }
        })
        return objProxy
      }
    
      const obj = reactive({
        name: 'later',
        age: 18
      })
      const obj2 = reactive({
        name: 'later',
        age: 22
      })
      function foo1() {
        console.log('foo1------')
        console.log(obj.name)
      }
      function foo2() {
        console.log('foo2------')
        console.log(obj.age)
      }
      function foo3() {
        console.log('foo3------')
        console.log(obj.name, obj.age)
        console.log(obj2.age)
      }
      watch(foo1)
      watch(foo2)
      watch(foo3)
    
      console.log('----------------------')
      // obj.name = 'hehe'
      // obj.name
      obj2.age = 20

13. nextTick

  • nextTick函数接收一个callback,该callback会在DOM下次更新完成时回调

  • nextTick传入的回调函数是宏任务or微任务:

    • Vue2中因版本不同,有的版本是宏任务,有的是微任务

    • Vue3中的是微任务(有传入回调函数的话,是放入到Promise.then方法中)

      typescript
      	// Vue3中的源码
      	export function nextTick<T = void>(
          this: T,
          fn?: (this: T) => void
        ): Promise<void> {
          const p = currentFlushPromise || resolvedPromise
          return fn ? p.then(this ? fn.bind(this) : fn) : p
        }

Released under the MIT License.