JavaScript手写响应式原理(详解)

本文深入浅出地介绍了响应式编程的基本原理,从简单的数组管理到使用 Proxy 和 WeakMap 实现对象变化的自动监听,再到依赖收集机制的实现,最后对比了 Vue2 和 Vue3 的响应式实现方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

响应式原理

首先我们有一个对象

  const obj = {
   name: 'zlk',
   age: 18
  }

这个对象可能在别处被用到

比如是这样的

function foo() {
   const newValue = obj.name
   console.log('hello world');
   console.log(obj.name);
  }

我们来改变obj对象中的name的值

obj.name = 'zlk'

这时候foo()应该被重新执行

那么我们如何让所以类似foo()的响应式重新执行一次呢

响应式函数封装

  watchFn(function foo() {
   const newValue = obj.name
   console.log('hello world');
   console.log(obj.name);
  })

我们再声明个数组

const reactiveFn = []

将响应式的函数push到数组中

  function watchFn(fn) {

   reactiveFn.push(fn)

  }

循环执行这个数组中的函数

  reactiveFn.forEach(fn => {

   fn()

  })

完整代码

const reactiveFn = []
function watchFn(fn) {
  reactiveFn.push(fn)
}
const obj = {
  name: 'zlk',
  age: 18
}
watchFn(function foo() {
  const newValue = obj.name
  console.log('hello world');
  console.log(obj.name);
})
watchFn(function demo() {
  console.log('响应式--2--');
})

function bar() {
  console.log('普通函数,无响应式');
}
reactiveFn.forEach(fn => {
  fn()
})

obj.name = 'zlk'

二,依赖收集类的封装

上面我们将需要响应的函数放进了数组里面,上面name发生改变需要重新执行函数,可是如果是age呢,如果还有别的响应式对象呢,频繁建数组?那很不方便管理

优点1:每一个属性对应一个类,每个类对应一个数组

优点2:我们可以在累里封装一个函数,遍历所有响应函数

重构

 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)

  }


  const obj = {

   name: 'zlk',

   age: 18

  }


  watchFn(function foo() {

   console.log('hello world');

   console.log(obj.name);

  })

  watchFn(function demo() {

   console.log('响应式--2--');

  })


  function bar() {

   console.log('普通函数,无响应式', obj.name);

  }


  obj.name = 'zlk'

  depend.notify()


  objProxy.name = 'zlk'

depend.notify()

自动监听对象变化

可是我们不能改一个对象中的值 depend.notify()一下

我们要让值被修改后,自动depend.notify(),执行响应式函数

所以我们用到了Proxy代理,当值被修改自动执行响应式函数

完整代码

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


    const obj = {
      name: 'zlk',
      age: 18
    }
    const objProxy = new Proxy(obj, {
      get(target, key, receiver) {
        return Reflect.get(target, key, receiver)
      },
      set(target, key, newValue, receiver) {
        Reflect.set(target, key, newValue, receiver)
        depend.notify()
      }
    })

    watchFn(function foo() {
      console.log('hello world');
      console.log(objProxy.name);
    })
    watchFn(function demo() {
      console.log('响应式--2--');
    })
    watchFn(function demo2() {
      console.log('响应式--2--', objProxy.age);
    })



    function bar() {
      console.log('普通函数,无响应式', objProxy.name);
    }


    objProxy.name = 'zlk'
    objProxy.name = 'aaa'
    objProxy.name = 'bbb'
    // depend.notify()
  </script>

依赖收集如何管理

可是我们又发现无论你修改name 还是age,所有响应式函数都会被执行

这不是我们想要的,我们的目的是修改name 执行用到name的函数自动执行,修改age执行用到age的响应函数,为什么呢,因为他们只有一个depend对象,没有进行区分。我们使用map wekmap来区分

我们封装一个获取depend函数,这里的数据结构是这样的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-612hSbze-1672193482993)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20221228094317352.png)]

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

  //获取depend函数

  const targetMap = new WeakMap()

  function getDepend(target, key) {

   let map = targetMap.get(target)

   if (!map) {

​    map = new Map()

​    targetMap.set(target, map)

   }


   let depend = map.get(key)

   if (!depend) {

​    depend = new Depend()

​    map.set(key, depend)

   }

   return depend

  }



  const obj = {

   name: 'zlk',

   age: 18

  }


  //自动监听收集依赖

  const objProxy = new Proxy(obj, {

   get(target, key, receiver) {return Reflect.get(target, key, receiver)

   },

   set(target, key, newValue, receiver) {

​    Reflect.set(target, key, newValue, receiver)

​    console.log(target, key);const depend = getDepend(target, key)

​    console.log(depend.reactiveFns);

​    depend.notify()

   }

  })



  watchFn(function foo() {

   console.log('hello world');

   console.log(objProxy.name);

  })

  watchFn(function demo() {

   console.log('响应式--2--');

  })

  watchFn(function demo2() {

   console.log('响应式--3--', objProxy.age);

  })





  function bar() {

   console.log('普通函数,无响应式', objProxy.name);

  }


  objProxy.name = 'zlk'

  objProxy.name = 'aaa'

  objProxy.name = 'bbb'

