4. 修改 track 和 trigger 函数
通过上面代码,我们已经实现一个简单 reactive()
函数,用来将普通对象转换为响应式对象。但是还缺少自动执行 track()
函数和 trigger()
函数,接下来修改上面代码:
const targetMap = new WeakMap(); let total = 0; const effect = () => { total = product.price * product.quantity }; const 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); } const trigger = (target, key) => { const depsMap = targetMap.get(target); if(!depsMap) return; let dep = depsMap.get(key); if(dep) { dep.forEach( effect => effect() ); } }; const reactive = (target) => { const handler = { get(target, key, receiver){ console.log('正在读取的数据:',key); const result = Reflect.get(target, key, receiver); track(target, key); // 自动调用 track 方法收集依赖 return result; }, set(target, key, value, receiver){ console.log('正在修改的数据:', key, ',值为:', value); const oldValue = target[key]; const result = Reflect.set(target, key, value, receiver); if(oldValue != result){ trigger(target, key); // 自动调用 trigger 方法执行依赖 } return result; } } return new Proxy(target, handler); } let product = reactive({price: 10, quantity: 2}); effect(); console.log(total); product.price = 20; console.log(total); // 正在读取的数据: price // 正在读取的数据: quantity // 20 // 正在修改的数据: price ,值为: 20 // 正在读取的数据: price // 正在读取的数据: quantity // 40
(图片来源:Vue Mastery)
三、activeEffect 和 ref
在上一节代码中,还存在一个问题: track
函数中的依赖( effect
函数)是外部定义的,当依赖发
生变化, track
函数收集依赖时都要手动修改其依赖的方法名。
比如现在的依赖为 foo
函数,就要修改 track
函数的逻辑,可能是这样:
const foo = () => { /**/ }; const track = (target, key) => { // ② // ... dep.add(foo); }
那么如何解决这个问题呢?
1. 引入 activeEffect 变量
接下来引入 activeEffect
变量,来保存当前运行的 effect 函数。
const foo = () => { /**/ }; const track = (target, key) => { // ② // ... dep.add(foo); }
然后在 track
函数中将 activeEffect
变量作为依赖:
const track = (target, key) => { if (activeEffect) { // 1. 判断当前是否有 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); // 2. 添加 activeEffect 依赖 } }
使用方式修改为:
effect(() => { total = product.price * product.quantity });
这样就可以解决手动修改依赖的问题,这也是 Vue3 解决该问题的方法。完善一下测试代码后,如下:
const targetMap = new WeakMap(); let activeEffect = null; // 引入 activeEffect 变量 const effect = eff => { activeEffect = eff; // 1. 将副作用赋值给 activeEffect activeEffect(); // 2. 执行 activeEffect activeEffect = null;// 3. 重置 activeEffect } const track = (target, key) => { if (activeEffect) { // 1. 判断当前是否有 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); // 2. 添加 activeEffect 依赖 } } const trigger = (target, key) => { const depsMap = targetMap.get(target); if (!depsMap) return; let dep = depsMap.get(key); if (dep) { dep.forEach(effect => effect()); } }; const reactive = (target) => { const handler = { get(target, key, receiver) { const result = Reflect.get(target, key, receiver); track(target, key); return result; }, set(target, key, value, receiver) { const oldValue = target[key]; const result = Reflect.set(target, key, value, receiver); if (oldValue != result) { trigger(target, key); } return result; } } return new Proxy(target, handler); } let product = reactive({ price: 10, quantity: 2 }); let total = 0, salePrice = 0; // 修改 effect 使用方式,将副作用作为参数传给 effect 方法 effect(() => { total = product.price * product.quantity }); effect(() => { salePrice = product.price * 0.9 }); console.log(total, salePrice); // 20 9 product.quantity = 5; console.log(total, salePrice); // 50 9 product.price = 20; console.log(total, salePrice); // 100 18
思考一下,如果把第一个 effect
函数中 product.price
换成 salePrice
会如何:
effect(() => { total = salePrice * product.quantity }); effect(() => { salePrice = product.price * 0.9 }); console.log(total, salePrice); // 0 9 product.quantity = 5; console.log(total, salePrice); // 45 9 product.price = 20; console.log(total, salePrice); // 45 18
得到的结果完全不同,因为 salePrice 并不是响应式变化,而是需要调用第二个 effect 函数才会变化,也就是 product.price 变量值发生变化。
代码地址: vue-3-reactivity/05-activeEffect.js at master · Code-Pop/vue-3-reactivity · GitHub
2. 引入 ref 方法
熟悉 Vue3 Composition API 的朋友可能会想到 Ref,它接收一个值,并返回一个响应式可变的 Ref 对象,其值可以通过 value
属性获取。
ref:接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value。
官网的使用示例如下:
const count = ref(0) console.log(count.value) // 0 count.value++ console.log(count.value) // 1
我们有 2 种方法实现 ref 函数:
- 使用
rective
函数
const ref = intialValue => reactive({value: intialValue});
这样是可以的,虽然 Vue3 不是这么实现。
- 使用对象的属性访问器(计算属性)
const ref = raw => { const r = { get value(){ track(r, 'value'); return raw; }, set value(newVal){ raw = newVal; trigger(r, 'value'); } } return r; }
使用方式如下:
let product = reactive({ price: 10, quantity: 2 }); let total = 0, salePrice = ref(0); effect(() => { salePrice.value = product.price * 0.9 }); effect(() => { total = salePrice.value * product.quantity }); console.log(total, salePrice.value); // 18 9 product.quantity = 5; console.log(total, salePrice.value); // 45 9 product.price = 20; console.log(total, salePrice.value); // 90 18
在 Vue3 中 ref 实现的核心也是如此。
代码地址: vue-3-reactivity/06-ref.js at master · Code-Pop/vue-3-reactivity · GitHub
四、实现简易 Computed 方法
用过 Vue 的同学可能会好奇,上面的 salePrice
和 total
变量为什么不使用 computed
方法呢?
没错,这个可以的,接下来一起实现个简单的 computed
方法。
const computed = getter => { let result = ref(); effect(() => result.value = getter()); return result; } let product = reactive({ price: 10, quantity: 2 }); let salePrice = computed(() => { return product.price * 0.9; }) let total = computed(() => { return salePrice.value * product.quantity; }) console.log(total.value, salePrice.value); product.quantity = 5; console.log(total.value, salePrice.value); product.price = 20; console.log(total.value, salePrice.value);
这里我们将一个函数作为参数传入 computed 方法,computed 方法内通过 ref 方法构建一个 ref 对象,然后通过 effct 方法,将 getter 方法返回值作为 computed 方法的返回值。
这样我们实现了个简单的 computed 方法,执行效果和前面一样。
五、源码学习建议
1. 构建 reactivity.cjs.js
这一节介绍如何去从 Vue 3 仓库打包一个 Reactivity 包来学习和使用。
准备流程如下:
- 从 Vue 3 仓库下载最新 Vue3 源码;
git clone https://github.com/vuejs/vue-next.git
- 安装依赖:
yarn install
- 构建 Reactivity 代码:
yarn build reactivity
复制 reactivity.cjs.js 到你的学习 demo 目录:
上一步构建完的内容,会保存在 packages/reactivity/dist目录下,我们只要在自己的学习 demo 中引入该目录的 reactivity.cjs.js 文件即可。
学习 demo 中引入:
const { reactive, computed, effect } = require("./reactivity.cjs.js");
2. Vue3 Reactivity 文件目录
在源码的 packages/reactivity/src
目录下,有以下几个主要文件:
- effect.ts:用来定义
effect
/track
/trigger
; - baseHandlers.ts:定义 Proxy 处理器( get 和 set);
- reactive.ts:定义
reactive
方法并创建 ES6 Proxy; - ref.ts:定义 reactive 的 ref 使用的对象访问器;
- computed.ts:定义计算属性的方法;
(图片来源:Vue Mastery)
六、总结
本文带大家从头开始学习如何实现简单版 Vue 3 响应式,实现了 Vue3 Reactivity 中的核心方法( effect / track / trigger / computed /ref 等方法),提高项目开发效率和代码调试能力。