一.reactive与effect功能
reactive方法会将对象变成proxy对象, effect中使用reactive对象时会进行依赖收集,稍后属性变化时会重新执行effect函数。
<div id="app"></div><script type="module">import {reactive,effect,} from "/node_modules/@vue/reactivity/dist/reactivity.esm-browser.js";// reactive创建一个响应式对象// effect 副作用函数,默认执行一次,数据变化后再次执行const state = reactive({ name: "orange", age: 18 });effect(() => {document.getElementById("app").innerHTML = state.name;});console.log(state);setTimeout(() => {state.name = "apple";}, 2000);</script>
二.reactive与effect实现
1.实现reactive
1.1 get、set基础实现
// reactive.ts
import { isObject } from "@vue/shared";const mutableHandlers = {get(target, key, receiver) {return Reflect.get(target, key, receiver);},set(target, key, value, receiver) {return Reflect.set(target, key, value, receiver);},
};export function reactive(target) {if (!isObject(target)) {return target;}const proxy = new Proxy(target, mutableHandlers);return proxy;
}
如下图,如果采用target[key]方法,获取aliasName时 不会触发name的get

1.2 完善重复
const state = { name: "orange", age: 18 };const p1 = reactive(state);const p2 = reactive(state);const p3 = reactive(p1);console.log(p1 === p2);console.log(p1 === p3);
- 响应式数据缓存,防止重复代理
使用map,key为对象,值为响应式对象,实现p1 === p2
const reactiveMap = new WeakMap(); //内存泄漏
export function reactive(target) {if (!isObject(target)) {return target;}const exitProxy = reactiveMap.get(target);if (exitProxy) {return exitProxy;}const proxy = new Proxy(target, mutableHandlers);reactiveMap.set(target, proxy);return proxy;
}
- 响应式数据标记
用枚举做标记,响应式对象都有get和set方法,p1初次创建时state没有get和set方法,target[ReactiveFlags.IS_REACTIVE]取值为false,创建p3时,p1响应式,取值为true,直接返回p1
export const enum ReactiveFlags {IS_REACTIVE = "__v_isReactive",
}get(target, key, receiver) {if (ReactiveFlags.IS_REACTIVE == key) {return true;}return Reflect.get(target, key, receiver);},export function reactive(target) {...,if (target[ReactiveFlags.IS_REACTIVE]) {return target;}const proxy = new Proxy(target, mutableHandlers);reactiveMap.set(target, proxy);return proxy;
}
2.实现effect
2.1 effect函数
vue2与vue3早期的依赖收集采用的都是栈方式存储,vue3后来改为树型数据存储。
effect执行时,把当前effect作为全局的,触发属性的get方法,收集依赖
let activeEffect;
class ReactiveEffect {public deps: Array<any> = []; // 判断依赖属性public active: boolean = true; // 是否激活public parent = undefined; constructor(public fn) {}run() {if (!this.active) {return this.fn();}try {this.parent = activeEffect;activeEffect = this;return this.fn();} finally {activeEffect = this.parent;this.parent = undefined;}}
}
export function effect(fn) {const _effect = new ReactiveEffect(fn);_effect.run();
}
2.2 依赖收集
执行effect时,会触发依赖属性的get方法,在属性的get中进行依赖收集
get(target, key, receiver) {if (ReactiveFlags.IS_REACTIVE == key) {return true;}console.log(activeEffect, key);track(target,key)return Reflect.get(target, key, receiver);},
/* 属性变动后 要重新执行run 需要把属性和effect关联起来 收集对象上属性关联的effect不能光记录属性,容易两个对象有重名的属性,所以需要带着对象记录target为对象let mapping = {target:{name:[effect1,effect2,effect3] 一个属性可以在多个页面使用}}*/
const targetMap = new WeakMap();
export function track(target, key) {// 如果取值操作没有发生在effect中,则不需要收集依赖if (!activeEffect) {return;}// 判断是否已经记录过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()));}let shouldTrack = !dep.has(key);if (shouldTrack) {dep.add(activeEffect);activeEffect.deps.push(dep); //同时effect记住当前这个属性}
}
2.3 属性更改 触发effect
set(target, key, value, receiver) {// 数据变化后触发对应的effect方法const oldValue = target[key];let r = Reflect.set(target, key, value, receiver);if (oldValue != value) {trigger(target, key, value, oldValue);}return r;},
export function trigger(target, key, newValue, oldValue) {// 通过对象找到对应的属性 让这个属性对应的effect重新执行const depsMap = targetMap.get(target);if (!depsMap) {return;}const dep = depsMap.get(key); // name 或者 age对应的所有effectconst effects = [...dep];// 运行的是数组 删除的是seteffects &&effects.forEach((effect) => {// 正在执行的effect ,不要多次执行if (effect !== activeEffect) effect.run();});}
2.4 清除effect依赖
const state = reactive({ flag: true, name: "orange", age: 18 });effect(() => {// 副作用函数 (effect执行渲染了页面)console.log("render");document.body.innerHTML = state.flag ? state.name : state.age;});setTimeout(() => {state.flag = false;setTimeout(() => {console.log("修改name,原则上不更新");state.name = "zf";}, 1000);}, 1000);
当切换flag时,effect的依赖已经更新,但是修改name又触发了依赖,需要在每次收集依赖前先清空

function cleanupEffect(effect) {let { deps } = effect; // 清理effectfor (let i = 0; i < deps.length; i++) {deps[i].delete(effect);}effect.deps.length = 0;
}
为了避免无限循环页面卡死情况,触发依赖时拷贝出来一份属性对应的effect列表,因为清除的时候在删除effect,边删除边添加会造成死循环
2.5 effect失活
通过effect返回当前effect实例,然后curEffect.effect.stop()调用stop方法
let runner = effect(() => {// 副作用函数 (effect执行渲染了页面)console.log("render");document.body.innerHTML = state.flag ? state.name : state.age;});runner.effect.stop()
stop() {cleanupEffect(this);this.active = false;}export function effect(fn) {const _effect = new ReactiveEffect(fn);_effect.run();const runner = _effect.run.bind(_effect); //调用effect.effect.stop() 停止effectrunner.effect = _effect;return runner;
}
失活后我们可以手动调用 runner()触发effect方法
2.6 调度执行
trigger触发时,我们可以自己决定副作用函数执行的时机、次数、及执行方式
export function effect(fn, options:any = {}) {const _effect = new ReactiveEffect(fn,options.scheduler); // 创建响应式effect// if(options){// Object.assign(_effect,options); // 扩展属性// }_effect.run(); // 让响应式effect默认执行const runner = _effect.run.bind(_effect);runner.effect = _effect;return runner; // 返回runner
}export function trigger(target, type, key?, newValue?, oldValue?) {const depsMap = targetMap.get(target);if (!depsMap) {return}let effects = depsMap.get(key);if (effects) {effects = new Set(effects);for (const effect of effects) {if (effect !== activeEffect) { if(effect.scheduler){ // 如果有调度函数则执行调度函数effect.scheduler()}else{effect.run(); }}}}
}