想看Vue3的源码,想了解Vue3的响应式原理、深度代理、如何避免多次trigger,想了解Vue3的Computed、Ref、Effect,想了解Vue3的DOM-Diff、模板编译,就必须要了解一下这些基础知识:

Proxy

Proxy可用来包装一个任意类型的对象,包括数组,函数,甚至另一个代理。

对数据的处理,对构造函数的处理,对数据的验证,说白了,就是在我们访问对象前添加了一层拦截,可以过滤很多操作,而这些过滤,由你来定义。

let proxy = new Proxy(target,handler)

target:需要使用Proxy包装的目标对象,可以是任意类型的对象,包括数组、函数、甚至另一个代理;

handler:一个对象,其属性是当执行一个操作时定义代理行为的函数。

举个栗子:

let obj = {name: 'demo'};
obj = new Proxy(obj,{
    get(target,key,receiver){
        console.log('获取了getter属性');
        if(key === 'name'){
            target[key] = 'name:' + target[key];
        }
        return target[key];
    }
})
console.log(obj.name);        
//获取了getter属性
//name:demo
recevier表示Proxy或者继承Proxy的对象。

这个例子很简单,定义一个obj对象,然后通过Proxy进行了包装,此时obj成为了Proxy实例,我们对obj的操作都将通过Proxy进行拦截。

通过set也可以进行一些拦截操作:

let obj = {
    name: 'demo',
    age: 10
};
obj = new Proxy(obj,{
    set(target,key,value,receiver){
        if(key === 'age' && typeof value !== 'number'){
            throw new Error('age字段必须为Number类型');
        }
        target[key] = value;
        return true;
    }
});
obj.age = 12;
console.log(obj.age);        //12
obj.age = 'aaa';            //Error: age字段必须为Number类型
set()方法应当返回一个true,用来表示属性设置的成功

handler中不仅有get和set的拦截器,一般常用的还有:

handler.has()
handler.ownKeys()
handler.apply()
handler.construct()

handler.getPrototypeOf()
handler.setPrototypeOf()
handler.defineProperty()
handler.deleteProperty()

再举一个栗子

let obj = {
    name: 'hello',
    age: 12
};
let p = new Proxy(obj,{
    has(target,key){
        console.log('called has ' + key);
        return key in target
    },
    ownKeys(target){
        console.log('called own keys');
        let arr = Reflect.ownKeys(target);
        return arr;
    }
});
//called own keys
console.log(Object.keys(obj));        //[ 'name', 'age' ]
//called has name
console.log('name' in obj);            //true
ownKeys 必须返回可枚举对象,否则就会违反Proxy约束,抛出类型错误。类似地,has必须返回一个boolean属性的值,若拦截的对象不可扩展、不可被配置也会违反约束,抛出类型错误。
function sum(...params){
    this.sum = params.reduce((prev,current) => prev + current,0)
    return this.sum;
}
let p = new Proxy(sum,{
    apply(target,thisArg,argumentList){
        console.log('called apply fn');
        return target(...argumentList) * 10;
    },
    construct(target,argumentList,newTarget){
        return new target(...argumentList);
    }
})
//called apply fn
console.log(p(1,2,3,4));        //100
console.log(p(1,2,'3',4));
//throw new TypeError('参数必须是数字类型!')

let res = new p(1,2,3,4);
console.log(res.sum);            //10
apply用于拦截函数的调用,target为目标函数,thisArg为被调用时上下文对象(即this指向),argumentList为被调用时的参数数组

construct用户拦截new操作符,即函数发生new行为时将被拦截,因此被代理的对象必须自身具有construct内部方法。construct方法必须返回一个对象。

Reflect

Reflect不是一个函数对象/构造函数,因此它不可以用来被new。

Reflect的所有属性和方法都是静态的(就像Math一样)。

Reflect出现的最直接表现就是,保持JS的简单,举个栗子

let s = Symbo('foo');
let obj = {
    name: 'hello',
    age: 12,
    [s]: 1
}
console.log([s]);
//getOwnPropertyNames获取string类型的key;getOwnPropertySymbols获取Symbol类型的key
let res = Object,getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj))

等同于

let s = Symbo('foo');
let obj = {
    name: 'hello',
    age: 12,
    [s]: 1
}
let res = Reflect.ownKeys(obj,'name');
//[ 'name', 'age', Symbol(foo) ]
  • Reflect将Object对象的一些明显属于语言内部的方法拿了过来;
  • 修改了某些Object方法的返回结果,比如Object.defineProperty()在无法定义属性时会抛出一个错误,而Reflect.defineProperty()则会返回false;
  • 并且Reflect让Object的操作都变成了函数行为,一些命令式的操作都转为了函数式行为,例如name in obj,delete obj[name]等操作;
  • 只要是Proxy对象有的方法,Reflect与其一一对应,这可以使Proxy更方便地调用Reflect方法。

