响应式原理

TIP

vue3.0采用Monorepo管理项目,代码采用ts编写,rollup打包

一、响应式API实现

0.使用方法

const { reactive, shallowReactive, readonly, shallowReadonly } = VueReactivity;
let obj = { name: 'test', age: { n: 10 } };
const state1 = reactive(obj); // 普通代理 》 递归代理全部属性
const state2 = shallowReactive(obj); // 浅代理 》 只代理第一层
const state3 = readonly(obj); // 只读代理 》 所有的属性不能修改
const state4 = shallowReadonly(obj); // 浅只读代理 》 第一层不能修改

1.入口文件

// packages/reactivity/src/index.ts
// 只导出方法,不实现功能
export {
	reactive,
    shallowReactive,
    shallowReadonly,
    readonly
} from './reactive';

2.四个函数

// packages/reactivity/src/reactive.ts
import {
    mutableHandlers,
    shallowReactiveHandlers,
    readonlyHandlers,
    shallowReadonlyHandlers
} from './baseHandlers';

import { isObject } from "@vue/shared";

// 定义两个缓存对象,用于存储被代理过的对象
// weakmap的优点:会自动垃圾回收,不会造成内存泄漏, 存储的key只能是对象
const reactiveMap = new WeakMap(); // 缓存普通代理
const readonlyMap = new WeakMap(); // 缓存只读代理

/**
 * 科里化-抽象出每个方法的公共Proxy进行拦截
 * @param target 目标对象
 * @param isReadonly 是否只读
 * @param baseHandlers 处理器的对象方法,包含get和set
 */
export function createReactiveObject(target, isReadonly, baseHandlers) {
	// 1.判断目标是否为对象,不是对象不用拦截,直接返回,只拦截对象
	if (!isObject(target)) {
		return target;
	}
	// 2.判断对象是否已经被代理过了,如果被代理过了就直接返回
	const proxyMap = isReadonly ? readonlyMap : reactiveMap;
	const existProxy = proxyMap.get(target);
	if (existProxy) {
		return existProxy;
	}

	// 3.没有被代理过的对象直接代理了,放入缓存并返回代理对象
	const proxy = new Proxy(target, baseHandlers);
	proxyMap.set(target, proxy);

	// 4. 返回代理对象
	return proxy;
}

// 普通代理
export function reactive(target) {
	return createReactiveObject(target, false, mutableHandlers);
}

// 浅普通代理:只代理第一层
export function shallowReactive(target) {
	return createReactiveObject(target, false, shallowReactiveHandlers);
}

// 浅的只读代理
export function shallowReadonly(target) {
	return createReactiveObject(target, true, shallowReadonlyHandlers);
}

// 只读代理
export function readonly(target) {
	return createReactiveObject(target, true, readonlyHandlers);
}

3.处理器对象

import { isObject, extend, isArray, isIntegerKey, hasOwn, hasChanged } from "@vue/shared/src";
import { readonly, reactive } from "./reactive";
import { TrackOpTypes, TriggerOrTypes } from "./operators";
import { track, trigger } from "./effect";

/**
 * 科里化-定义get方法的拦截处理
 * @param isReadonly 是否只读
 * @param shallow 是否为浅代理
 */
function createGetter(isReadonly = false, shallow = false) {
	return function get(target, key, receiver) {
		// 取值
		const res = Reflect.get(target, key, receiver);

		// 如果不是只读需要进行依赖搜集,等数据变化后更新对应的视图
		if (!isReadonly) {
			console.log('依赖收集');
		}
		// 如果为浅代理,则直接返回
		if (shallow) {
			return res;
		}
		// 如果为对象,需要递归进行代理(取值才代理 》 懒代理)
		if (isObject(res)) {
			return isReadonly ? readonly(res) : reactive(res);
		}
		return res;
	}
}

// 定义四个get方法
const get = createGetter();
const shallowGet = createGetter(false, true);
const readonlyGet = createGetter(true);
const showllowReadonlyGet = createGetter(true, true);

/**
 * 科里化-定义set方法的拦截处理
 * @param shallow 是否为浅代理
 */
function createSetter(shallow = false) {
	return function set(target, key, value, receiver) {
		// 更新值
		const result = Reflect.set(target, key, value, receiver);
		return result;
	}
}

