Vue3&Vue2响应式实现

71 阅读5分钟

Vue官方响应式图解

image.png

前言

说到Vue的响应式,这恐怕是我们前端人员在面试的时候必问的问题了吧,还记得今年我刚大三暑假出来面试的时候,面试官A问:说说你对Vue响应式的理解,3和2两者有什么区别,当时我心里就想这你***的什么呀,laozi都没听过

当然这个面试也是凉凉了,花了一天学了一下到底什么是响应式?经过一番研究,原来是这么回事....又一次的面试被问到这个问题,也是对答如流,直接手写出来,也顺利的拿下了offer

那今天咱们就来认识认识什么是响应式

一、什么是响应式?

看一下代码,当我们修改num的值时,想要对应的执行某一个引用num的函数。就像Vue中,修改了data里的属性时,对应的computed也会发生改变。上面的这样一种可以自动响应数据变化的代码机制,我们就称之为是响应式

let num = 3;
 
function changeNum() {
    
    console.log('num被响应式的改变了', num);
    
}
 
num = 10;

二、怎么实现一个简单的响应式?

1、我们以Vue3的Proxy为例,Vue2实现方法在最下面,二者差异不大

    const obj = {
      name: '张三',
      age: 19
    }
 
    // 类具有更好的封装性
    class Depend {
      constructor() {
        //这里使用Set是为了解决在watch函数中多次引用同一个属性会创建多个相同函数的问题,因为Set存储的数据是不重复的
        this.reactives = new Set()
      }
 
      addDepend(fn) {
        this.reactives.add(fn)
      }
 
      notify() {
        this.reactives.forEach(fn => fn && fn())
      }
    }
 
    //定义一个监听对应函数变化的函数, 形参fn接收变化的函数
    const depend = new Depend()
    function watchFn(fn) {
      depend.addDepend(fn)
    }
 
    //这里不限于使用箭头函数,匿名函数,命名函数...都可以
    watchFn(() => {
      console.log(`${obj.name}被响应了`)
    })
 
    watchFn(() => {
      console.log(`${obj.age}被响应了`)
    })
 
    depend.notify()
 
    setTimeout(() => {
      obj.name = "curry"
      depend.notify()
    }, 3000);

image.png

此时我们就实现了一个简单的响应式,但是这里有一个问题,我们每次修改obj属性时,得手动去调用notify(),这有点不太响应式...

2、此时可以将上面的代码使用Proxy代理监听属性的变化,自动执行notify()

但是我们看到控制台的打印结果,我们明明只是对name属性进行了修改,但是age的响应函数也被执行了,我们应该让每一个属性对应一个depend依赖

    const obj = {
      name: '张三',
      age: 19
    }
 
    // 类具有更好的封装性
    class Depend {
      constructor() {
        //这里使用Set是为了解决在watchFn函数中多次引用同一个属性会创建多个相同函数的问题,因为Set存储的数据是不重复的
        this.reactives = new Set()
      }
 
      addDepend(fn) {
        this.reactives.add(fn)
      }
 
      notify() {
        this.reactives.forEach(fn => fn && fn())
      }
    }
    const objProxy = new Proxy(obj, {
      get(target, key) {
        // 不了解Reflect可以去mdn看看 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
        return Reflect.get(target, key)
      },
      set(target, key, newValue) {
        Reflect.set(target, key, newValue)
        depend.notify()
      }
    })
    //定义一个监听对应函数变化的函数, 形参fn接收变化的函数
    const depend = new Depend()
    function watchFn(fn) {
      depend.addDepend(fn)
    }
 
    //这里不限于使用箭头函数,匿名函数,命名函数...都可以
    watchFn(() => {
      console.log(`${objProxy.name}被响应了`)
    })
 
    watchFn(() => {
      console.log(`${objProxy.age}被响应了`)
    })
 
    depend.notify()
 
    setTimeout(() => {
      objProxy.name = "curry"
    }, 3000);
    
    

image.png

3、接下来我们来实现属性对应独立的依赖的方法

    const obj = {
      name: '张三',
      age: 19
    }
 
    // 类具有更好的封装性
    class Depend {
      constructor() {
        //这里使用Set是为了解决在watch函数中多次引用同一个属性会创建多个相同函数的问题,因为Set存储的数据是不重复的
        this.reactives = new Set()
      }
 
      addDepend(fn) {
        this.reactives.add(fn)
      }
 
      notify() {
        this.reactives.forEach(fn => fn && fn())
      }
    }
 
    // 获取依赖, 根据第一个参数对象拿到map,再根据key去获取依赖
    const weakMap = new WeakMap()
    function getDepend(target, key) {
      let map = weakMap.get(target)
      // 在第一次获取的时候肯定是没有对应的依赖的
      if (!map) {
        map = new Map()
        weakMap.set(target, map)
      }
 
      let depend = map.get(key)
      if (!depend) {
        depend = new Depend()
        map.set(key, depend)
      }
 
      return depend
    }
 
    const objProxy = new Proxy(obj, {
      get(target, key) {
        // 不了解Reflect可以去mdn看看 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
 
        // 在每个响应式函数中获取的obj属性都会先执行get,可以获取对应的依赖
        const depend = getDepend(target, key)
        depend.addDepend(reFn)
        return Reflect.get(target, key)
      },
      set(target, key, newValue) {
        Reflect.set(target, key, newValue)
        // 当我们在修改值时,获取对应的依赖,调用notify 执行对应的函数即可 
        const depend = getDepend(target, key)
        depend.notify()
      }
    })
    //定义一个监听对应函数变化的函数, 形参fn接收变化的函数
    // const depend = new Depend()
    let reFn = null;
    function watchFn(fn) {
      reFn = fn
      fn()
      reFn = null
    }
 
    //这里不限于使用箭头函数,匿名函数,命名函数...都可以
    watchFn(() => {
      console.log(`${objProxy.name}被响应了`)
    })
 
    watchFn(() => {
      console.log(`${objProxy.age}被响应了`)
    })
 
    setTimeout(() => {
      objProxy.name = "curry"
    }, 3000);

