JS基础系列之 —— ES6中重要但不常用的方法

3,849 阅读19分钟

前言

本次分享主要来说一下ES6中的重要但不常用的方法,这里的不常用不代表我们真的不用,只是说我们可能并不会直接使用它们,但是在日常调用其他的一些API或者语法糖的时候,底层会用到这些方法。所以除了介绍这些方法,我也会介绍一下这些方法在我们日常调用API或者语法糖底层会有使用。

一、迭代器(Iterator)

开始的开始:循环

我们从最最开始的循环开始讲起。

来,我们先来看一个数组。平时我们的开发过程中该如何循环一个数组呢,当然有很多种办法。我们来看看。

let arr = ['亨利鲁斯', '斯多葛', '时间的朋友'];

//第一个方法,我们可以用for循环对吧
for(let i = 0; i<arr.length; i++){
    console.log(arr[i]);
}

//第二个方法,除了for循环,我们还能用看起来更优雅的forEach循环
arr.forEach(item=>{
    console.log(item);
});

//第三个方法:除了上面两个,我们还可以使用for in 
for(let i in arr){
    console.log(arr[i]);
}

诶,循环数组用了这三个方法,那如果我们要循环一个字符串呢?

let str = 'abcd';

//也可以用for循环
for(let i=0; i<str.length; i++){
    console.log(str[i]);
}

//for in循环
for(let k in str){
    console.log(str[k]);
}

forEach只能遍历数组不能遍历字符串,如果要使用forEach的话,只能将字符串转换为数组。

那我们要遍历一个map对象呢?

let map = new Map();
map.set('first', '第一');
map.set('second', '第二');
map.set('third', '第三');
// 遍历map专属的一个方法,entries()
for (let item of map.entries()) { 
  console.log(item[0], item[1]); 
}

// 还有就是forEach了
map.forEach((val,key)=>{
    console.log(val,key);
})

对于上述各集合数据,我们发现没有一个循环方法是可以一次性解决上面的三种数据类型的。

虽然forEach循环不能循环字符串,但字符串可以转为数组再使用forEach输出,但这操作并不优雅,每次使用都要将它转换成数组,特别麻烦。而且forEach循环存在缺点:不能使用break,continue语句跳出循环,或者使用return从函数体返回

而for循环在有些情况写代码会增加复杂度,而且不能循环对象。

相比下,for...in的缺点是它不仅会遍历当前对象,还会遍历原型链上的可枚举属性。而且for...in主要还是为遍历对象而设计的,并不太适用于遍历数组。

比如下面这段代码👇

Array.prototype.protoValue = 'hello'; 
let arr = [1, 2, 3, 4]; 
for(let i in arr) { 
    console.log(arr[i]); // 1 2 3 4 hello 
}

我们在数组原型上添加了一个hello,for in遍历的时候不仅仅遍历了arr,还遍历了原型上的protoValue属性。

那我们话说回来,有没有一种更好的循环能一统上述循环问题? ES6就有了,用for...of循环。

for...of循环出现

它的出现解决了什么问题呢?首先是当然是能解决了上述我们的问题

我们再用for of来对上面的数组,字符串和map进行循环输出看看。

for(let v of arr) { 
    console.log(v); // 1 2 3 4 
} 

for(let v of str) { 
    console.log(v); // a b c d e 
 } 

for(let v of map) { 
    console.log(v); 
    // (2) ["first", "第一"]
    // (2) ["second", "第二"] 
    // (2) ["third", "第三"] 
}

诶,发现都能完美的进行循环取得他们的值。

来看看它的优点:

  1. 简洁直接的遍历数组。

  2. 它避开了 for-in 循环的所有缺点。

  3. 与forEach循环不同的是,它可以使用break、continue 和 return 语句。

阮一峰老师的《ECMAScript 6 入门》提到:

ES6 借鉴 C++、Java、C# 和 Python 语言,引入了for...of循环,作为遍历所有数据结构的统一的方法。
一个数据结构只要具有 iterator 接口,就可以用for...of循环遍历它的成员。
也就是说,并不是所有的对象都能使用for...of循环,只有实现了Iterator接口的对象,才能够for...of来进行遍历取值。