// 定义set方法
const set = createSetter();
const shallowSet = createSetter(true);

let readonlyObjSet = {
    set: (target, key) => {
        console.warn(`set on key ${key} falied: target is readonly.`)
    }
}

// 导出四个函数
export const mutableHandlers = {
    get,
    set
}

export const shallowReactiveHandlers = {
    get: shallowGet,
    set: shallowSet
}

export const readonlyHandlers = extend({
	get: readonlyGet,
}, readonlyObjSet)

export const shallowReadonlyHandlers = extend({
	get: showllowReadonlyGet,
}, readonlyObjSet)

二、effect实现

0.使用方法

<div id="app"></div>
<script src="../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<script>
	let {effect, reactive} = VueReactivity;
	let state = reactive({name: 'test', age: 10, arr: [1, 2, 3]});

	// 定义需要依赖收集的属性
	effect(() => {
		console.log('render');
		// app.innerHTML = state.name + state.age;
		app.innerHTML = state.arr[2] + state.arr.length;
	});

	// 赋值》更新操作
	setTimeout(() => {
		// state.name = 'xxx'; // 更改属性
		// state.aaa = 'xxx'; // 新增属性
		// state.arr.push(1); // 给数组新增值
		// state.arr[2] = 100; // 更新原有数组索引和值
		// state.arr[3] = 100; // 给数组新增索引和值
		state.arr.length = 100; // 更改数组长度
	}, 1000);
</script>

1.入口文件

// packages/reactivity/src/index.ts
export {
    effect
} from './effect'

2.主要结构

// packages/reactivity/src/effect.ts

/**
 * effect函数
 * @param fn 需要执行的函数
 * @param options 配置对象
 */
export function effect(fn, options: any = {}) {
	// 创建响应式的effect,可以做到数据变化重新执行 
	const effect = createReactiveEffect(fn, options);
	// 如果不是懒配置,则默认需要先执行一次effect
	if (!options.lazy) {
		effect();
	}
	return effect;
}

let uid = 0; // effect的标识量
let activeEffect; // 存储当前的effect
const effectStack = []; // effect的记录栈
/**
 * 创建响应式的effect
 * @param fn 需要执行的函数
 * @param options 配置对象
 */
function createReactiveEffect(fn, options) {
	const effect = function reactiveEffect() {
		// 为了避免添加重复的effect和规避死循环,需要加一次判断
		if (!effectStack.includes(effect)) {
			try {
				effectStack.push(effect);
				activeEffect = effect;
				// 执行fn函数
				return fn();
			} finally {
				// fn函数执行完了之后,需要出栈,并且修改当前的activeEffect
				effectStack.pop();
				activeEffect = effectStack[effectStack.length - 1];
			}
		}
	}

	effect.id = uid++; // 制作一个effect的唯一标识,用于区分effect
	effect._isEffect = true; // 用于标识当前effect是响应式的
	effect.raw = fn; // 保留effect对应的原函数
	effect.options = options; // 保留用户配置的属性对象

	return effect;
}

3.依赖收集

  • 在get取值的时候进行依赖收集
// packages/reactivity/src/baseHandlers.ts
function createGetter(isReadonly = false, shallow = false) {
	return function get(target, key, receiver) {
		// 取值
		const res = Reflect.get(target, key, receiver);

		// 如果不是只读需要进行依赖搜集,等数据变化后更新对应的视图
		if (!isReadonly) {
			console.log('执行effect时会取值','收集effect');
			track(target, TrackOpTypes.GET, key);
		}
		//...
	}
}
  • 建立各种依赖储存到WeakMap中
// packages/reactivity/src/effect.ts

// 搜集某个对象的属性,对应的effect函数,存储数据结构为:
// weakMap:{name:'zf',age:12} => map
// map: {name => set(effect, effect), age => set(effect)}
const targetMap = new WeakMap();
/**
 * 依赖搜集函数
 * @param target 目标对象
 * @param type 搜集的类型
 * @param key 搜集的key
 */
