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函数的实现
// 用于存储正在执行的effect, 再收集依赖的步骤需要用到
let activeEffect;
//依赖存储数据解构
const targetMap = new WeakMap();
// 用于收集依赖
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 TriggerOpTypes.ADD:
// 如果是数组新增并且是用的下标, 直接获取length的依赖执行
if (Array.isArray(target)) {
if (isIntegerKey(key)) {
add(depsMap.get("length"));
}
} else {
depsMap.forEach((dep, key) => {
add(dep);
})
}
break;
case TriggerOpTypes.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函数实现
// this is javascript
// 副作用函数, 相当于vue2中的watcher
export function effect (fn) {
// 因为副作用函数执行时, 我们需要将当前执行的副作用函数储存,获取值时使用
// 并且需要增加一些属性, 所以用高阶函数创建effect
let effect = createReactiveEffect(fn);
if (!effect.options.lazy) {
// 如果不是懒函数(即computed), 用于第一次收集依赖
effect();
}
return 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;
return 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
git地址
代码链接 miniVue3文件夹为vue3的简单实现代码