map 和weakMap 响应式 知识补充,方便理解上面代码,如果上面不理解,先把下面这段搞懂再去理解上面代码

 const obj1 = {

   name: "why",

   age: 18

  }



  const obj2 = {

   name: "why",

   age: 18

  }



  // 1.创建WeakMap

  const weakMap = new WeakMap();



  // 2. 收集依赖结构

  // 2.1 使用map来收集

  const obj1Map = new Map();

  obj1Map.set("name", [obj1GetName, obj1SetName])

  obj1Map.set("age", [obj1GetAge, obj1SetAge])

  weakMap.set(obj1, obj1Map)




  // 3.如果obj1.name发生改变

  // Proxy/Object.defineProperty

  obj1.name = "test";

  const targetMap = weakMap.get(obj1);

  const fns = targetMap.get("name")

  fns.forEach(item => item())



  function obj1GetName() {

   console.log("obj1GetName")

  }



  function obj1SetName() {

   console.log("obj1SetName")

  }


  function obj1GetAge() {

   console.log("obj1GetAge")

  }


  function obj1SetAge() {

   console.log("obj1SetAge")

  }

最终完整收集依赖

你发现上面depend.reactiveFns打印出来为[ ]了吗?

我们现在想如何把响应函数自动放到数组里,然后去执行

proxy的get里面写

const depend = getDepend(target, key)

depend.addDepend(dependFN)

一步步debugger,你会了解整个流程,我这边简单说一下帮助理解

你先执行watchFn函数,将响应式函数传入,途中将函数赋值给全局变量,传入后执行响应函数代码块,因为里面用到代理监听对象,所以它会去往proxy中的get中,将去创建自己的depend对象,创建好后,将全局变量中的响应函数代码块放入创建类里面数组中,之后如果你设置修改了值,将去往proxy中set方法重新执行数组中响应函数。

 class Depend {

   constructor() {this.reactiveFns = []  

   }

   addDepend(reactiveFn) {debuggerthis.reactiveFns.push(reactiveFn)

   }

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

   }

  }



  let dependFN = null //全局变量

  function watchFn(fn) {

   debugger

   dependFN = fn  //将响应函数放到全局变量里,让proxy中的get中可以获取到

   fn() //执行响应函数
  }





  //获取depend函数

  const targetMap = new WeakMap()

  function getDepend(target, key) {

   debugger

   let map = targetMap.get(target)

   if (!map) {

​    map = new Map()

​    targetMap.set(target, map)

   }



   let depend = map.get(key)

   if (!depend) {

​    depend = new Depend()

​    map.set(key, depend)

   }

   return depend

  }



  const obj = {

   name: 'zlk',

   age: 18

  }





  //自动监听收集依赖

  const objProxy = new Proxy(obj, {

   get(target, key, receiver) {debuggerconst depend = getDepend(target, key)

​    console.log(depend);

​    depend.addDepend(dependFN)return Reflect.get(target, key, receiver)

   },

   set(target, key, newValue, receiver) {debugger

​    Reflect.set(target, key, newValue, receiver)const depend = getDepend(target, key)

​    depend.notify()

   }

  })



  watchFn(function foo() {

   debugger

   console.log('响应式1name', objProxy.name, objProxy.age);

  })



  watchFn(function demo2() {

   console.log('响应式2age', objProxy.age);

  })





  // function bar() {

  //  console.log('普通函数,无响应式', objProxy.name);

  // }





  objProxy.name = 'zhf'



  console.log('--------------------------');

  objProxy.age = 24

vue3响应式原理

1,因为上面我们只有一个对象obj ,可是如果有多个呢,你将要写多个代理?我们还是封装个函数吧,把代理封装到reactive函数中,这时候你是不是发现和vue3中的reactive一样了。

2,创建数组reactiveFns时我们改成了set,set和数组一样,只不过多了去重的功能。是为了如果有这样的代码执行,代码会被执行两次

watchFn(function foo() {
debugger
console.log(‘响应式1name–1’, objProxy.name);
console.log(‘响应式1name–2’, objProxy.name);
})
在这里插入图片描述

  class Depend {
      constructor() {
        this.reactiveFns = new Set()
      }
      addDepend(reactiveFn) {
        debugger
        this.reactiveFns.add(reactiveFn)
      }
      notify() {
        debugger
        this.reactiveFns.forEach(fn => {
          fn()
        })
      }
    }

    let dependFN = null
    function watchFn(fn) {
      debugger
      dependFN = fn
      fn()


    }


    //获取depend函数
    const targetMap = new WeakMap()
    function getDepend(target, key) {
      debugger
      let map = targetMap.get(target)
      if (!map) {
        map = new Map()
        targetMap.set(target, map)
      }

      let depend = map.get(key)
      if (!depend) {
        depend = new Depend()
        map.set(key, depend)
      }
      return depend
    }

    const objProxy = reactive({
      name: 'zlk',
      age: 18
    })
    //自动监听收集依赖
    function reactive(obj) {
      return new Proxy(obj, {
        get(target, key, receiver) {
          debugger
          const depend = getDepend(target, key)
          console.log(depend);
          depend.addDepend(dependFN)
          return Reflect.get(target, key, receiver)
        },
        set(target, key, newValue, receiver) {
          debugger
          Reflect.set(target, key, newValue, receiver)
          const depend = getDepend(target, key)
          depend.notify()
        }
      })
    }


    watchFn(function foo() {
      debugger
      console.log('响应式1name', objProxy.name);
    })

    watchFn(function demo2() {
      console.log('响应式2age', objProxy.age);
    })
    console.log('--------------------------改变后');


    // function bar() {
    //   console.log('普通函数,无响应式', objProxy.name);
    // }


    objProxy.name = 'zhf'

vue2响应式原理

proxy代理替换为object.defineProperty
在这里插入图片描述

次文章需要理解的api
proxy-Reflect
map和weakmap
class类封装构造函数

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值