export function track(target, type, key) {
	// 如果当前属性没有在effect中应用,则不需要搜集
	if (activeEffect === undefined) {
		return;
	}
	// 1.第一层weakmap存储目标对象 》 map
	let depsMap = targetMap.get(target);
	if (!depsMap) {
		targetMap.set(target, depsMap = new Map);
	}
	// 2.第二层map存储key 》 set
	let dep = depsMap.get(key);
	if (!dep) {
		depsMap.set(key, dep = new Set);
	}
	// 3.第三层set储存activeEffect
	if (!dep.has(activeEffect)) {
		dep.add(activeEffect);
	}
}

4.触发更新

  • 对新增属性和修改属性做分类
// packages/reactivity/src/baseHandlers.ts
function createSetter(shallow = false) {
	return function set(target, key, value, receiver) {
		// 先获取老的值
		const oldValue = target[key];
		// 判断是新增还是更新的标志量:
		// 如果是数组就判断数组索引是否小于当前长度,小于则是修改,大于等于则是新增
		// 如果是对象就判断对象是否包含key,包含则修改,不包含则新增
		let hadKey = isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key);
		
		// 更新值
		const result = Reflect.set(target, key, value, receiver);

		if (!hadKey) {
			// 新增
			trigger(target, TriggerOrTypes.ADD, key, value);
		} else if (hasChanged(oldValue,value)) {
			// 修改 - 只有老值和新值不一样的时候才修改
			trigger(target, TriggerOrTypes.SET, key, value, oldValue)
		}

		return result;
	}
}
  • 将需要触发的effect找到依次执行
// packages/reactivity/src/effect.ts

import { isArray, isIntegerKey } from "@vue/shared/src";
import { TriggerOrTypes } from "./operators";

/**
 * 触发更新函数
 * @param target 目标对象
 * @param type 操作类型
 * @param key 操作的key
 * @param newValue 新值
 * @param oldValue 老值
 */
export function trigger(target, type, key?, newValue?, oldValue?) {
	// [1, 2, 3, 1] 0 "3" 1 undefined
	console.log(target, type, key, newValue, oldValue);
	
	// 如果这个属性没有 收集过effect,那不需要做任何操作
	const depsMap = targetMap.get(target);
	if (!depsMap) return;

	// 用set存储偶有需要执行的effect,最终一起执行
	const effects = new Set();
	// 定义遍历effects的函数
	const add = (effectsToAdd) => {
		if (effectsToAdd) {
			effectsToAdd.forEach(effect => effects.add(effect));
		}
	}

	// 收集需要触发更新的依赖
	if (key === 'length' && isArray(target)) {
		// 如果修改的是数组的长度,需要特殊处理
		depsMap.forEach((dep, key) => {
			// 如果更改的是长度,或者 更改的长度小于收集的索引,也需要触发更新
			if (key === 'length' || key > newValue) {
				add(dep);
			}
		});
	} else {
		// 修改的是对象、不是数组的长度
		if (key !== undefined) {
			add(depsMap.get(key));
		}
		// 如果给数组添加了一个索引就触发长度的更新
		switch (type) {
			case TriggerOrTypes.ADD:
				// 目标是数组,并且是是数组的索引
				if (isArray(target) && isIntegerKey(key)) {
					add(depsMap.get('length'));
				}
				break;
		}
	}

	// 全部更新操作
	effects.forEach((effect: any) => effect());
}

三、Refs实现

  • ref 和 reactive的区别 reactive内部采用proxy,ref中内部使用的是defineProperty

0.使用方法

  • shallowRef不会把对象的key变为响应式,ref可以,因为ref进行了reactive的迭代操作
  • ref和shallowRef的使用
<div id="app"></div>
<script src="../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<script>
	let {effect, ref ,shallowRef} = VueReactivity;
	// 将普通的类型 转化成一个对象,这个对象中有value属性 指向原来的值
	let test = ref('test');
	let state = ref({name: 'name'});
	let shallow = shallowRef({name: 'name'});

	effect(() => {
		// // track
		app.innerHTML = test.value + ' ' + state.value.name + ' ' + shallow.value.name;
	});

	setTimeout(() => {
		test.value = 'hello'; // trigger
		state.value.name = 'world'; // trigger
		shallow.value.name = 'world'; // 不会trigger
	}, 1000);
</script>
  • toRef和toRefs的使用