那么,Iterator是什么呢?我们接着说

Iterator迭代器

概念

它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

作用

  1. 为各种数据结构,提供一个统一的、简便的访问接口;

  2. 使得数据结构的成员能够按某种次序排列;

  3. 创造一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

现在明白了,Iterator的产生主要是为了使用for...of方法。但具体Iterator概念还是有些抽象,如果要直接具体的描述的话:

就是一个具有 next()方法的对象,并且每次调用next()方法都会返回一个结果对象,这个结果对象有两个属性, 如下

{
    value: 表示当前的值,
    done: 表示遍历是否结束
}

这里多了一些概念,我们来梳理一下:

Iterator是一个特殊的对象:

  1. 它具有next()方法,调用该方法就会返回一个结果对象

  2. 结果对象有两个属性值:value和done。

  3. value表示具体的返回值;done是布尔类型,表示集合是否完成遍历,没有则返回true,有就返回false。

  4. 内部有一个指针,指向数据结构的起始位置。每调用一次next()方法,指针都会向后移动一个位置,直到指向最后一个位置。

模拟Iterator

那我们就可以根据上面的概念,模拟一个迭代器

 function createIterator(arr){
    let i = 0;
    return {
        next: function(){
            let done = i >= arr.length;
            let value = !done ? arr[i++] : undefined;
            return {
              value,
              done
            }
        }
    }
}

var iterator = new createIterator([1,2,3]);

console.log(iterator.next());//{ value: 1, done: false }
console.log(iterator.next());//{ value: 2, done: false }
console.log(iterator.next());//{ value: 3, done: false }
console.log(iterator.next());//{ value: undefined, done: true }

过程:

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

  2. 第一次调用指针对象的next方法,next 方法内部通过闭包来保存指针i 的值,每次调用i都会+1,指向下一个。故第一次指针指向数据结构的第一个成员,输出1。

  3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员,输出2。

  4. 第三次调用指针对象的next方法,数组长度为3,此时数据结构的结束位置,输出3。

  5. 第四次调用指针对象的next方法,此时已遍历完成了,输出done为true表示完成,value为undefined,此后第n次调用都是该结果。

看到这里大家大概知道Iterator了,但Iterator接口主要供for...of消费。我们试着用for...of循环上面创建Iterator对象

var iterator = new createIterator([1,2,3]);
for (let value of iterator) { 
    console.log(value); // TypeError: iterator is not iterable
}

就说我们创建的iterator是不可遍历的,这样的结构不能被for of 循环,那我们怎么样的结构才是可以遍历的呢?

可迭代对象Iterable

如何实现

ES6还引入了一个新的叫Symbol的对象,symbol值是唯一的。

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。

我们就按照这个规定来给我们的iterator添加一个Symbol.iterator属性。

var arr = [1, 2, 3];

arr[Symbol.iterator] = function() {
    var _this = this;
    var i = 0;
    return {
        next: function() {
            var done = (i >= _this.length);
            var value = !done ? _this[i++] : undefined;
            return {
                done: done,
                value: value
            };
        }
    };
}

// 此时可以for...of遍历
for(var item of arr){
    console.log(item); //1,2,3
}

所以可以看到,for...of遍历的其实是对象的Symbol.iterator属性。

原生具备Iterator接口的数据结构

在ES6中,所有的集合对象,包括数组,arguments对象,typedArray(类型化数组对象,描述了一个底层的二进制数据缓冲区的一个类数组视图),DOM NodeList对象,Map和Set,还有字符串,都是可以迭代的,都有默认的迭代器,所以不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。

来看几个可迭代的数据结构的例子:

数组

let arr = [1,2,3];
let iteratorObj = arr[Symbol.iterator]();

console.log(iteratorObj.next());//{ value: 1, done: false }
console.log(iteratorObj.next());//{ value: 2, done: false }
console.log(iteratorObj.next());//{ value: 3, done: false }
console.log(iteratorObj.next());//{ value: undefined, done: true }

