用了这么久的vue, 有没有思考过vue框架最大的特点是啥?我觉得最大的特点在于:能够实现视图和函数的关联,当我们数据变化的时候,视图也会发生相应的变化。
如果用一个dom元素来表示一个视图的话,那么视图在计算机里面表现出来的就是一个对象。当然我们知道在Vue里面使用的是虚拟DOM,虚拟DOM表现出来的也是一个对象。同样也可以用字符串来表示视图,比如 <h1> hello world</h1>表现出来的数据形式就是一个字符串。那么视图的本质是什么? 无论是真实dom还是虚拟dom,或者一个字符串,本质上是个数据。视图也可以是数据,那我们要找的就是数据和数据之间的关联。那这有意思了,听上去好像怪怪的,这让我们想到了之前用过的Excel。Excel里面会发生大量的数据和数据之间的关联,数据和数据之间,存在着某种特定的计算,这类计算是创建视图的过程。在计算机中,过程表现为函数或方法,回到计算机中,我们要做的就是如何把 创建视图的 函数 和 数据 进行关联。
通过上面的分析,会发现,关联就是为了建立一个对应关系,那么如何来建立一个对应关系?对应关系中涉及到两个东西,一个是数据,一个是 函数。数据和函数之间的关系之所以能被建立,是因为在函数的运行期间,读到了这个数据,所以就必须要去监听数据的读取,将来这个数据发生变化的时候,还要重新运行函数。除了监听数据的读取,还要监听数据的修改。js中提供了两个监听数据的方法 defineProperty 和 proxy。
// recative.js const targetMap = new WeakMap(); export function recative(target){ // 已经代理过了,直接返回 if(targetMap.has(target)){ return targetMap.get(target); } const proxy = new Proxy(target, { get(target, key, receiver){ // 堆代码 duidaima.com // 依赖搜集 tract(target, key); // 返回对象的属性值 const result = Reflect.get(target, key, receiver); // 深度代理 if(isObject(target)){ return reactive(result); } return result; }, set(target, key, value, receiver){ // target[key] = value; 设置对象相应的属性值 // 也可以使用反射赋值, 赋值成功返回true,否则返回fals const type = target.hasOwnProperty(key) ? TriggerOpTypes.SET : TriggerOpTypes.ADD; trigger(target, type, key); return Reflect.set(target, key, value, receiver); } has(target, key){ // 依赖搜集 tract(target, key); return Reflect.has(target, key); // 判断对象是否有相应的属性值 } }); targetMap.set(target, proxy); return proxy; } function deleteProperty(target, key){ // 原来有的key const hadKey = target.hasOwnProperty(key); // 删除对象相应的属性值 const result = Reflect.deleteProperty(target, key); if(hadKey && result){ trigger(target, TriggerOpTypes.DELETE, key); } // 派发更新 targger(target, TriggerOpTypes.DELETE, key); return result; },2.3 测试案例
// index.js import { reactive } from './reactive.js'; const state = reactive({ a: 1, b: 2 }); // 开始依赖搜集 function fn(){ state.a; state.b; } fn(); // 更新数据 state.a = "aaaa"; state.b++三、监听数据的读取和修改
const obj = { a: 1, b: 2, get c(){ console.log(this); // {a:1,b:2} this指向的obj,并不是代理对象 return this.a + this.b; }, d: { e: 4 } } const state = reactive(obj); function fn(){ state.c;// 输出3 state.d; // 输出{e:4} } fn();读取obj的c属性,会去收集什么样的依赖?读属性c的同时,又用到了属性a和b,也会收集到a和b的依赖,这是我们期望的事。如果没收集依赖,this指向的是obj,并不是代理对象,只有把这个this指向代理对象的时候,才能够收集到a和b。那么this指向怎么改变? 这个时候就需要去深入理解属性的读取。读属性其实是在运行一个内部方法 [[Get]], 传入两个属性,一个是你要读取属性的名字propertyKey,第二个属性是Receiver用来指定this的指向。
// track.js 依赖搜集 const targetMap = new WeakMap(); const TIERATE_KEY = Symbol('iterate'); // 唯一key let activeEffect = undefined; // 函数消息 let showldTrack = false; // 关联关系 export function track(target, key, type){ if(!showldTrack || !activeEffect){ return; } // 首先获取propMap let propMap = targetMap.get(target); // 没有propMap时,创建一个,并设置到targetMap中去 if(!propMap){ propMap = new Map(); targetMap.set(taeget, propMap); } if(type === TrackOpTypes.ITERATE){ key === ITERATE_KEY; } // 获取typeMap let typeMap = new Map(); // 没有typeMap时,创建一个,并设置到propMap中去 if(!typeMap){ typeMap = new Set(); propMap.set(key, typeMap) } // 获取depSet let depSet = typeMap.get(type); // 没有depSet时,创建一个,并设置到typeMap中去 if(!depSet){ depSet = new Map(); typeMap.set(type, depSet) } // 如果对象中不包含该函数, 给他设置进去 if(!depSet.has(activeEffect)){ depSet.add(activeEffect) } } // 重新收集依赖 export function effect(fn){ const effectFn = () => { try{ activeEffect = effectFn; fn(); } finally { activeEffect = null; } } }依赖收集的目的是为了派发更新,收集的是运行函数的整个环境。
// targger.js 派发更新 export function targger(target, key, type){ const effectFns = getEffectFns(target, key, type); for(const effectFn of effectFns){ effectFn(); } } function getEffectFns(target, key, type){ const proMap = targetMap.get(target); if(!propMap){ return; }; const keys = [key]; // 操作类型为新增或者删除属性的时候, 添加一个TIERATE_KEY if([TriggerOpType.ADD, TriggerOpType.DELETE].includes(type)){ keys.push(TIERATE_KEY); } const effectsFns = new Set(); // 循环去拿typeMap的每一项 for(const key of keys){ const typeMap = propMap.get(key); if(!typeMap){ continue; }; } // 最终返回我们派发更新的函数 return effectsFns; }四、如何知晓数据对应的函数