首发于 林景宜的记事本
Vue3响应式原理

Vue3响应式原理

Vue3 的稳定版发布了很久了,去阅读官网的文档时发现推荐的一个优秀的 Vue 教程网站: Vue Mastery,里面的一篇 Vue 3 Reactivity(Vue3 响应式)讲的是真的不错。跟着学完收获很多,顺着课程的思路总结一篇 Vue3 响应式的笔记,手动还原响应式的原理。

手动实现响应式

响应式,就是一个变量依赖了其他变量,当被依赖的其他变量更新后该变量也要响应式的更新。所以从零开始,先实现手动的更新。

单个变量的手动响应

先看几个单词的意思,

再看手动实现的代码,这里用到了 Set,不熟悉的话可参考 MDN:

上述代码是最最简单的实现,流程就是三步:

  1. 通过 effect 来表明影响 total 的依赖
  2. 通过 track 来保存 effect
  3. 通过 trigger 来执行 effect

最后输出的 total 肯定就是计算后的 10

对对象的多个属性手动响应

上个例子中 pricequantity 都是放在了不同的变量里,现在更进一步,把他们放到同一个对象里 let product = { price: 5, quantity: 2 },现在如果想让 product 对象变为响应式,就需要指定每个键的响应。

depsMap 的意思就是 dependencemap。也就是一个 map 中,每个键都对应某个属性的 dep

这里用到了 Map,不熟悉的话可参考 MDN

在上述代码中,实现了对整个 product 对象的手动响应。

对多个对象的多个属性进行手动响应

继续升级上述代码,如果有多个对象需要响应式,那么就需要给不同的对象设置不同 depsMap。所以创建一个 WeakMap 类型的变量,命名为 targetMap 来存储多个对象的 depsMaptarget 就是指的需要被响应式的对象。

而之所以用 Map 类型是因为 Map 可以用“对象”作为键,用 WeakMap 方便对键(也就是被响应式的对象)进行垃圾回收。不熟悉 WeakMap 的话可参考 MDN

上述代码实现了对不同对象的不同键进行手动响应。到了这一步,可以用课程中的一张图来清楚的表示下 targetMapdepsMapdep 之间的关系:


变为自动响应

继续升级代码,给上述代码添加自动响应。在这里用到了 ProxyReflect, 不熟悉的话可参考 MDN。

const targetMap = new WeakMap();
function track(target, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  dep.add(effect);
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  let dep = depsMap.get(key);
  if (dep) {
    dep.forEach((effect) => {
      effect();
    });
  }
}
// 新增  
/**
 * @description: 例用Proxy和Reflect实现自动响应式
 * @param {Object} target 要响应的对象
 * @return {Proxy} 返回要响应对象的代理
 */
function reactive(target) {
  const handlers = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver);
      // 在访问这个target对象的key键之前,先把effect保存下
      track(target, key);
      return result;
    },
    set(target, key, value, receiver) {
      let oldValue = target[key];
      // 下面两步的顺序不能颠倒,很关键
      // 这一步其实就已经赋值成功了
      let result = Reflect.set(target, key, value, receiver);
      // 到这里再执行get时获取的是新设的值
      if (result && oldValue != value) {
        // 如果把这个target对象的key键的值改了,就得执行一遍对应的effect
        trigger(target, key);
      }
      return result;
    },
  };
  return new Proxy(target, handlers);
}
//  
let product = reactive({ price: 5, quantity: 2 });
let total = 0;

var effect = () => {
  total = product.price * product.quantity;
};

// 首次调用计算出total
effect();
console.log(total); // output: 10

// 注意:在这里与前面代码的不同就是我们没有手动调用trigger,实现的自动响应
product.quantity = 3;
console.log(total); // output: 15

这段代码实现了自动响应,最关键的核心部分就是 reactive 函数,它返回了一个 Proxy,代理了对 target 对象的存取。首先在在 get 返回之前,自动调用 trackeffect 存到对应的位置。

巧妙的地方是 set,当我们执行 product.quantity = 3; 时,会先将 quantity 设为 3,再自动触发 trigger。这时最关键的地方来了,trigger 调用了存储的对应的 effect,计算出最新的 total15,实现了自动响应。

优化自动响应过程

上述代码实现了自动响应,但是现在还有两个明显不如人意的地方:

  1. 不能设置多种 effect
  2. 在我们设置 quantity3 的时候,trigger 调用了对应的 effect,这里的 effect 函数执行来计算 total 时,会再走一遍 proxyget 的流程。所以就会触发 track 的流程,但是这里我们并不需要触发 track 再保存一遍 effect

下面来优化上述两个问题:

const targetMap = new WeakMap();
let activeEffect = null; //   新增,是否需要添加effect的标志

