Reactive from Vue 2.0 to 3.0
Reactive in Vue 2.0
思想
- 监听数据,拦截属性,重新 Object.defineProperty
- 对嵌套的对象进行递归监听
- 对新增对象,在 set 之前进行监听
- 拦截数组,劫持方法并重写
代码
// 重写数组方法
// 1. 函数劫持
// 2. 面向切片编程 AOP
let oldArrayPrototype = Array.prototype;
let proto = Object.create(oldArrayPrototype);
['push', 'pop', 'shift', 'unshift'].forEach(method => {
proto[method] = function() { // 函数劫持
updateView(); // AOP
oldArrayPrototype[method].apply(this, arguments);
}
});
// data 观察者
function observer(target) {
if (typeof target !== 'object' || target === null) {
return target;
}
if (Array.isArray(target)) { // (3): 拦截数组,重写数组方法
// Object.setPrototypeOf(target, proto)
target.__proto__ = proto;
}
for(let key in target) {
defineReactive(target, key, target[key]);
}
}
function defineReactive(target, key, value) {
observer(value); // (1): 嵌套对象
Object.defineProperty(target, key, {
get() {
return value;
},
set(newVal) {
if (newVal !== value) {
observer(newVal); // (2): 对新增对象
updateView();
value = newVal;
}
}
});
}
function updateView() {
console.log('更新视图');
}
let data = {
count: 100,
name: { a: 'hello' }, // 递归 observer
arr: [1, 2, 3]
};
observer(data);
// data.count = 200;
// (1): 嵌套对象
// data.name.a = 'hi'; // (1)
// (2): 对新增对象
// data.name = { b: 'world' };
// data.name.b = 'new world';
// (3): 新增属性响应式
// 重写数组方法: push, pop, shift, unshift 等
data.arr.push(4);
问题
- 默认一上来就对 target 进行递归监听
- 数组长度改变无效,data.arr.length = 100 无效
- 对于不存在的属性不能拦截
Reactive in Vue 3.0
思想
- Proxy 取代 Object.defineProperty,性能更好
- 在 get 触发时,才开始递归监听
- 屏蔽无效 set, 避免重复渲染
- 使用 WeakMap 保存原生对象,被代理对象,避免重复代理
- 使用 WeakMap 保存依赖关系,get 时订阅,set 时发布
- activeEffectStacks 栈顶始终保存 watchingActiveEffect,执行后立即弹出
代码
function isObject(val) {
return typeof val === 'object' && val !== null;
}
const toProxy = new WeakMap(); // { 原对象: 代理对象 }
const toRaw = new WeakMap(); // { 代理对象: 原对象 }
// 响应式核心
function reactive(target) {
return createReactive(target);
}
// 响应式工厂函数
function createReactive(target) {
if (!isObject(target)) {
return target;
}
// (2.1) 若被代理过,返回代理过的结果
if (toProxy.has(target)) {
return toProxy.get(target);
}
// (2.2) 若是被代理过的对象,则直接返回自己
if (toRaw.has(target)) {
return target;
}
const baseHandler = {
get(target, key, reciever) {
const result = Reflect.get(target, key, reciever);
console.log('获取', key);
// (1) 对于嵌套对象,在 get 时,才递归代理监听
// 订阅: 收集依赖
track(target, key);
return isObject(result) ? reactive(result) : result;
},
set(target, key, value, reciever) {
let hadKey = target.hasOwnProperty(key);
let oldVal = target[key];
const result = Reflect.set(target, key, value, reciever);
// (3) 对于数组操作,会进行两次 set,引起视图重复更新
if (!hadKey) {
console.log('新增属性', key, value);
// 发布: 触发响应 更新视图
trigger(target, key);
} else if (value !== oldVal) {
console.log('有效修改属性', key, value);
// 发布: 触发响应 更新视图
trigger(target, key);
}
return result;
},
deleteProperty(target, key, reciever) {
console.log('删除', key);
return Reflect.deleteProperty(target, key, reciever);
}
};
let observed = new Proxy(target, baseHandler);
toProxy.set(target, observed);
toRaw.set(observed, target);
return observed;
}
// -----------------------
const activeEffectStacks = []; // effect 栈, 栈顶为 watchingActiveEffect
const targetsMap = new WeakMap();
// 收集依赖
function track(target, key) {
const effect = activeEffectStacks[activeEffectStacks.length - 1];
let depsMap = targetsMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetsMap.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(effect);
}
// 触发响应
function trigger(target, key) {
let depsMap = targetsMap.get(target);
if (depsMap) {
deps = depsMap.get(key);
if (deps) {
deps.forEach(effect => {
effect();
});
}
}
}
// 响应式副作用
function watchEffect(fn) {
let effect = createReactiveWatchEffect(fn);
effect(); // 默认执行一次
}
// 响应式副作用 工厂函数
function createReactiveWatchEffect(fn) {
function effect() {
// 1. 将 effect 存入栈中
// 2. 执行 fn
return run(effect, fn);
}
return effect;
}
function run(effect, fn) {
try {
activeEffectStacks.push(effect);
fn(); // fn 有可能 throw Error
} finally {
activeEffectStacks.pop();
}
}
测试
// 测试用例
const data = {
count: 100,
name: {
a: 'hello'
},
arr: [1, 2, 3]
};
const proxy = reactive(data);
// proxy.z;
// proxy.count = 101;
// delete proxy.count;
// (1) 嵌套对象
// proxy.name.a = 'hi';
// (2) WeakMap
// (2.1) 对于代理过的对象,不重复代理
// const proxy2 = reactive(data);
// console.log(proxy2 === proxy);
// (2.2) 屏蔽多层代理
// const proxy3 = reactive(proxy);
// console.log(proxy3 === proxy);
// (3) 对于数组操作,会进行两次 set
// set 数组下标 index
// set 数组长度 length
// 对于无效操作,避免重复更新视图
// proxy.arr.push(4);
// (4) effect
watchEffect(function() {
console.log('watchEffect 执行了:', proxy.count);
});
proxy.count = 200;
问题
- 兼容性不好,如 IE 11, 要 fallback 到 2.0 的响应式