上面代码中,变量arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator属性上面。所以,调用这个属性,就得到遍历器对象。

arguments

我们知道普通对象是默认没有部署这个接口的,所以arguments这个属性没有在原型上,而是在对自身的属性上。

function test(){
    let obj = arguments[Symbol.iterator]();
    console.log(arguments);
    console.log(obj.next());
    console.log(obj.next());
    console.log(obj.next());
}

test(1,2,3);

NodeList

<div class="test">1</div>
<div class="test">2</div>
<div class="test">3</div>

const nodeList = document.getElementsByClassName('test')
for (const node of nodeList) {
    console.log(node);
}

Map

我们可以直接从原型上来证明

console.log(Map.prototype.hasOwnProperty(Symbol.iterator)); // true

我们熟悉的调用Iterator接口的场合

有些场合会默认调用Iterator接口(即Symbol.iterator方法),除了上文介绍到的for...of循环,还会有几个别的场合。

1、解构赋值

let set = new Set().add('a').add('b').add('c');

let [x,y] = set;
// x='a'; y='b'

let [first, ...rest] = set;
// first='a'; rest=['b','c'];

解构赋值在我们日常写代码的过程中也很常见。在需要获取对象中的一些属性时,我们就可以使用解构赋值。

submitForm(form) {
  this.$refs[form].validate((valid) => {
    if (!valid) {
        console.log("请正确输入");
    } else {
        this.$proxy({
            url: "",
            method: 'post',
            params: {
                first_name: this.form.firstName,
                last_name: this.form.lastName,
                age: this.form.age,
                gender: this.form.gender,
            }
        })
    }
  })
}

写成这样,把需要的属性都解构出来,就不用一个一个去链接了,看起来会优雅很多,不会太过于扎眼。

submitForm(form) {
  this.$refs[form].validate((valid) => {
    if (!valid) {
        console.log("请正确输入");
    } else {
        const { firstName, lastName, age, gender } = this.form;
        this.$proxy({
            url: "",
            method: 'post',
            params: {
                first_name: firstName,
                last_name: lastName,
                age,
                gender,
            }
        })
    }
  })
}

2、扩展运算符

扩展运算符底层也是使用的Iterator接口,那扩展运算符我们能如何使用呢。

A. 引用组件

我们平时引用组件大多像这样一个一个引用进文件,然后再在components里面注册。

<script>
import componentOne from './components/ComponentOne.vue';
import componentTwo from './components/ComponentTwo.vue';
import componentThree from './components/ComponentThree.vue';
import componentFour from './components/ComponentFour.vue';
import componentFive from './components/ComponentFive.vue';
import componentSix from './components/ComponentSix.vue';
import componentSeven from './components/ComponentSeven.vue';
import componentEight from './components/ComponentEight.vue';

export default {
  components: {
    componentOne,
    componentTwo,
    componentThree,
    componentFour,
    componentFive,
    componentSix,
    componentSeven,
    componentEight,
  }
}
</script>

那如果我们要引用的组件特别多呢?

想象一下import文件从componentOne到componentTwenty,光是引用组件注册组件显示一屏都放不下,看起来就特别不友好。

那这种情况下应该怎么做呢?

使用一个单独的index.js文件用来引入导出组件,然后在需要使用这些组件的文件中引入这个index文件,最后在components中注册一下就好了。这样在我们的.vue文件中一共也就两行代码。

./components/index.js

import componentOne from './components/ComponentOne.vue';
import componentTwo from './components/ComponentTwo.vue';
import componentThree from './components/ComponentThree.vue';
import componentFour from './components/ComponentFour.vue';
import componentFive from './components/ComponentFive.vue';
import componentSix from './components/ComponentSix.vue';
import componentSeven from './components/ComponentSeven.vue';
import componentEight from './components/ComponentEight.vue';

export default {
    componentOne,
    componentTwo,
    componentThree,
    componentFour,
    componentFive,
    componentSix,
    componentSeven,
    componentEight,
};

xx.vue

<script>
import components from './components';

export default {
  components: { ...components },
}
</script>