function track(target, key) {
  //   新增,只有再activeEffect为真时才执行保存的操作
  if (activeEffect) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
      depsMap.set(key, (dep = new Set()));
    }
    dep.add(activeEffect); //   修改,有直接添加effect改为了添加activeEffect
  }
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  let dep = depsMap.get(key);
  if (dep) {
    dep.forEach((effect) => {
      effect();
    });
  }
}

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver);
      track(target, key);
      return result;
    },
    set(target, key, value, receiver) {
      let oldValue = target[key];
      let result = Reflect.set(target, key, value, receiver);
      if (result && oldValue != value) {
        trigger(target, key);
      }
      return result;
    },
  };
  return new Proxy(target, handler);
}

//   新增
// 为了用统一的方式,把eff添加到对应的dep中,顺便还执行了一遍设置了初始值
// 这样以后,只有我们手动调用effect那次才会保存dep,用trigger触发的get就不会再保存一遍了
function effect(eff) {
  activeEffect = eff;
  activeEffect();
  activeEffect = null;
}
//  

let product = reactive({ price: 5, quantity: 2 });
let salePrice = 0;
let total = 0;

//   同理也要对effect函数改造,把每一个要保存的dep变为了effect函数的参数
// 手动设定了total呃salePrice的初始值
effect(() => {
  total = product.price * product.quantity;
});
// 所以这里就不会把这个eff添加给quantity
effect(() => {
  salePrice = product.price * 0.9;
});
//  

console.log(total, salePrice); // output: 10, 4.5

// 设置quantity只会重新计算total
product.quantity = 3;
console.log(total, salePrice); // output: 15, 4.5

// 设置price后,total和salePrice对应的effect都会执行,都会被重新计算
product.price = 10;
console.log(total, salePrice); // output: 30, 9

上述代码通过添加了一个 activeEffect 的标志位,解决了无效的重复执行保存的缺点;并把 effect() 变为 effect(eff) 的带参数形式,解决了多个 effect 的问题。

到这里为止,Vue3 Composition APIreactive 方法的实现流程已经被我们手动大致实现了一遍!

实现 ref

Vue3 Composition API 设计中,reactive 主要用于引用类型,另外专门提供了一个 ref 方法实现对原始类型的响应式。

大部分的地方都不用变,基本上就是添加了个 ref 方法,实现的方式就是对象访问器 getter/setter 来模仿 Proxy

const targetMap = new WeakMap();
let activeEffect = null;

function track(target, key) {
  if (activeEffect) {
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
      depsMap.set(key, (dep = new Set()));
    }
    dep.add(activeEffect);
  }
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  let dep = depsMap.get(key);
  if (dep) {
    dep.forEach((effect) => {
      effect();
    });
  }
}

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver);
      track(target, key);
      return result;
    },
    set(target, key, value, receiver) {
      let oldValue = target[key];
      let result = Reflect.set(target, key, value, receiver);
      if (result && oldValue != value) {
        trigger(target, key);
      }
      return result;
    },
  };
  return new Proxy(target, handler);
}

//   新增
/**
 * @description: ref使用getter和setter实现,模仿了Proxy的get和set
 * @param {Primary} raw
 * @return {Object} 返回响应对象
 */
function ref(raw) {
  const r = {
    get value() {
      // 在get之前,先保存到targetMap中
      track(r, 'value');
      return raw;
    },
    set value(newVal) {
      raw = newVal;
      // set了之后,触发effect更新
      trigger(r, 'value');
    },
  };
  return r;
}
//  

function effect(eff) {
  activeEffect = eff;
  activeEffect();
  activeEffect = null;
}

let product = reactive({ price: 5, quantity: 2 });
let salePrice = ref(0); //   修改 此时的salePrice自身也是个响应式对象
let total = 0;

// 此时的salePrice自身也是个响应式对象
effect(() => {
  salePrice.value = product.price * 0.9; //   修改
});

// 注意这里计算总价的方式变了,使用的是打折后的值来计算
effect(() => {
  total = salePrice.value * product.quantity; //   修改
});

console.log(total, salePrice); // output: 9, 4.5

product.quantity = 3;
console.log(total, salePrice); // output: 13.5, 4.5

product.price = 10;
console.log(total, salePrice); // output: 27, 9

其实可以发现,reactive 也能实现对原始类型的响应式,为什么还要专门提供一个 ref 方法?看对尤大的访谈中,尤大回答的是 reactive 还会添加更多处理流程,对于原始类型来说,是一种无用的负担。

实现 computed

跟响应式相关的最后一部分内容就是 computed 了,继续来手动实现它:

//   代码不变
const targetMap = new WeakMap();
let activeEffect = null;

function track(target, key) {
  if (activeEffect) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
      depsMap.set(key, (dep = new Set()));
    }
    dep.add(activeEffect);
  }
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  let dep = depsMap.get(key);
  if (dep) {
    dep.forEach((eff) => {
      eff();
    });
  }
}

function reactive(target) {
  const handler = {
    get(target, key, receiver) {
      let result = Reflect.get(target, key, receiver);
      track(target, key);
      return result;
    },
    set(target, key, value, receiver) {
      let oldValue = target[key];
      let result = Reflect.set(target, key, value, receiver);
      if (result && oldValue != value) {
        trigger(target, key);
      }
      return result;
    },
  };
  return new Proxy(target, handler);
}

