Skip to content

响应式原理

TIP

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

一、响应式 API 实现

0.使用方法

js
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.入口文件

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

2.四个函数

js
// 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.处理器对象

js
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.使用方法

html
<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.入口文件

js
// packages/reactivity/src/index.ts
export { effect } from "./effect";

2.主要结构

js
// 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 取值的时候进行依赖收集
js
// 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 中
js
// 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.触发更新

  • 对新增属性和修改属性做分类
js
// 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 找到依次执行
js
// 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 的使用
html
<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 的使用
html
<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.入口文件

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

2.ref 和 shallowRef

js
// 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

js
// 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;
}

四、工具类

js
// 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;

Released under the MIT License.