可以看看前后的对比,是不是看起来简洁了很多。

B. 还有就是使用扩展运算符浅拷贝,如果我们对象只有一层的情况下,它也是深拷贝

我们就可以利用这一点来对一些简单对象进行合并和赋值

这段代码可以看到我们其实需要export一个对象,本来我们也可以这样写return obj ,但是这样的话,外部访问的时候就会增加一个访问链接,比如说访问property1,直接return会写成obj.obj.property1,而我们使用扩展运算符进行拷贝之后就只用obj.property1就能访问到property1。

module.exports = () => {
  const obj = {
    property1: {},
    property2: {},
    property3: {},
  }

  return { ...obj }
}

下面👇这段代码是在这个变量可能会被频回到原始值的情况,这种情况就可以将初始值单独写出去,然后在使用的时候用扩展运算符直接拷贝就可以,因为只有一层,可以认为是一个深拷贝,所以也不用担心修改的时候改到初始值。

const initFormData = {
  id: null,
  text: null,
  type: 'text',
  status: 0,
}

export default {
  data() {
    return {
      formData: { ...initFormData },
    };
  }
}

3、yield*关键字

yield*后面跟的是一个可遍历的结构,执行时也会调用迭代器函数。

let foo = function* () {
  yield 1;
  yield* [2,3,4];
  yield 5;
};

yield属于Generator内容后面会专门说到,这里就不做过多描述了。

总结

ES6的出现带来了很多新的数据结构,比如 Map ,Set ,所以为了数据获取的方便,增加了一种统一获取数据的方式 for of 。 而 for of 执行的时候引擎会自动调用对象的迭代器来取值。

不是所有的对象都支持这种方式,必须是实现了Iterator接口的才可以,这样的对象我们称他们为可迭代对象。

迭代器它其实就是一个方法, 用来返回迭代器对象。

可迭代对象是部署了 Iterator 接口的对象,同时拥有正确的迭代器方法。

其实ES6里很多地方都应用了Iterator,平时可以多留意观察,多想一步,会对大家理解代码有帮助。

二、生成器

为什么需要生成器Generator

在JavaScript中,异步编程场景使用非常多,经常会现需要逐步完成多个异步操作的情况。之前用回调函数实现异步编程,如果碰到了这种问题就需要嵌套使用回调函数,异步操作越多,嵌套得就越深,导致代码的可维护性较差,代码阅读起来也很困难,就有一个词,叫回调地狱。

Generator函数是ES6提出的一种异步编程解决方案,它可以避免回调的嵌套,语法行为与传统函数完全不同。除此之外,Generator的特性在某些场景使用也十分方便。

概念

语法上,首先可以把它理解成,Generator 函数是一个状态机,还是一个Iterator对象生成函数。它返回的遍历器对象可以依次遍历Generator函数内部的每一个状态。Generator函数是生成一个了对象,但是调用的时候前面不能加new命令。

通俗来说,Generator函数它可以随时暂停函数运行,并可以在任意时候恢复函数运行。

与 Iterator 接口的关系

任意一个对象的Symbol.iterator方法,等于这个对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

由于 Generator 函数就是Iterator迭代器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

  var myIterable = {};
  myIterable[Symbol.iterator] = function*() {
    yield 1;
    yield 2;
    yield 3;
  };
  console.log([...myIterable]); // [1, 2, 3]

上面代码中,Generator 函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了 Iterator 接口,可以被...运算符遍历了。

Generator 函数执行后,返回一个迭代器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。

特点

  1. function关键字与函数名之间有一个*号。

  2. 函数体内部使用yield表达式,用来定义不同的内部状态

  3. 普通函数的执行模式是: 执行-结束, 生成器的执行模式是: 执行-暂停-结束。生成器可以在执行当中暂停自身,可以立即恢复执行也可以过一段时间之后恢复执行。最大的区别就是它不像普通函数那样保证运行到完毕。

yied关键字

生成器函数中,有一个特殊的新关键字:yield。由于Generator函数返回的是一个Iterator对象,只有调用next方法才会遍历下一个内部状态,而yield关键字就是暂停标志。因为有它,所以才能实现执行-暂停-结束的执行模式。yield后面是js的表达式。