function ref(raw) {
  const r = {
    get value() {
      track(r, 'value');
      return raw;
    },
    set value(newVal) {
      raw = newVal;
      trigger(r, 'value');
    },
  };
  return r;
}

function effect(eff) {
  activeEffect = eff;
  activeEffect();
  activeEffect = null;
}
//   代码不变

//   新增
/**
 * @description: computed实现,其实就是封装了ref
 * @param {Function} getter 取值函数
 * @return {Object} ref返回的对象
 */
function computed(getter) {
  // 创建一个响应式的引用
  let result = ref();
  // 用effect封装着调用getter,将结果设给result.value的同时,也将eff保存在了targetMap中的对应位置
  effect(() => (result.value = getter()));
  // 最后把result返回
  return result;
}
//  

let product = reactive({ price: 5, quantity: 2 });

//   修改
// 此时的salePrice自身也是一个响应式对象
let salePrice = computed(() => {
  return product.price * 0.9;
});

// total也是个响应式对象
let total = computed(() => {
  return salePrice.value * product.quantity;
});
//  

console.log(total.value, salePrice.value); // output: 9, 4.5

product.quantity = 3;
console.log(total.value, salePrice.value); // output: 13.5, 4.5

product.price = 10;
console.log(total.value, salePrice.value); // output: 27, 9

可以发现,computed 本质上就是封装了 ref 方法,用 effect 封装着来调用 getter,将结果设给 result.value 的同时,也将 eff 保存在了 targetMap 中的对应位置,实现了 computed 的响应式。

Vue3 源码中响应式的实现

Vue3 整体是用 Typescript 写的,reactivity 是一个独立的模块,源代码位于 packages/reactivity/src目录下,几个不同的方法分别位于不同文件中:


关于 Vue Mastery 课程

Vue Mastery 课程是收费的,25% 的收入会捐给 Vue 项目,所以大家对课程感兴趣的话可以开会员支持一波。不过它的会员很贵,可以有一些取巧的方法跳过收费验证,可以关注“林景宜的记事本”公众号发送“Vue3 响应式”获取方法试看一波。


前端记事本,不定期更新,欢迎关注!


深圳SEO优化公司保定优化推荐凉山建网站推荐福田关键词按天扣费南京企业网站建设报价松岗网站推广工具公司潮州设计网站多少钱海北seo排名报价辽源外贸网站建设多少钱承德百度竞价报价绍兴模板推广多少钱普洱网站关键词优化丹东企业网站制作多少钱凉山模板推广推荐昆明模板网站建设推荐大丰网络营销多少钱贵港百度竞价包年推广价格黑河关键词排名推荐乌海SEO按天收费迁安网站关键词优化推荐中山关键词排名包年推广推荐广安网站搭建哪家好南山百度竞价公司大运seo网站优化南联网站定制多少钱焦作百度网站优化排名价格仙桃优秀网站设计公司潜江优化公司辽源营销网站推荐钦州网站推广方案多少钱德阳网站推广工具公司歼20紧急升空逼退外机英媒称团队夜以继日筹划王妃复出草木蔓发 春山在望成都发生巨响 当地回应60岁老人炒菠菜未焯水致肾病恶化男子涉嫌走私被判11年却一天牢没坐劳斯莱斯右转逼停直行车网传落水者说“没让你救”系谣言广东通报13岁男孩性侵女童不予立案贵州小伙回应在美国卖三蹦子火了淀粉肠小王子日销售额涨超10倍有个姐真把千机伞做出来了近3万元金手镯仅含足金十克呼北高速交通事故已致14人死亡杨洋拄拐现身医院国产伟哥去年销售近13亿男子给前妻转账 现任妻子起诉要回新基金只募集到26元还是员工自购男孩疑遭霸凌 家长讨说法被踢出群充个话费竟沦为间接洗钱工具新的一天从800个哈欠开始单亲妈妈陷入热恋 14岁儿子报警#春分立蛋大挑战#中国投资客涌入日本东京买房两大学生合买彩票中奖一人不认账新加坡主帅:唯一目标击败中国队月嫂回应掌掴婴儿是在赶虫子19岁小伙救下5人后溺亡 多方发声清明节放假3天调休1天张家界的山上“长”满了韩国人?开封王婆为何火了主播靠辱骂母亲走红被批捕封号代拍被何赛飞拿着魔杖追着打阿根廷将发行1万与2万面值的纸币库克现身上海为江西彩礼“减负”的“试婚人”因自嘲式简历走红的教授更新简介殡仪馆花卉高于市场价3倍还重复用网友称在豆瓣酱里吃出老鼠头315晚会后胖东来又人满为患了网友建议重庆地铁不准乘客携带菜筐特朗普谈“凯特王妃P图照”罗斯否认插足凯特王妃婚姻青海通报栏杆断裂小学生跌落住进ICU恒大被罚41.75亿到底怎么缴湖南一县政协主席疑涉刑案被控制茶百道就改标签日期致歉王树国3次鞠躬告别西交大师生张立群任西安交通大学校长杨倩无缘巴黎奥运

深圳SEO优化公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化