闽公网安备 35020302035485号
用了这么久的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;
}
四、如何知晓数据对应的函数