yield关键字的优先级比较低,几乎yield之后的任何表达式都会先进行计算,然后再通过yield向外界产生值。而且yield是右结合运算符,也就是这样

yield yield 2 等价于 (yield (yield 2))

总结:

  1. 它可以指定调用方法时的返回值以及调用顺序。

  2. 每当执行完yield语句,函数就会停止执行,直到再次调用next方法才会继续执行

  3. yield关键字只能在生成器内部使用,其他地方会导致语法错误

运行生成器函数

function* gen(){ 
  yield 1; 
  yield 2; 
  yield 3;
}
let generator = gen(); // 生成器返回的是一个指向内部状态的generator对象
  console.log(generator.next()); // {value: 1, done: false}
  console.log(generator.next()); // {value: 2, done: false}
  console.log(generator.next()); // {value: 3, done: false}
  console.log(generator.next()); // {value: undefined, done: true}

首先,Generator函数,不管内部有没有yield语句,调用函数时都不会执行任何语句,也不返回函数执行结果,而是返回一个指向内部状态的generator对象,也可以看作是一个Iterator对象。只有当调用next(),内部语句才会执行。

在这个函数内部有3个yield表达式,也就是说函数有三个状态:1、2、3。而Generator函数在此分段执行,调用next方法函数内部逻辑开始执行,遇到yield表达式停止,返回Iterator对象,再次调用next方法,会从上一次停止时的yield处开始,直到最后。所以我们可以理解yield语句只是函数暂停执行的一个标记。

yield* 表达式

yield*可以将可迭代的对象iterable放在一个生成器里,生成器函数运行到yield* 位置时,将控制权委托给这个迭代器,直到执行完成为止。举个例子,数组也是可迭代对象,因此yield*也可委托给数组:

  function* gen1() {
    yield 2;
    yield 3;
  }
  function* gen2() {
    yield 1;
    yield* gen1();
    yield* [4, 5];
  }
  const generator = gen2();
  console.log(generator.next()); // {value: 1, done: false}
  console.log(generator.next()); // {value: 2, done: false}
  console.log(generator.next()); // {value: 3, done: false}
  console.log(generator.next()); // {value: 4, done: false}
  console.log(generator.next()); // {value: 5, done: false}
  console.log(generator.next()); // {value: undefined, done: true}

常用的Generator语法糖-async函数

ES2017 标准引入了 async 函数,使得异步操作变得更加方便,而async 函数是就是 Generator 函数的语法糖。

  1. async 对应的是 *

  2. await 对应的是 yield

async和await,比起*号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

感觉async await函数已经封装的很好了,平时使用基本不会有什么问题,各方面都比Generator要友好,所以大家对generator有个了解,在使用的时候知道它的底层使用的是Generator就好。

总结

Generator 是一个可以暂停和继续执行的函数,他可以完全实现 Iterator 的功能,并且由于可以保存上下文,他非常适合实现简单的状态机。另外通过一些流程控制代码的配合,可以比较容易进行异步操作。

Async/Await 就是generator进行异步操作的语法糖,该语法糖相比下有更好的应用和语义化,可以通过编写形似同步的代码来处理异步流程,从而提高代码的简洁性和可读性。

三、Proxy

概念与用法

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

var obj = new Proxy({}, {
  get: function (target, propKey, receiver) {
    console.log(`getting ${propKey}!`);
    return Reflect.get(target, propKey, receiver);
  },
  set: function (target, propKey, value, receiver) {
    console.log(`setting ${propKey}!`);
    return Reflect.set(target, propKey, value, receiver);
  }
});

比如说上面👆这段代码,对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为。对设置了拦截行为的对象obj,去读写它的属性,就会得到下面的结果。

obj.count = 1
//  setting count!
++obj.count
//  getting count!
//  setting count!
//  2

proxy在Vue3中的使用

用法大概是这么个用法,那我们就可以看看proxy在vue3中的使用

vue2如何实现的数据响应

