• Vue响应式系统的实现底层逻辑分析
  • 发布于 1个月前
  • 83 热度
    0 评论
  • 久就旧
  • 0 粉丝 27 篇博客
  •   

用了这么久的vue, 有没有思考过vue框架最大的特点是啥?我觉得最大的特点在于:能够实现视图和函数的关联,当我们数据变化的时候,视图也会发生相应的变化。


一、什么叫视图?

如果用一个dom元素来表示一个视图的话,那么视图在计算机里面表现出来的就是一个对象。当然我们知道在Vue里面使用的是虚拟DOM,虚拟DOM表现出来的也是一个对象。同样也可以用字符串来表示视图,比如 <h1> hello world</h1>表现出来的数据形式就是一个字符串。那么视图的本质是什么? 无论是真实dom还是虚拟dom,或者一个字符串,本质上是个数据。视图也可以是数据,那我们要找的就是数据和数据之间的关联。那这有意思了,听上去好像怪怪的,这让我们想到了之前用过的Excel。Excel里面会发生大量的数据和数据之间的关联,数据和数据之间,存在着某种特定的计算,这类计算是创建视图的过程。在计算机中,过程表现为函数或方法,回到计算机中,我们要做的就是如何把 创建视图的 函数 和 数据 进行关联。


二、响应式系统的实现

通过上面的分析,会发现,关联就是为了建立一个对应关系,那么如何来建立一个对应关系?对应关系中涉及到两个东西,一个是数据,一个是 函数。数据和函数之间的关系之所以能被建立,是因为在函数的运行期间,读到了这个数据,所以就必须要去监听数据的读取,将来这个数据发生变化的时候,还要重新运行函数。除了监听数据的读取,还要监听数据的修改。js中提供了两个监听数据的方法 defineProperty 和 proxy。


2.1 defineProperty
监听范围很窄,只能通过属性描述符,去监听已有属性的读取和赋值,但是它的好处在于兼容性好。
2.2 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++
三、监听数据的读取和修改
3.1 Reflect
先来看一段代码:
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的指向。

通过Reflect,可以直接调用它的内部方法,不用经过语法层面的中转,上面的每一个内部方法,在Reflect里面都有对应的函数。同样的,要对d里面的e进行依赖收集,再那袋属性值的时候,如果是一个对象,需要对这个对象再进行一次代理并返回,这样就完成了深度代理。

3.2 读取,依赖搜集
前面讲到,读的时候,需要进行依赖收集,有人可能会问,依赖收集是啥?其实就是查找 对象的某个属性被哪个函数用到了。这样就记录了,哪个函数在读那个数据,或者说哪个被哪个函数读取了。
// 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;
        }
    }
}
依赖收集的目的是为了派发更新,收集的是运行函数的整个环境。
3.3 修改,派发更新
写入的时候需要进行派发更新,派发更新就是基于依赖收集,找到的对象的某个属性,把它对应的函数重新运行一遍就完事了;
// 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; 
}
四、如何知晓数据对应的函数
想要给依赖搜集和派发更新建立联系,就需要一个数据结构来进行保存,结构关系如下:

taegetMap是一个map结构,键是我们的对象,不管是数组,还是普通对象也好,里面包含了大量的数据。我们要知道那个数据对应哪个函数,首先要从对象出发找到这个数据。每个对象又对应一个map, 也就是propMap, propsMap中每一个属性又对应该对象的具体行为,如get、set、has之类的,存放在typeMap中;typeMap的具体操作类型,又对应dep中的具体函数。在我们进行依赖收集的时候,需要3个参数(target, key, type),分别对应某个对象(target)的某个属性(key)的某些行为(type) ;

有了这个结构还不够,告诉了某个对象的某个属性的某个行为,问题是哪个函数在用呢?需要找到这个函数,加到dep集合里面去,这里就使用一个重大问题。仔细思考一下,先不说怎么实现的问题,响应式内部并不会把所有的对象都进行依赖,最好的方式就是让用户自己决定依赖关系。
用户评论