再看一下Reflect.apply的用法,它是通过指定的参数列表发起对目标函数的调用:

let ages = [11, 33, 12, 54, 18, 96];
let youngest = Reflect.apply(Math.min,Math,ages);        
console.log(youngest);        //11

可以理解为调用Math.min方法,this指向为Math,参数为ages;相比于Math.min.apply(Math,ages),Reflect的最大好处是可以避免别人也写了一个同名的apply函数时,再去写一长串代码:

Function.prototype.apply.call(context,...args)
Reflect.set(target,key,value,receiver);
Reflect.get(target,key,receiver);
Reflect.has(target,key);
Reflect.defineProperty(target,key,attributes);
Reflect.deleteProperty(target,key);
Reflect.getPrototypeOf(target);
Reflect.setPrototypeOf(target,prototype);
Reflect.isExtensible(target);        //判断一个对象是否可扩展
Reflect.getOwnPropertyDescriptor(target,key);        //返回给定属性描述符

以上列举了Reflect的方法,下面是一些属性是使用示例:

let Obj = {
    name: 'hello',
    age: 12
}
console.log(Reflect.has(Obj,'name'));        //true
console.log(Reflect.defineProperty(Obj,'sex',{value: 'female'}));        //true
console.log(Obj.sex);        //female
console.log(Reflect.deleteProperty(Obj,'age');        //true
console.log(Obj.age);        //undefined

通过这里例子的展示,能感受到反射机制主要是用来从一个类中获取额外的元信息,或者是给类的一些方法添加注解,在实时运行中获取到对应的元信息。

这不仅仅在一个对象上生效,更在Proxy及对应的traps上也有效。

WeakMap

WeakMap`对象是一组键值对的集合,其中键是弱引用,键必须是对象值可以是任意值

WeakMap对每个键对象都是弱引用,意味着在没有其他引用存在时垃圾回收能正确进行;但由于是弱引用,WeakMap的key又是不可枚举的。因此,如果你想往对象上添加数据,又不想干扰垃圾回收机制,就可以选择WeakMap

let map = new WeakMap();
let map2 = new WeakMap();

let obj1 = {};
let obj2 = function(){};
let obj3 = global;

map.set(obj1,28);
map.set(obj2,'absolute');
map.set(obj3,undefined);

console.log(map.get(obj1));        //28
console.log(map.get(obj2));     //absolute
console.log(map.get(obj3));     //undefined

console.log(map2.get(obj1));    //undefined

console.log(map.has(obj1));     //true

map.delete(obj1);

console.log(map.has(obj1));     //false

WeakMap的原型上只有四个方法:set,get,delete,has;掌握这四种方法十分容易,现在是不是对WeakMap已经了解了呢?

Set

Set对象允许存储任何类型的唯一值。它是值的集合,并且相同的元素只会出现一次,即元素是唯一的。

一个例子展示所有方法

let set = new Set();

set.add(1);
set.add(2); 
set.add(2);             //重复的值不会被添加进去
set.add(3).add(4);      //可以链式调用

console.log(set);       //Set { 1, 2 }

let obj = {
    name: 'hello',
    age: 12
};

set.add(obj);
set.add({ name: 'hello',age: 12})        //这里add的对象和obj不是同一个引用对象,因此可以add

console.log(set);       //Set { 1, 2, { name: 'hello', age: 12 }, { name: 'hello', age: 12 } }

console.log(set.has(1));             //true     若指定值存在于Set对象中则返回true
console.log(set.has(6));             //false    否则返回false

console.log(set.delete(1));          //true     成功删除指定元素则返回true
console.log(set.delete(6));          //false    否则返回false

console.log(set.has(1));             //false

let setIterator = set.entries();     

console.log(setIterator.next().value);          //[2,2]
console.log(setIterator.next().value);          //[3,3]
console.log(setIterator.next().value);          //...
console.log(setIterator.next().value);          //[ { name: 'hello', age: 12 }, { name: 'hello', age: 12 } ]


set.clear();            //用来清空Set对象中所有元素

console.log(set)        //Set {}

值得注意的是entries方法,它返回的是一个新的迭代器对象,类似[value,value]形式的数组。因为Set集合不像Map一样有key,因此为了和Map形式保持一致,使得每一个entry的key和value都拥有相同的值,所以是[value,value]形式。

小结

掌握了这些基本概念,不仅在Vue3源码分析上有所帮助,更会对前端代码将来的趋势有所体会。希望本文能给您提供一些帮助,喜欢的话就点个赞吧~
默认文件1593448383802.png


Wen前端严选
876 声望99 粉丝

精选前端前沿技术,涵盖前端完整技术体系!进阶高级前端!