4.3 设计一个完善的响应式系统

4.3 设计一个完善的响应式系统

关键词

响应系统的工作流程:

  • 读取操作发生时,将副作用函数收集到“桶”中。
  • 设置操作发生时,从“桶”中取出副作用函数并执行。
  • activeEffect 全局变量当做中介存储副作用函数。
  • 重新定义effect函数 =>变成了注册副作用函数的函数

笔记

改善4.2缺陷逻辑
  1. 改善4.2中的缺陷,希望副作用函数的名字改变或者是匿名函数都能正常收集到Set存储桶中。因此需要提供一个注册副作用函数的机制
  2. 副作用函数已存储到activeEffect中,所以get拦截函数应该把activeEffect收集到Set存储桶中,如此响应式系统就不依赖副作用函数的名字了。
// 用一个全局变量存储被注册的副作用函数
let activeEffect
// effect 函数用于注册副作用函数
// 参数fn 就是将要注册的副作用函数
function effect(fn) {
// 当调用effect 注册副作用函数时,将副作用函数fn 赋值给actvieEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}
// 使用一个匿名的副作用函数作为 effect 函数的参数
// 首先会把匿名的副作用函数 fn 赋值给全局变量 activeEffect
// 接着执行被注册的匿名副作用函数 fn,会触发响应式数据 obj.text 的读取操作,进而触发代理对象 Proxy 的 get 拦截函数
effect(
   // 一个匿名的副作用函数
   () => {
     document.body.innerText = obj.text
   }
)
代理对象改动
// 代理对象 Proxy
const obj = new Proxy(data, {
  get(target, key) {
    // 将 activeEffect 中存储的副作用函数收集到“桶”中
    if (activeEffect) {  // 新增
      bucket.add(activeEffect)  // 新增
    }  // 新增
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    bucket.forEach(fn => fn())
    return true
  }
})
修改后存在问题 :不存在属性依然执行

当我们修改响应式数据obj 不存在的一个属性的时候,匿名副作用函数依然会执行

effect(
  // 一个匿名的副作用函数
  () => {
    console.log('effct run') // 会打印两次
    document.body.innerText = obj.text;
  }
);

setTimeout(()=>{
  obj.noExist = 'hello vue3'
})
完整代码 demo (如下)
const bucket = new Set();
const data = { text: "hello world" };

// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 函数用于注册副作用函数
// 参数fn 就是将要注册的副作用函数
function effect(fn) {
  // 当调用effect 注册副作用函数时,将副作用函数fn 赋值给actvieEffect
  activeEffect = fn;
  // 执行副作用函数
  fn();
}

// 代理对象 Proxy
const obj = new Proxy(data, {
  get(target, key) {
    // 将 activeEffect 中存储的副作用函数收集到“桶”中
    if (activeEffect) {// 新增
      bucket.add(activeEffect); // 新增
    } // 新增
    return target[key];
  },
  set(target, key, newVal) {
    target[key] = newVal;
    bucket.forEach((fn) => fn());
    return true;
  }
});

// 使用一个匿名的副作用函数作为 effect 函数的参数
// 首先会把匿名的副作用函数 fn 赋值给全局变量 activeEffect
// 接着执行被注册的匿名副作用函数 fn,会触发响应式数据 obj.text 的读取操作,进而触发代理对象 Proxy 的 get 拦截函数
effect(
  // 一个匿名的副作用函数
  () => {
    console.log('effct run') // 会打印两次
    document.body.innerText = obj.text;
  }
);

setTimeout(()=>{
  obj.noExist = 'hello vue3'
})

根本原因:没有在副作用函数与被操作的目标字段之间建立明确的联系

也就是说,当我们读取或者设置时,无论操作哪一个属性,那么get和set方法都会执行

解决方案: 修改数据结构

观察代码,并思考如何设计数据结构。(树型结构)

effect(function effectFn() {
  document.body.innerText = obj.text
})

在这段代码中存在三个角色:

  • 被操作(读取)的代理对象 obj
  • 被操作(读取)的字段名 text
  • 使用 effect 函数注册的副作用函数 effectFn
树形结构(三种情况举例)

树形结构

使用WeakMap 代替Set作为桶的数据结构
// 存储副作用函数的桶
const bucket = new WeakMap()
修改get/set 拦截器
const obj = new Proxy(data,{
  get(target,key){
    // 没有activeEffect直接return
    if(!activeEffect){
      return target[key]
    }
    // 根据target 从WeakMap桶中取得depsMap,它也是一个Map类型:key-->effects
    let depsMap = bucket.get(target)
    // 如果不存在depsMap 则新建一个Map与target关联
    if(!depsMap) {
      bucket.set(target,(depsMap = new Map()))
    }
    // 根据key 从 depsMap 中取得deps,它是一个Set类型
    // 里面存储这所有与当前Key相关联的副作用函数effects
    let deps = depsMap.get(key)
    // 如果deps不存在 同样新建一个Set并关联
    if(!deps) {
      depsMap.set(key,(deps = new Set()))
    }
    // 最后将当前激活的副作用函数添加到WeakMap 桶里
    deps.add(activeEffect)
    // 返回属性值
    return target[key]
  },
  set(target,key,newVal){
   // 设置属性值
    target[key] = newVal
    // 根据target 从WeakMap桶中取得depsMap 它是key -->effects
    const depsMap = bucket.get(target)
    if(!depsMap) { return }
    //根据key 获取所有副作用函数effects
    const effects = depsMap.get(key)
    // 执行副作用函数
    effects && effects.forEach(fn=>fn())
  }
})

WeakMap、Map 和 Set 之间的关系

WeakMap、Map 和 Set 之间的关系

为什么要使用 WeakMap(WeakMap与Map的区别)

WeakMap 对 key 是弱引用,不影响垃圾回收机制,不会造成内存泄露,自动回收。

立即执行函数执行完毕后,Map的key foo 依然保持着对 foo 对象的引用,而WeakMap 的key bar 属性就不能访问了。

更多参见底部延伸阅读

const map = new Map();
const weakmap = new WeakMap();
(function(){
    const foo = {foo: 1};
    const bar = {bar: 2};
    map.set(foo, 1);
    weakmap.set(bar, 2);
})()
优化代码 封装内部逻辑 trigger 触发 track 追踪

代码优化,封装get和set内把副作用函数放入桶内和执行副作用函数的逻辑

const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    trigger(target, key)
  }
})
// 在 get 拦截函数内调用 track 函数追踪变化
function track(target, key) {
  // 没有 activeEffect,直接 return
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
}
// 在 set 拦截函数内调用 trigger 函数触发变化
function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(fn => fn())
}

延伸阅读

阮一峰 Es6 WeakMap

张鑫旭 JS WeakMap应该什么时候使用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值