一、什么是响应式?
- 有一个初始化的值,有一段代码使用了这个值;
- 在有一个新的值时,这段代码可以自动重新执行;
let initial = 10; //使用了这个值 console.log(10); //有一个新的值 console.log(initial * 2); //可以自动重新执行 initial = 20; - 这样一种可以自动响应数据变量的代码机制,我们就称之为是响应式的;
1.1. 对象的响应式
- 现在有一个响应式对象;
const obj = { name: 'yzh', age: 18 }; - 当对象里的数据发生变更时,需要执行上百行代码:
const newName = obj.name; console.log("zheshiyiduanmiaoshu"); console.log("Hello World"); console.log(obj.name) // 100行 obj.name = "luffy";
二、响应式函数设计
- 执行的代码中可能不止一行代码,所以可以将这些代码放到一个函数中;
- 当数据发生变化时,自动去执行某一个函数;
2.1. 响应式函数的实现
- 1)封装一个新的函数watchFn;
- 2)传入到watchFn的函数,就是需要响应式的;
- 3)默认定义的函数都是不需要响应式的;
const reactiveFns = []; //收集需要响应的函数 function watchFn(fn) { reactiveFns.push(fn) }; const obj = { name: "yzh", age: 18 }; //依赖响应式对象的函数 watchFn(function() { const newName = obj.name; console.log("zheshiyiduanmiaoshu"); console.log("Hello World"); console.log("Hello World1"); console.log("Hello World2"); console.log("Hello World3"); console.log("Hello World4"); console.log(obj.name) // 100行 }); //依赖响应式对象的函数 watchFn(function() { console.log('watchFn: ', obj.name); }); function foo() { console.log('普通函数/默认定义的函数'); }; obj.name = 'ace'; reactiveFns.forEach(fn => { fn() });
三、响应式依赖的收集
- 目前我们收集的依赖是放到一个数组中来保存的,但是这里会存在数据管理的问题:
- 在实际开发中需要监听很多对象的响应式;
- 这些对象需要监听的不只是一个属性。它们很多属性的变化,都会有对应的响应式函数;
- 所以不可能在全局维护一大堆的数组来保存这些响应函数;
- 设计一个类,这个类用于管理某一个对象的某一个属性的所有响应式函数;
- 相当于替代了原来的简单 reactiveFns 的数组;
class Depend { constructor() { this.reactiveFns= [] } addDepend(reactiveFn) { this.reactiveFns.push(reactiveFn) } notify() { this.reactiveFns.forEach(fn => { fn() }) } } const depend = new Depend(); function watchFn(fn) { depend.addDepend(fn) } const obj = { name: "yzh", age: 18 }; watchFn(function() { const newName = obj.name; console.log("zheshiyiduanmiaoshu"); console.log('需要响应的代码块1'); console.log(obj.name) // 100行 }); watchFn(function() { console.log('watchFn: ', obj.name); }); function foo() { console.log('普通函数/默认定义的函数'); }; obj.name = 'ace'; depend.notify();
四、监听对象的变化
- 两种方式:
- 1)通过 Object.defineProperty 的方式(vue2采用的方式);
- 2)通过 new Proxy 的方式(vue3采用的方式);
- 这里使用Proxy的方式来自动监听对象变化:
//原来 //obj.name = 'ace'; //depend.notify(); //更改为: const objProxy = new Proxy(obj, { get(target, key, receiver) { return Reflect.get(target, key, receiver); }, set(target, key, newVal, receiver) { Reflect.set(target, key, newVal, receiver); depend.notify(); } }); objProxy.name = 'ace';
五、对象的依赖管理
- 目前是创建了一个Depend对象,用来管理对于name变化需要监听的响应函数;
- 通过WeakMap来管理不同对象的不同依赖关系;
5.1. 对象依赖管理的实现
- 监听对象的变化代码重构
const targetMap = new WeakMap(); const objProxy = new Proxy(obj, { get(target, key, receiver) { return Reflect.get(target, key, receiver); }, set(target, key, newVal, receiver) { Reflect.set(target, key, newVal, receiver); let map = targetMap.get(target); if (!map) { map = new Map(); targetMap.set(target, map); }; let depend = map.get(key); if (!depend) { depend = new Depend(); map.set(key, depend); }; depend.notify(); } });
5.2. 正确的依赖收集
- 收集依赖watchFn代码重构;
- 如果一个函数中使用了某个对象的key,那么它应该被收集依赖;
//原来: //function watchFn(fn) { // depend.addDepend(fn); //}; //更改为: let activefn = null; function watchFn(fn) { activefn = fn; //7.调用函数会触发get fn(); activefn = null; }; // 封装一个获取depend的函数 const targetMap = new WeakMap(); function getDepend(target, key) { let map = targetMap.get(target); if (!map) { map = new Map(); targetMap.set(target, map); }; let depend = map.get(key); if (!depend) { depend = new Depend(); map.set(key, depend); }; return depend; }; const objProxy = new Proxy(obj, { get(target, key, receiver) { const depend = getDepend(target, key); depend.addDepend(activefn); return Reflect.get(target, key, receiver); }, set(target, key, newVal, receiver) { Reflect.set(target, key, newVal, receiver); const depend = getDepend(target, key); depend.notify(); } }); watchFn(function() { console.log(objProxy.name, 'name需要响应的代码块'); }); watchFn(function() { console.log(objProxy.age, 'age需要响应的代码块'); }); objProxy.name = 'ace'; /* yzh name需要响应的代码块 18 age需要响应的代码块 ace name需要响应的代码块 */
六、Depend重构
- 问题一:如果函数中有用到两次key,比如name,那么这个函数会被收集两次;
- 解决:不使用数组,而是使用Set;
- 问题二:我们并不希望将添加reactiveFn放到get中,因为它是属于Dep的行为;
- 解决:addDepend方法优化;
let activefn = null; class Depend { constructor() { // this.reactiveFns = []; this.reactiveFns = new Set(); } // addDepend(reactiveFn) { // this.reactiveFns.push(reactiveFn) // } addDepend() { if (activefn) { this.reactiveFns.add(activefn); } } notify() { this.reactiveFns.forEach(fn => { fn(); }) } } const objProxy = new Proxy(obj, { get(target, key, receiver) { const depend = getDepend(target, key); // depend.addDepend(activefn); depend.addDepend(); return Reflect.get(target, key, receiver); }, set(target, key, newVal, receiver) { Reflect.set(target, key, newVal, receiver); const depend = getDepend(target, key); depend.notify(); } })
七、创建响应式对象
- 目前的响应式是针对于obj一个对象的,我们可以创建出来一个函数,针对所有的对象都可以变成响应式对象;
- 完整代码:
let activefn = null; // 3. class Depend { constructor() { this.reactiveFns = new Set(); } addDepend() { if (activefn) { this.reactiveFns.add(activefn); } } notify() { this.reactiveFns.forEach(fn => { fn(); }) } }; // 1. function watchFn(fn) { activefn = fn; //7.调用函数会触发get fn(); activefn = null; }; // 5.封装一个获取depend函数 const targetMap = new WeakMap(); function getDepend(target, key) { // 7.1.根据target对象获取map,第一次肯定是没有所以设置进去 let map = targetMap.get(target); if (!map) { map = new Map(); targetMap.set(target, map); }; // 根据key获取depend对象,第一次肯定是没有所以设置进去 let depend = map.get(key); if (!depend) { depend = new Depend(); map.set(key, depend); }; return depend; }; function reactive(obj) { return new Proxy(obj, { get(target, key, receiver) { //根据target key获取对应的depend const depend = getDepend(target, key); depend.addDepend(); return Reflect.get(target, key, receiver); }, set(target, key, newVal, receiver) { Reflect.set(target, key, newVal, receiver); // 6.拿到属于变化的depend调用notify const depend = getDepend(target, key); depend.notify(); } }); }; const info = reactive({ name: 'luffy' }); watchFn(() => { console.log(info.name); }); info.name = 'yzh';
八、Vue2响应式原理
- 可以将reactive函数进行如下的重构:
- 在传入对象时,我们可以遍历所有的key,并且通过属性存储描述符来监听属性的获取和修改;
- 在setter和getter方法中的逻辑和前面的Proxy是一致的;
function reactive(obj) { Object.keys(obj).forEach(key => { let value = obj[key]; Object.defineProperty(obj, key, { get() { const depend = getDepend(obj, key); depend.addDepend(); return value; }, set(newVal) { value = newVal; const depend = getDepend(obj, key); depend.notify(); } }) }); return obj; };
V3为什么不用 Object.defineProperty ???