回忆一下vue渲染的流程,在new Vue(),对Vue进行初始化的时候,就会遍历data中的数据,使用Object.definedProperty()劫持了getter和setter,在getter中做数据依赖收集的处理,在setter中监听数据的变化,并通知到订阅当前数据的地方。

那它的部分源码就会像下面这样👇

let childOb = !shallow && observe(val);
// 对 data中的数据进行深度遍历,给对象的每个属性添加响应式
          Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get: function reactiveGetter() {
              const value = getter ? getter.call(obj) : val;
              if (Dep.target) {
                // 进行依赖收集
                dep.depend();
                if (childOb) {
                  childOb.dep.depend();
                  if (Array.isArray(value)) {
                    // 是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。
                    dependArray(value);
                  }
                }
              }
              return value;
            },
            set: function reactiveSetter(newVal) {
              const value = getter ? getter.call(obj) : val;
              /* eslint-disable no-self-compare */
              if (newVal === value || (newVal !== newVal && value !== value)) {
                return;
              }
              /* eslint-enable no-self-compare */
              if (process.env.NODE_ENV !== 'production' && customSetter) {
                customSetter();
              }
              if (getter && !setter) return;
              if (setter) {
                setter.call(obj, newVal);
              } else {
                val = newVal;
              }
              // 新的值需要重新进行observe,保证数据响应式
              childOb = !shallow && observe(newVal);
              // 将数据变化通知所有的观察者
              dep.notify();
            },
          });

vue2实现的数据响应的问题

<ul id="example">
  <li v-for="item in items">
    {{ item }}
  </li>
</ul>

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
  const vm = new Vue({
    el: '#example',
    data: {
      items: ['a', 'b', 'c'],
    },
  });
  // 直接使用下标修改数据不是实时响应
  setTimeout(() => {
    vm.items[1] = 'x';
    vm.items[3] = 'd';
    console.log(vm.items);
    // 此时打印结果为 ['a', 'x', 'c', 'd'],但页面内容没有更新
  }, 500);
  // 使用 $set 修改数据是实时响应
  setTimeout(() => {
    vm.$set(vm.items, 1, 'x1');
    vm.$set(vm.items, 3, 'd1');
    console.log(vm.items);
    // 此时打印结果为 ['a', 'x1', 'c', 'd1'],页面内容更新
  }, 2000);
</script>
  1. 检测不到对象属性的添加和删除:当你在对象上新加了一个属性newProperty,当前新加的这个属性并没有加入vue检测数据更新的机制(因为是在初始化之后添加的)。vue.set是能让vue知道你添加了属性,它会给你做处理,set是能让vue知道你添加了属性, 它会给你做处理,set内部也是通过调用Object.defineProperty()去处理的

  2. 无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。

  3. 当data中数据比较多且层级很深的时候,会有性能问题,因为要遍历data中所有的数据并给其设置成响应式的。

vue3使用proxy实现

为什么使用 Proxy 可以解决上面的问题呢?我们直接看源码中createReactiveObject函数部分

function createReactiveObject(
    target: Target,
    isReadonly: boolean,
    baseHandlers: ProxyHandler<any>,
    collectionHandlers: ProxyHandler<any>,
    proxyMap: WeakMap<Target, any>
  ) {
    // 如果目标不是对象,直接返回原始值
    if (!isObject(target)) {
      return target;
    }
    // 如果目标已经是一个代理,直接返回
    // 除非对一个响应式对象执行 readonly
    if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
      return target;
    }
    // 目标已经存在对应的代理对象
    const existingProxy = proxyMap.get(target);
    if (existingProxy) {
      return existingProxy;
    }
    // 只有白名单里的类型才能被创建响应式对象
    const targetType = getTargetType(target);
    if (targetType === TargetType.INVALID) {
      return target;
    }
    const proxy = new Proxy(target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers);
    proxyMap.set(target, proxy);
    return proxy;
  }

在这个函数的逻辑部分,可以看到基础数据类型并不会被转换成代理对象,而是直接返回原始值。

并且会将已经生成的代理对象缓存进传入的 proxyMap,当这个代理对象已存在时不会重复生成,会直接返回已有对象。

