1.先列举对于reactive实现过程中重要的几个东西
- reactive, 此api可实现对数据转换为响应式数据,保证经过reative转换过的数据使用后,改变数据,界面能随之更新。
- proxy基本api的认识和使用,如果不太了解的话, 推荐阮一峰的ES6入门
- track依赖收集函数, 收集当前访问数据所需要的依赖, 在proxy的get中执行
- trigger依赖触发函数,通知执行收集的effect函数,在proxy的set中执行
- targetMap,一个WeakMap数据结构,存储数据依赖,相当于vue2中的dep
- effect函数,相当于vue2中的watcher,组件渲染、计算属性computed、watch函数、watchEffect函数实现都是依赖于effect函数
- activeEffect, 表示当前正在执行的effect,用于收集依赖
- effectStack,用于模拟栈结构存储effect的数组,解决effect执行过程中effect嵌套执行的场景,保证正确收集依赖函数
示例
const { reactive, effect } = vue; const effectDom = document.querySelector(".effect"); const state = reactive([1, 2, 3]); effect(function () { effectDom.innerHTML = state }) setTimeout(() => { state[2] = 2; }, 1000)
将数组初次渲染到界面中后,1秒后修改state数据, 界面自动更新
2.reactive函数
reactive函数实现
// reactive函数 export function reactive (target) { return createReactiveObject(target, baseHandler) } // 定义createReactiveObject 创建proxy代理, 是因为代理模式有多种, 可能是只读的也可能是 // 即可读,也可设置,通过高阶函数灵活传入baseHandler, 创建不同功能的响应式数据如 readOnlyRactive和Reactive function createReactiveObject (target, baseHandler) { // 首先判断target是否是对象类型, 如果不是不许转换成响应式 if (!isObject(target)) { return; } // 进行代理 let proxy = new Proxy(target, baseHandler); return proxy; } // baseHandler export const baseHandler = { get (target, key, recevier) { // 获取key的值 const res = Reflect.get(target, key); // 收集依赖 track(target, key); return res; }, set (target, key, val, recevier) { // /先获取旧的值 const oldVal = Reflect.get(target, key); // 需要判断是新增属性值, 还是更改属性值 // 需判断数组情况和对象情况如果是arr[10]这种形式更改的数组 // 需要判断长度和key的大小, 其余的按对象属性走 const hadKey = Array.isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key); // 设置新值 const res = Reflect.set(target, key, val); if (!hadKey) { // 新增属性 通知依赖更新 trigger(target, "ADD", key, val, oldVal); } else { // 修改属性 通知依赖更新 trigger(target, "SET", key, val, oldVal); } return res; } }
1 rective函数中调用createReactiveObject函数创建响应式对象, 因为在源码中不光有有reactive数据, 还有readOnly数据,通过createReactiveObject参数的不同创建不同的数据类型, 我们这里只实现rective数据。
-
createReactiveObject实现。
2.1 createReactiveObject中第一步会先判断数据类型, 如果是基本的数据类型,不会进行响应式转换。
2.2 用proxy对传入的数据进行代理,用baseHandler作为代理配置项,传入proxy第二个参数。
2.3 返回proxy代理后的数据,提供给用户使用
。 -
当用户访问或设置proxy代理返回的数据时,会触发baseHandle 对象里的get和set。
-
baseHandler中get实现逻辑。
4.1 用Reflect.get获取值。
4.2 用track收集数据所需的依赖(主要逻辑)。
4.3 返回数据给用户。
-
baseHandler中set实现逻辑。
5.1 先使用 Reflect.get 获取旧值oldValue。
5.2。判断当前设置的key是否存在数据target中, 如果是数组 并且key是字符串的数字, 就根据当前key的数值是否大于数组长度判断是否为新增数据还是修改数据, 如果是对象就用hasOwnProperty判断key是否存在于对象中,判断是新增还是修改。
5.3 用Reflect.set设置新值
5.4 触发trigger通知依赖进行更新,根据hadKey告知trigger是添加属性还是更改属性。将newValue和oldValue都传入函数中
3.track函数实现
// 用于存储正在执行的effect, 再收集依赖的步骤需要用到 let activeEffect; //依赖存储数据解构 const targetMap = new WeakMap(); // 用于收集依赖 export function track (target, key) { // 获取对象存储的deps对象 let depsMap = targetMap.get(target); // 如果deps对象不存在说明是第一次此对象收集依赖需新建对应的数据结构 if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } // 获取对象中key存储的依赖dep数据 let deps = depsMap.get(key); // deps不存在说明是第一次收集, 需要新建set数据结构收集依赖 if (!deps) { depsMap.set(key, (deps = new Set())); } // 当前执行的activeEffect就是我们当前值需要收集的依赖函数 // 先判断是否已经收集过当前的副作用函数, 不要重复收集 if (!deps.has(activeEffect)) { // 加入到依赖中 deps.add(activeEffect); // 副作用函数也需要收集deps, 形成一个双向记录的过程 activeEffect.deps.push(deps); } }
- 先根据target获取到依赖的map对象depsMap,如果depsMap不存在就先创建一个Map以target为key存入targetMap中。
2.根据获取数据的key值从depsMap中获取到当前key的所有依赖deps, 如果deps不存在, 就创建一个set数据结构以key为键值存入depsMap中。
- 最后一步是建立一个双向的数据存储,deps存储effect副作用函数,用于下次数据变更触发执行,effect也要存储它的deps,供后续取消数据响应。
4 trigger函数的实现
// 用于触发依赖 export function trigger (target, type, key, val, oldVal) { // 获取对象target的依赖数据map let depsMap = targetMap.get(target); // 没有收集过依赖不许执行 if (!depsMap) { return } // 获取effect数组 let deps = depsMap.get(key); let effects = new Set(); const add = (effectsSet) => { if (effectsSet) { effectsSet.forEach(effect => { effects.add(effect); }) } } if (key != void 0) { //处理特殊情况对数组 /* 情况1: effect(function () { effectDom.innerHTML = state.length }) setTimeout(() => { state.length = 2; }, 1000) 当effect中获取length时 改变length会触发依赖更新,这是正常情况 */ /*情况2: effect(function () { effectDom.innerHTML = state[2] }) setTimeout(() => { state.length = 2; }, 1000) 当用下标获取值时, 修改length不会触发更新, 因为对length未收集依赖, 收集的是effect中下标的依赖键值为下标, 但因为此时 length长度变小, 导致数组变化 所以需要更新界面 */ // 如果修改的是length 并且target是数组 if (key === "length" && Array.isArray(target)) { // 循环target的所有key的依赖 depsMap.forEach((dep, key) => { // 因为在用 state.length = 2修改值时 只有当设置的length的 // 值(val)小于 数组依赖项的下标时,才更新界面 // 或本身就有length收集的依赖时 if (key === "length" || key >= val) { // 触发更新 add(dep); } }) } else { // 根据我们传入的type处理余下特殊情况 switch (type) { case "ADD": // 如果是数组新增并且是用的下标, 直接获取length的依赖执行 if (Array.isArray(target)) { if (isIntegerKey(key)) { add(depsMap.get("length")); } } else { depsMap.forEach((dep, key) => { add(dep); }) } break; case "SET": add(deps); break; } } } const run = (effect) => { if (effect.options.scheduler) { effect.options.scheduler(effect); } else { effect(); } } effects.forEach(run) }
-
先获取当前target数据的depMap依赖集合, 如果depMap不存在, 说明当前数据没有依赖,改变这个数据,不需要更新界面, 直接return掉
-
从depMap中获取key值得依赖set集合, 定义一个effects的set数据结构存储将要执行的effect,并定义向effects中添加effect的add函数方法
-
通过数据的改变获取相应的需要更新的依赖函数,用add添加到effects中
3.1 首先要处理数组操作length改变数组的特殊情况,
1– 当修改length的数值比模板中使用的数组的数据的下标小, 说明大于现在数组长度的数据都不需要在页面中展示了, 所以需要循环depsMap找出所有大于length的下标依赖并执行更新界面
2– 当模板中直接使用数组数据, 不是单个数组的某个值时, 模板渲染时会访问数组的length数组, 所以length属性会收集对应的数组依赖, 当我们后续改变length值导致数组数据改变时,可以获取length属性的依赖并添加到effects, 去执行依赖函数,更新界面
3.2 处理完数组的特殊情况, 下面就根据set函数中hadKey判断的是添加属性ADD操作,还是修改属性Set操作1– ADD添加属性
如果是数组, 就判断key是否是数值的字符串,如果是说明是通过下标修改的数组, 这里就获取length的依赖函数集合通过add添加到effects中
如果是对象新家属性, 就获取所有对象数据的依赖,添加到effects
2– set修改数据, 直接获取对应key值得set集合添加到effects中 -
循环effects数据结构,通过run函数执行effect副作用函数执行更新操作
effect函数实现
// 副作用函数, 相当于vue2中的watcher export function effect (fn) { // 因为副作用函数执行时, 我们需要将当前执行的副作用函数储存,获取值时使用 // 并且需要增加一些属性, 所以用高阶函数创建effect let effect = createReactiveEffect(fn); if (!effect.options.lazy) { // 如果不是懒函数(即computed), 用于第一次收集依赖 effect(); } } // 用于给每个副作用函数增加标记, 标记唯一 let uid = 0; // 真正创建副作用函数, 并做一些属性定义 function createReactiveEffect (fn, options = {}) { const effect = function () { // 副作用函数执行完需要将activeEffect,重置为空, 防止不在effect函数中的取值收集依赖 // !effectStack.includes(effect)判断用于处理死循序如下写法 // effect(function () { // state.a++; // }) // 当effect中是state.a++时, 因为a每次增加, 所以会通知依赖更新频繁触发, // 导致死循环, 所以增加判断,当前effect 执行时, 因为effect在effectStack栈中 // 而且我们已经在执行副作用函数了, 所以不需要在触发了 if (!effectStack.includes(effect)) { try { // 先将effect存入栈中解决effect嵌套问题 effectStack.push(effect); activeEffect = effect; fn() } finally { // 执行完后将activeEffect设置成栈中前一个effect effectStack.pop(); activeEffect = effectStack[effectStack.length - 1]; } } } effect.active = true; // 唯一标记 effect.uid = uid++; // 用于存储被哪个值依赖 effect.deps = []; // effect的一些其他属性, 如lazy、shelduler effect.options = options; return effect }
- effect函数内部逻辑, 先通过高阶函数createReactiveEffect创建effect,然后判断effect.options.lazy, 如果lazy是true说明是计算属性computed或watch函数, 不需要创建就执行,如果不是true就需要立即执行effect函数
2.createReactiveEffect函数实现
2.1 在createReactiveEffect中创建effect,并为effect增加一些属性如deps用于储存依赖dep、options配置项、uid标识effect的唯一性、active标识是否是活动的effect。
2.2 effect内部其实最根本就是执行传入的fn函数, fn可能是用户通过计算属性或watch、watchEffect定义的, 也可能是渲染函数。
2.3 effect函数执行时需要将effect先存入effectStack中,再将赋值给activeEffect(activeEffect= effect)然后执行fn, fn中可能访问了用proxy代理的数据,访问数据触发proxy 中的baseHandler的get方法,get中的track就会把activeEffect存入访问数据的Set结构中, 这样就完成了依赖的收集
2.4 effect执行最后需要将当前的effect函数从effectStack栈中删除, 并将栈中的最后一项赋值给activeEffect即 activeEffect = effectStack[effectStack.length – 1];
这里effectStack栈结构的作用
-
防止死循环
effect(function () { state.a++; })
此写法当effect执行时又改变了 state.a的值,导致触发了trigger方法,又通知effect执行,依次往复就会无线死循环
增加!effectStack.includes(effect)判断当前effect是否在栈中来判断是否正在运行,如果在栈中,就不再执行,就会避免死循环 -
嵌套effect函数的情况
const counter = reactive({ num: 0, num2: 0 }) function logCount() { console.log('num2:', counter.num2) } effect(() => { effect(logCount) console.log('num:', counter.num) } )
当执行外部effect时,activeEffect被设置为外层的effect, 当内部effect执行时被设置为activeEffect
-
如果不使用effectStack栈结构,我们需要在effect函数执行完时将activeEffect设置为null, 所以这个例子中会导致内部effect执行完activeEffect是null当执行console.log(‘num:’, counter.num) 时,num收集不到依赖函数
-
当使用栈effectStack,activeEffect被设置为外层的effect,并将effect推入数组,然后执行内部effect,内部effect被推入数组,activeEffect为内层effect, 然后执行console.log(‘num2:’, counter.num2) ,num2成功收集内部effect函数为依赖, 收集完成effectStack删除内部effect,并将数组的最后一项赋值给activeEffect, 此时activeEffect是外层的effect, 然后执行console.log(‘num:’, counter.num) ,num成功收集外部effect函数作为依赖,从effectStack删除外层effect函数,因为现在数组为空,所以activeEffect被设置为null