【Promise】阮一峰JavaScript教程-7-异步操作

本文详细介绍了JavaScript中的异步操作,包括单线程模型、同步任务与异步任务的区别、任务队列和事件循环、回调函数、事件监听、发布/订阅模式,以及Promise的原理、状态和用法,最后涉及了定时器和微任务的概念。
摘要由CSDN通过智能技术生成

教程官方地址:异步操作 - JavaScript 教程 - 网道

1 异步操作概述

1.1 单线程模型

JavaScript是单线程模型,只在一个线程上运行。

JavaScript引擎有多个线程,单个脚本只能在一个线程上运行。

为避免复杂性,JavaScript一开始就是单线程,将来也不会改变。优点:实现起来比较简单,执行环境相对单纯,缺点:只要有一个任务耗时很长,后面的任务就得排队等着,会拖延整个程序的执行。JavaScript语言本身不慢,慢的是读写外部数据,比如等待Ajax请求返回结果。

1.2 同步任务和异步任务

  • 程序里的任务分为两种
    • 同步任务
      • 指没有被引擎挂起、在主线程上排队执行的任务。
      • 只有前一个任务执行完毕,才能执行后一个任务
    • 异步任务
      • 指被引擎放在一边,不进入主线程、而进入任务队列的任务。
      • 只有引擎认为某个异步任务可以执行了(比如Ajax操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。
      • 排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“阻塞”效应。
  • Ajax操作可以当做同步任务处理,也可做异步任务处理
    • 同步任务
      • 主线程就等着Ajax请求返回结果,再往下执行
    • 异步任务
      • 主线程发出Ajax请求后,就直接往下执行,等到Ajax操作有了结果,主线程再执行相应的回调函数。

1.3 任务队列和事件循环

  • JavaScript运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(事实上,存在多个任务队列,为方便理解,这里假设只存在一个队列)。
  • 主线程先执行所有的同步任务。等到同步任务都执行完,就会去看任务队列里面的异步任务。
    • 如果满足条件。这里JavaScript引擎怎么知道异步任务有没有结果,能不能进入主线程呢?其实就是JavaScript引擎不停检查,只要同步任务执行完了,引擎就会去检查那些挂起的异步任务是不是可以进入主线程了。这种循环检查的机制,叫做事件循环 event loop
      • 异步任务就重新进入主线程开始执行,此时的异步任务就变成同步任务了。
      • 异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行相应的回调函数。
        • 如果一个异步任务没有回调函数,那么就不会进入任务队列。也就不会进入主线程。
    • 当任务队列清空,程序就结束执行。

1.4 异步操作的模式

以下是异步操作的几种模式。

1.4.1 回调函数

  • 回调函数是异步操作最基本的方法。
    • 优点:简单、易于理解和实现
    • 缺点:不利于代码的阅读和维护。各个部分之间高度耦合,使程序结构混乱,流程难以追踪。

1.4.2 事件监听

  • 采用事件驱动模式,异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
    • 举例:为f1绑定事件,且事件处理函数为f2
    • 优点:容易理解,可以绑定多个事件,有利于实现模块化。
    • 缺点:整个程序变成事件驱动型,运行流程变得很不清晰。阅读代码的时候,很难看出主流程。

1.4.3 发布/订阅

  • 发布/订阅模式
    • 将事件理解为一个“信号”
    • 如果存在一个信号中心,某个任务执行完成,就向信号中心发布一个信号
    • 其他任务可以向信号中心订阅这个信号,从而知道什么时候自己可以开始执行
    • 此为发布/订阅模式,又称观察者模式
  • 优点:与事件监听类似,但是优于后者
    • 可以通过查看消息中心,了解存在多少信号,每个信号有多少订阅者,从而监控程序的执行

1.5 异步操作的流程控制

如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守这种顺序。

1.5.1 串行执行

依次执行所有的异步任务。6个一秒的任务,需要执行6秒。

1.5.2 并行执行

所有异步任务同时执行。6个1秒的任务,需要执行1秒。

并行执行效率高于串行执行,但是如果并行的任务较多,容易耗尽系统资源,拖慢运行速度,因此有了第三种流程控制方式。

1.5.3 并行与串行的结合

所以串行与并行的结合,就是设置一个门槛,每次最多只能并行执行 n 个异步任务,这样就避免了过分占用系统资源。达到效率与资源的最佳平衡。

理解这个意思,但是里面的代码看不懂。

2 定时器

JavaScript提供定时执行代码的功能,叫做定时器,主要由setTimeout() 和 setInterval() 两个函数来完成。

可以去看之前JavaScript笔记的BOM和DOM操作的第7.2节。http://t.csdnimg.cn/N9ps8

这里补充一个setTimeout(f,0) 的知识。

2.1 setTimeout(f,0)

setTimeout(f,0)的f会理解执行吗?

不会。

要等到当前脚本的同步任务全部处理完以后,才会执行setTimeout指定的回调函数f,即,f会在下一轮事件循环一开始就执行。

setTimeout(f,0)的目的就是,尽可能早地执行f,但是并不能保证立刻就执行f。

实际中,setTimeout(f,0)的f不会在0毫秒后运行,不同的浏览器有不同的实现。Edge浏览器会等到4毫秒后运行。此外还有其他不同的情况。具体可以查看阮一峰的教程定时器 - JavaScript 教程 - 网道

3 Promise

3.1 概述

Promise对象是JavaScript的异步操作解决方案,为异步操作提供统一接口。

它起到代理作用,充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。

Promise是一个对象,也是一个构造函数.

function f1(resolve, reject) {
    //异步代码 ...
}

var p1 = new Promise(f1);

这里,Promise构造函数接受一个f1的回调函数作为参数,f1里面是异步操作的代码。然后返回的p1就是一个Promise实例对象。

Promise的设计思想是所有异步任务都返回一个Promise实例。

Promise实例有一个then方法,用来指定下一步的回调函数。

var p1 = new Promise(f1);
p1.then(f2);

上面代码中,f1的异步操作执行完成后,就会执行f2。

Promise使得f1和f2变成了链式写法,不仅改善了可读性,而且对于多层嵌套的回调函数尤其方便。

传统的回调函数写法使得代码混成一团,变得横向发展而不是向下发展。Promise就是为了解决这个问题,使得异步流程可以写成同步流程(外观上是同步流程)。

ECMAScript6将Promise写入语言标准,目前JavaScript原生支持Promise对象。

3.2 Promise的状态

Promise对象通过自身的状态,来控制异步操作。

  • Promise实例有3种状态。
    • 异步操作未完成  pending
    • 异步操作成功  fulfilled
    • 异步操作失败  rejected

以上,fulfilled和rejected合在一起被称为resolved(已定型)

状态转移

一旦状态发生变化,就不会再有新的状态变化,即Promise-承诺,即Promise实例的状态变化只可能发生一次。

  • Promise的最终结果只有两种,
    • 成功:fulfilled
    • 失败:rejected

3.3 Promise构造函数

JavaScript提供原生的Promise构造函数,用来生成Promise实例。

var p1 = new Promise(function(resolve, reject) {
    //...
    
    if (异步操作成功) {
        resolve(vaule);
    } else { //异步操作失败
        reject(new Error());
    }
});
  • Promise构造函数接受一个函数作为参数,这个函数两个参数分别是resolve和reject两个函数,由JavaScript引擎提供,不用自己实现。
    • resolve函数,将Promise实例的状态从 pending 变为 fulfilled,在异步操作成功时调用,将异步操作的结果作为参数传递出去。
    • reject函数,将Promise实例的状态从 pending 变为 rejected,在异步操作失败时调用,将异步操作报的错误,作为参数传递出去。

3.4 Promise.prototype.then()

  • Promise实例的then方法,用来添加回调函数。
  • then方法可以接受两个回调函数
    • 异步操作成功时的回调函数
    • 异步操作失败时的回调函数
    • 一旦状态改变,就调用相应的回调函数
var p1 = new Promise(function(resolve, reject) {
    resolve('成功');
});
p1.then(console.log, console.error);
// 成功

var p2 = new Promise(function(resolve, reject) {
    reject(new Error('失败'));
});
p2.then(console.log, console.error);
// 失败

then方法可以链式使用。

p1
    .then(step1)
    .then(step2)
    .then(step3)
    .then(
        console.log,
        console.error
    );

p1有4个then,即有4个回调函数。只要前一步的状态变为fulfilled,就会依次执行紧跟在后面的回调函数。 

为什么console.error是rejected的回调函数。

怎么确实哪个回调函数是fulfilled时的回调函数,哪个回调函数是rejected时的回调函数。

明白了,.then(异步操作成功时的回调函数,异步操作失败时的回调函数)

p1
    .then(step1)  //step1是异步操作成功时的回调函数
    .then(step2)  //step2是异步操作成功时的回调函数
    .then(step3) //step3是异步操作成功时的回调函数
    .then(
        console.log,
        console.error
    ); //console.log是异步操作成功时的回调函数,参数是step3的返回值
//console.error是异步操作失败时的回调函数,参数是p1/step1/2/3任意的返回值

3.5 then()用法辨析

简而言之,Promise的用法,就是使用then方法添加回调函数。

有以下4种写法

//写法一
f1().then(function () {
     return f2();
});

//写法二
f1().then(function () {
     f2();
});

//写法三
f1().then(f2());

//写法四
f1().then(f2);

再用then方法接一个回调函数f3.

  • 写法一的f3回调函数的参数,是f2函数的运行结果。
    //写法一
    f1().then(function () {
         return f2();
    }).then(f3);
    
  • 写法二的f3回调函数的参数是undefined
    //写法二
    f1().then(function () {
        f2();
        return;
    }).then(f3);
    
  • 写法三的f3回调函数的参数,是f2函数返回的函数的运行结果
    //写法三
    f1().then(f2())
        .then(f3);
    
  • 写法四与写法1只有1个差别,就是f2会接收到f1()返回的结果。这里有点简略了,就是f2没写括号,那么会收到f1函数的返回值。f3还是会收到f2函数的返回值。
    //写法三
    f1().then(f2)
        .then(f3);
    

3.6 实例:图片加载

3.7 小结

Promise的优点:

1. 让回调函数变成了规范的链式写法,程序流程可以看得很清楚。

2. Promise实例的状态一旦改变,无论何时查询,都能得到这个状态。

缺点:编写难度比传统写法高。

3.8 微任务

Promise的回调函数属于异步任务,会在同步任务之后执行。

new Promise(function (resolve, reject) {
    resolve(1);
}).then(console.log);

console.log(2);
//2
//1

这里console.log(2)是同步任务,而then的回调函数属于异步任务,一定晚于同步任务。

(这里,resolve的参数1,传递给then,即then里应该是console.log(1))

但是,Promise的回调函数属于微任务,与正常任务追加到下一轮事件循环不同,微任务追加到本轮事件循环,这意味着,微任务的执行时间一定早于正常任务。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值