此时我们修改name,就只会执行对应的name依赖,age的响应函数就不会被执行了

image.png

4、但是如果我们有多个对象,就要去再重新写一下new Proxy ,我们换一种封装的方法,将Proxy也封装起来

    // 类具有更好的封装性
    class Depend {
      constructor() {
        //这里使用Set是为了解决在watch函数中多次引用同一个属性会创建多个相同函数的问题,因为Set存储的数据是不重复的
        this.reactives = new Set()
      }
 
      addDepend(fn) {
        this.reactives.add(fn)
      }
 
      notify() {
        this.reactives.forEach(fn => fn && fn())
      }
    }
 
    // 获取依赖, 根据第一个参数对象拿到map,再根据key去获取依赖
    const weakMap = new WeakMap()
    function getDepend(target, key) {
      let map = weakMap.get(target)
      // 在第一次获取的时候肯定是没有对应的依赖的
      if (!map) {
        map = new Map()
        weakMap.set(target, map)
      }
 
      let depend = map.get(key)
      if (!depend) {
        depend = new Depend()
        map.set(key, depend)
      }
 
      return depend
    }
    function reactive(obj) {
      return new Proxy(obj, {
        get(target, key) {
          // 不了解Reflect可以去mdn看看 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
 
          // 在每个响应式函数中获取的obj属性都会先执行get,可以获取对应的依赖
          const depend = getDepend(target, key)
          depend.addDepend(reFn)
          return Reflect.get(target, key)
        },
        set(target, key, newValue) {
          Reflect.set(target, key, newValue)
          // 当我们在修改值时,获取对应的依赖,调用notify 执行对应的函数即可 
          const depend = getDepend(target, key)
          depend.notify()
        }
      })
    }
 
    const obj = {
      name: '张三',
      age: 19
    }
 
    const objProxy = reactive(obj)
    //定义一个监听对应函数变化的函数, 形参fn接收变化的函数
    // const depend = new Depend()
    let reFn = null;
    function watchFn(fn) {
      reFn = fn
      fn()
      reFn = null
    }
 
    //这里不限于使用箭头函数,匿名函数,命名函数...都可以
    watchFn(() => {
      console.log(`${objProxy.name}被响应了`)
    })
 
    watchFn(() => {
      console.log(`${objProxy.age}被响应了`)
    })
 
    setTimeout(() => {
      objProxy.name = "curry"
    }, 3000);
 
    const infoProxy = reactive({
      address: "金州"
    })
 
    watchFn(() => {
      console.log(`我的家在${infoProxy.address}`)
    })
 
    setTimeout(() => {
      infoProxy.address = "勇士"
    }, 5000);
  

此时看到reactive是不是想到了Vue3中 import { reactive } from 'vue' 没错,就是它,以上就是reactive的实现过程

5、Vue2的方式,就是采用Object.defineProperty。 其实这个api在设计时并不是为了响应式而生的,所以我们还是应该使用Proxy

    // 类具有更好的封装性
    class Depend {
      constructor() {
        //这里使用Set是为了解决在watch函数中多次引用同一个属性会创建多个相同函数的问题,因为Set存储的数据是不重复的
        this.reactives = new Set()
      }
 
      addDepend(fn) {
        this.reactives.add(fn)
      }
 
      notify() {
        this.reactives.forEach(fn => fn && fn())
      }
    }
 
    // 获取依赖, 根据第一个参数对象拿到map,再根据key去获取依赖
    const weakMap = new WeakMap()
    function getDepend(target, key) {
      let map = weakMap.get(target)
      // 在第一次获取的时候肯定是没有对应的依赖的
      if (!map) {
        map = new Map()
        weakMap.set(target, map)
      }
 
      let depend = map.get(key)
      if (!depend) {
        depend = new Depend()
        map.set(key, depend)
      }
 
      return depend
    }
    function reactive(obj) {
      Object.keys(obj).forEach(key => {
        let value = obj[key]
        Object.defineProperty(obj, key, {
          get() {
            // 在每个响应式函数中获取的obj属性都会先执行get,可以获取对应的依赖
            const depend = getDepend(obj, key)
            depend.addDepend(reFn)
            return value
          },
          set(newValue) {
            value = newValue
            // 当我们在修改值时,获取对应的依赖,调用notify 执行对应的函数即可 
            const depend = getDepend(obj, key)
            depend.notify()
          }
        })
      })
      return obj
 
    }
 
    const obj1 = {
      name: '张三',
      age: 19
    }
 
    const objProxy = reactive(obj1)
    //定义一个监听对应函数变化的函数, 形参fn接收变化的函数
    // const depend = new Depend()
    let reFn = null;
    function watchFn(fn) {
      reFn = fn
      fn()
      reFn = null
    }
 
    //这里不限于使用箭头函数,匿名函数,命名函数...都可以
    watchFn(() => {
      console.log(`${objProxy.name}被响应了`)
    })
 
    watchFn(() => {
      console.log(`${objProxy.age}被响应了`)
    })
 
    setTimeout(() => {
      objProxy.name = "curry"
    }, 3000);
 
    const infoProxy = reactive({
      address: "金州"
    })
 
    watchFn(() => {
      console.log(`我的家在${infoProxy.address}`)
    })
 
    setTimeout(() => {
      infoProxy.address = "勇士"
    }, 5000);
    
    

致辞,实现完毕,有疑问或者建议欢迎下方留言