这篇文章上次修改于 281 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

Hello, 大家!
这是进击的前端项目之JavaScript系列的异步子系列的第一篇。
进击的前端这个项目目的在于构建大前端的体系化知识,感兴趣的话可以给个Star关注一下这个项目

本篇仅是一个异步的入门理解或者分析,更高级、更深的内容将在后续文章中进行阐述,由浅入深、层层递进。

经典设计 - 链式调用

FWF论坛我发表过这样一个看法,JQuery不一定要用作项目开发,但是有必要学,甚至是深入学习其架构设计。为什么?JQuery除了使得让我们对DOM直接操作变得方便后,还体现了一大设计特点:链式调用。在大一刚接触JQuery的时候我就提出了这样一个疑惑,JQuery的链式调用到底是如何实现的?好神奇啊!是魔法吗?在前端领域持续学习后,会发现链式调用的实现非常的简单,甚至在我个人的轻量JSSeraphine中就大量使用了链式调用。但是,链式抵用原理简单是简单,如何用这些看似简单的设计模式或者基础知识去实现更优雅的设计或者技术方案,是我们需要思考的。大佬之所以是大佬,不过是将一些简单的、基础的东西玩得神乎其技罢了。

我们先来看看什么是链式调用:

const obj = new Link(el);
// some core operations...

obj.showMessage().changeMessage("message change...");   // 链式调用

这就是链式调用的样子,笔者在这里不过多阐述,我们直接来看看链式调用是如何实现的,也就是实现上述代码中注释的部分。

首先我们的需求是实现一个对象,且这个对象能够实现链式调用操作。实现展示信息和改变信息两个功能。

function Link(el) {
    this.el = el;
}

Link.prototype = {
    showMessage() {
        console.log(this.el);
        return this;
    },

    changeMessage(el) {
        this.el = el;
        this.showMessage();
        return this;
    }
}


const obj = new Link('123');
obj.showMessage();
obj.showMessage().changeMessage('321');

通过Link.prototype可以看出来,链式调用的核心便是返回相关对象的引用(注意,尽量不要像代码中一样直接修改prototype的引用)

那,链式调用跟我们的异步有什么关系呢?

异步处理,让单线程也能高性能

要想理解异步,首先我们要先理解同步是什么。同步简单来说的话就是一个个排队执行,比如踢球、洗澡、学习这三件事,不能同时做,只能一件一件来。需要前一件事有了结果(或者说结束了)再执行下一件事

如果小学有参加过数学竞赛的同学应该对华罗庚的统筹安排有所了解,那就是一个经典的异步处理。比如踢球、烧水、洗澡、学习这四件事,可以踢完球后,将水烧着然后去洗澡,洗完澡后等谁烧好后就可以倒杯水开始学习了。

在计算机中异步处理往往是针对我们的I/O操作的。比如网络请求就是一个网络I/O,文件读写就是个操作系统I/O。那为什么JS需要用到那么多的异步处理呢?因为JS是一个单线程语言,它不像Java可以多线程地处理事务。就想我们大多数普通人,是无法一心二用同时做很多事情,所以只能通过聪明的统筹安排达到较高的效率。

在早期我们通过回调函数实现JS的异步处理。回调函数简单来讲就是将一个函数(假设函数a)以参数的形式传递个另一个参数(假设函数b),然后在另一个参数(函数b)执行到的某个阶段去调用这个函数(a),函数a便被称作回调函数。

function netWorkReq(callback) {
        const data = {};
        setTimeout(() => {
        // 假设现在正在读取网络资源
        data.name = 'Uni';

        // 假设获取网络资源成功,返回数据 data
        callback(data);
    }, 3000);
}

function getData(data) {
    console.log(data);
}

netWorkReq(getData);

笔者模拟的这份代码肯定不是一个优秀的代码,但是为了力求还原一个耗时较长的I/O操作便写成这样。我们的目的在于模拟的这个网络请求拿到数据后,触发我们getData这个操作。就像是我们的目的是喝水,我们先烧着水(发起网络请求),然后去洗澡(处理后续同步操作),洗完澡水烧开了喝水(网络资源请求到,有结果了,触发getData)。

回调地狱

但是这样子很快便会产生新的问题,如果我有多个异步操作呢?我们来看看通过Node使用多个回调函数实现多个异步操作

const fs = require('fs');

fs.readFile('a.json', (err, data) => {
  // a.json 读取完后读取 b.json
  fs.readFile('b.json', (err, data) => {
      // b.json 读取完后读取 c.json
      fs.readFile('c.json', (err, data) => {
          // c.json 读取完后读取 d.json
          fs.readFile('d.json', (err, data) => {
              console.log(data);
          })
      })
  })
})

可以看到,问题是非常明显的,多个嵌套调用导致我们逻辑冗余以及可维护性差,不如我想讲读取b.json和读取d.json操作调换一下,我得重构整个bd的逻辑,这是非常难维护的。而且如果我其中某个文件读取失败,后续操作怎么办?这个状态是非常难控制的。

链式调用引出回调地狱克星 - Promise

为什么笔者说JQuery真的很值得深入了解呢?因为我们的Promise最早便是通过JQuery让全世界的开发者知道的,最后被纳入ES6标准之中。不难看出JQuery的很多设计是优秀的、不过时的、甚至是前沿的。

我们先来看看Promise的简单是用,以及Promise是如何实现的从而解决了回调带来的问题的,深入问题将在本系列的后续文章中讲解。

首先promise是通过一个叫Promise的内置构造函数实例化的。

const promise = new Promise(callback);

Promise构造函数中,我们需要传入一个回调函数,这个回调函数是关于我们异步成功和失败后的后续操作的(也就是说在异步成功或失败时触发响应的操作)

我们更新下代码:

const promise = new Promise((resolve, reject) => {
    // some operation...
    
    
    if (/* 异步操作成功 */) {
        resolve(data);  // 异步处理成功触发 resolve 这个函数,并传入我们想要返回的值    
    } else {
        reject(err);  // 异步处理失败触发 reject 这个函数,并且返回我们的错误                     
    }
});

promise.then(data => {
    console.log(data);
})

Promise对象在创建后便开始执行,当出现resolevreject两者中的一种处理结果的时候,then开始执行其回调函数。而我们的then 方法之后返回的则是一个新的promise

为什么说Promise解决了回调带来的问题呢?

  • 通过回调函数的延迟绑定清晰操作状态(成功还是错误),即从then中传入回调函数
  • 通过返回值穿透实现链式调用,即通过then中的回调函数的传入值创建新的Promise对象然后返回到外层供继续调用,清晰了异步操作结构,解决了回调带来的逻辑冗余
  • 错误冒泡机制。

这篇文章主要目的是抛出异步解决的一个切入方向,具体实现以及深入操作将会在本系列的下篇文章补全。