<div id="app"></div>
<script src="../node_modules/@vue/reactivity/dist/reactivity.global.js"></script>
<script>
	let {effect, reactive, toRef ,toRefs} = VueReactivity;
	// 将普通的类型 转化成一个对象,这个对象中有value属性 指向原来的值
	let proxy = reactive({name: 'test', age: 10});
	// 将对象的全部属性转化为响应式
	let {name, age} = toRefs(proxy);

	let proxy2 = reactive({name: 'test', age:10});
	let name2 = toRef(proxy2, 'name');

	effect(() => {
		// track
		app.innerHTML = name.value + age.value + name2.value;
	});

	// 赋值》更新操作
	setTimeout(() => {
		name.value = 'hello'; // trigger
		name2.value = 'hello'; // trigger
	}, 1000);
</script>

1.入口文件

// packages/reactivity/src/index.ts
export {
    ref,
    shallowRef,
    toRef,
    toRefs
} from './ref'

2.ref和shallowRef

// packages/reactivity/src/ref.ts
import { isObject, hasChanged, isArray } from "@vue/shared/src";
import { reactive } from "./reactive";
import { track, trigger } from "./effect";
import { TrackOpTypes, TriggerOrTypes } from "./operators";

// 转换函数
const convert = (val) => isObject(val) ? reactive(val) : val;

// ref 和 shallowRef实现的基类
class RefImpl {
	public _value; // 表示 声明了一个_value属性 但是没有赋值
	public __v_isRef = true; // 产生的实例会被添加 __v_isRef 表示是一个ref属性
	// 参数中前面增加修饰符 标识此属性放到了实例上
	constructor(public rawValue, public shallow) {
		// 如果是深度 需要把里面的都变成响应式的
		this._value = shallow ? rawValue : convert(rawValue);
	}
	// 取value值,会代理到_value上
	get value() {
		// 收集依赖
		track(this, TrackOpTypes.GET, 'value');
		return this._value;
	}
	// 设置值
	set value(newValue) {
		// 判断老值和新值是否有变化
		if (hasChanged(this._value, newValue)) {
			this.rawValue = newValue;
			this._value = this.shallow ? newValue : convert(newValue);
			// 触发更新
			trigger(this, TriggerOrTypes.SET, 'value', newValue);
		}
	}
}

function createRef(rawValue, shallow = false) {
	return new RefImpl(rawValue, shallow);
}

// 导出方法
export function ref(value) {
	return createRef(value);
}

export function shallowRef(value) {
	return createRef(value, true);
}

3.toRef和toRefs

// packages/reactivity/src/ref.ts

// toRef 实现的基类
class ObjectRefImpl {
	public __v_isRef = true;
	constructor(public target, public key) {}
	// 取值
    get value(){
		// 如果原对象是响应式的就会依赖收集
        return this.target[this.key];
	}
	// 设值值
    set value(newValue){
		// 如果原来对象是响应式的 那么就会触发更新
        this.target[this.key] = newValue;
    }
}

// 将某一个对象或数组对应的key转化成ref
export function toRef(target, key) {
    return new ObjectRefImpl(target, key);
}

// 将一个对象或数组的所有属性转化为red
export function toRefs(object){
    const ret = isArray(object) ? new Array(object.length) : {};
    for(let key in object){
        ret[key] = toRef(object, key);
    }
    return ret;
}

四、工具类

// packages/shared/src/index.ts

// 判断是否为对象
export const isObject = (value) => typeof value == 'object' && value !== null;
// 合并方法
export const extend = Object.assign
// 判断数组
export const isArray = Array.isArray;
// 判断function
export const isFunction = (value) => typeof value == 'function'
// 判断数字
export const isNumber = (value) => typeof value == 'number';
// 判断字符串
export const isString = (value) => typeof value === 'string';
// 判断是否为数组的索引
export const isIntegerKey = (key) => parseInt(key) + '' === key;
// 判断对象是否包含某个属性
let hasOwnpRroperty = Object.prototype.hasOwnProperty;
export const hasOwn = (target, key) => hasOwnpRroperty.call(target, key);
// 判断新老值是否一样
export const hasChanged = (oldValue,value) => oldValue !== value;