也会通过 TargetType 来判断 target 目标对象的类型,Vue3 仅会对 Array、Object、Map、Set、WeakMap、WeakSet 生成代理,其他对象会被标记为 INVALID,并返回原始值。

当目标对象通过类型校验后,会通过 new Proxy() 生成一个代理对象 proxy,handler 参数的传入也是与 targetType 相关参数,比如说get,set,是否可更改之类的。最终返回已生成的 proxy 对象。

所以回顾 reactive api,我们可能会得到一个代理对象,也可能只是获得传入的 target 目标对象的原始值。

所以proxy能解决vue2中的数据响应问题主要是将对象转成了proxy,因为Proxy是拦截对象,对对象进行一个"拦截",外界对该对象的访问,都必须先通过这层拦截。无论访问对象的什么属性,之前定义的还是新增的,它都会走到拦截中。

四、最后来两个小甜点

可选链操作符(?.)

允许读取位于连接对象链深处的属性的值,而不必明确验证链中的每个引用是否有效。?. 操作符的功能类似于 . 链式操作符,不同之处在于,在引用为空(nullish ) (null 或者 undefined) 的情况下不会引起错误,该表达式短路返回值是 undefined。与函数调用一起使用时,如果给定的函数不存在,则返回 undefined。

一段垃圾代码:

<code-card
  v-if="card.info && card.info.show"
/>

在这里因为和数据交互问题,我没办法确定我在渲染的时候一定拿到了info的值,所以我直接访问card.info.show可能会报错,cannot read property 'show 'of undefined,导致整个页面都无法渲染,为了避免这个报错,我就加了个对card.info的校验,以保证它存在的时候我才读取show。

但是如果我们使用?.代码就会变成这样。

<code-card
  v-if="card.info?.show"
/>

优秀。而且我都不用担心值的问题,因为返回 ?. undefined我完全可以直接使用。

诶,定义里面说到还可以和函数一起使用,那我们再看看和函数一起怎么使用。

相信大家一定遇到过这个报错。x is not a function 。比如说,还是我的代码

delete(value, callback) {
  this.$axios
    .post('delete', {id: value})
    .then(({ data }) => {
      if (data.status !== 0) {
        if (callback) callback('fail');
        return;
      }
      if (callback) callback('success');
    });
}

这是菜单删除一个页面或者一个分组或者一个导航的代码。子组件调用父组件的函数的时候,需要根据这个函数最后的处理结果来进行不同的行为处理,就使用了一个回调函数。但是问题是三个子组件都在触发父组件的这个函数,而且并不是所有的子组件都需要回调函数。所以就写成了这样,使用了两个if,乍一看好像没什么问题。但是我们有?.这个之后,我们就可以直接这样用。

delete(value, callback) {
  this.$axios
    .post('delete', {id: value})
    .then(({ data }) => {
      if (data.status !== 0) {
        const a = callback?.();
        return;
      }
      const b = callback?.();
    });
}

虽然在这里这样使用也不好,但是这里主要是帮助理解,万一你还需要这个函数的返回值呢对吧,就可以用上了。

空值合并运算符

空值合并操作符(??)是一个逻辑操作符,当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数。

逻辑或操作符(||不同,逻辑或操作符会在左侧操作数为假值时返回右侧操作数。也就是说,如果使用 || 来为某些变量设置默认值,可能会遇到意料之外的行为。比如为假值(例如,'' 或 0)时。前两个星期的一个bug。

this.$http
  .post('', {
    url: '',
    method: 'post',
    headers: {},
    data: {
      selected: selected || -1,
    }
  });

看起来并没有什么问题,但是,在我们的selected可能的值是-1 0 1的情况下,那我们selected为0的时候就会被赋值为-1,这就导致本来应该有三种数据,但是只有两种数据。像这种,可能存在的值可能是0或者'' 的情况下,?? 就派上用场了。

我们就可以这样使用。

this.$http
  .post('', {
    url: '',
    method: 'post',
    headers: {},
    data: {
      selected: selected ?? -1,
    }
  });