单线程JavaScript的异步编程

633 阅读15分钟

总览

  • 异步:现在和将来之间的时间间隙
  • 并行:能够同时发生的事情

异步编程的需求:浏览器中的JavaScript程序存在典型的事件驱动程序,基于JavaScript的服务器则通常要等待客户端发送网络请求之后执行程序

异步事件有:回调函数(错误不能被异步操作发起者获取、串联多个异步操作就需要深度嵌套的回调函数,形成回调地狱)、事件监听addEventListener/on(事件驱动,代码执行流程不清晰)、事件监听的优化版“发布/订阅方式”

最基本回调函数 ES6新增promise,通过期约链,标准化异步错误处理。
ES2017 async与await,将异步代码写成同步形式
ES2018异步迭代器和for/await循环,看起来同步的简单循环中操作异步事件流。

从OS角度理解同步异步、阻塞与非阻塞

异步与同步是对应的,它们描述的是线程之间的关系。
两个线程要么是同步,要么是异步。

阻塞与非阻塞是对于同一个线程来说,在某一时刻,线程要么处于阻塞,要么处于非阻塞。

关系
阻塞是同步机制的结果,非阻塞式使用异步机制的结果

基础

JavaScript是单线程的
即JavaScript的执行时只有一个主线程,其宿主环境(node、浏览器)都是多线程的。主线程是单线程,所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。所有阻塞的部分交给一个线程池处理,然后主线程通过事务队列跟线程池协作。
非阻塞:主线程执行到异步任务时,主线程会pending这个任务,当异步任务执行完毕会被放到异步任务队列中。异步任务队列在当前执行栈中的所有任务执行完毕后执行。

基于事件循环:主线程的执行过程就是一个 tick(同步、微任务、宏任务),不断循环

异步任务队列其实有两种,微任务(先)和宏任务(后) 在这里插入图片描述

单线程执行意味着浏览器会在脚本和事件处理程序执行期间停止响应用户输入,所以JavaScript程序员有责任保证JavaScript脚本和时间处理程序不会长时间运行。如果脚本执行计算量大的任务,就会导致文档加载延迟。所以异步或者是web worker多线程机制是必要的

promise

Promise是一个有状态的对象,可以获取异步操作的消息,状态转换不可撤销

  • pendding 正在请求,当把一件事情交给promise后
  • fulfilled/resolved 成功兑现
  • rejected 失败
var myFirstPromise = new Promise(function(resolve, reject){
    //当异步代码执行成功时,我们才会调用resolve(...), 当异步代码失败时就会调用reject(...)
    //在本例中,我们使用setTimeout(...)来模拟异步代码,实际编码时可能是XHR请求或是HTML5的一些API方法.
    setTimeout(function(){
        resolve("成功!"); //代码正常执行!
    }, 250);
});

myFirstPromise.then(function(successMessage){
    //successMessage的值是上面调用resolve(...)方法传入的值.
    //successMessage参数不一定非要是字符串类型,这里只是举个例子
    document.write("Yay! " + successMessage);
});

new Promise()传入的函数参数是会被同步立即执行的
实际的应用中会在pendding与落定之间-加入异步事件
当期约进入落定状态时,promise的处理程序会被排期,把处理程序推进消息队列,当前同步代码执行完成前不会执行。

缺点:

  • 一旦开始执行,中途无法取消
  • 不设置回调函数,promise内部抛出的错误,无法返回到外部
  • 处于pendding状态时,无法得知进展到哪一个阶段
// 借助定时器
new Promise( (resolve, reject) =>{
    cancelFn( () => {
        setTimeout( console.log, 0, 'delay cancelled');
        resolve();
    });
});
// 封装一个promise,增加一个cancel方法,方法内部
reject('cancel promise');

为避免根据读取到的期约状态,以同步方式处理期约对象。期约故意将异步行为封装起来,从而隔离外部的同步代码。即同步、异步执行的二元性:

try{
	throw new Error('foo');
}catch(e){
	console.log(e);//Error:foo成功抛出并捕获错误
}

try{
	Promise.reject(new Error('bar'));
}catch(e){
	console.log(e);
}
//Uncaught (in promise) Error:bar抛出错误却没捕获到
  • 如果不设置回调函数,Promise内部抛出的错误,不会抛到执行同步代码的线程里,因此try/catch块并不能捕获该错误
  • 拒绝期约的错误通过浏览器异步消息队列来处理的,代码一旦开始以异步模式执行,则唯一与之交互的方式就是使用异步结构——更具体的说就是期约的方法。

期约的处理程序

是链接外部同步代码与异步代码之间的桥梁,可以访问异步操作的返回数据、处理期约成功和失败的结果。连续对期约求值、添加状态转变的代码...
期约连锁即链式调用: 每个期约实例的方法then、catch、finally都会返回一个新的期约对象。 后续期约等待之前的期约,串行化异步任务。

Promise.prototype.then()

为期约实例添加处理程序的主要方法,then()方法接收最多的两个参数:onResolved处理程序onRejected处理程序。这两个参数都是可选的,如果提供的话分别会在期约进入兑现或拒绝状态时执行:


//不传解决监听器的规范写法
p2.then(null,//避免在内存上创建多余
		()=>onRejected('f'));

promise调用reject、resolve的情况决定调用then当中的哪一个函数。当有返回值却没有显式resolve的时候,都认定为resolve,执行then处理程序的第一个回调函数

var p = new Promise((resolve, reject) => {resolve(1);}).then(
    ()=>{console.log(1);return false;},
    ()=>{console.log(2);reject(1);}).then(
        (x)=>{console.log(3)},()=>{console.log(4)})
 
//1 3

实现Thenable接口即可以调用then()

const then={
	then(callback){callback('baz');}
};

在ECMAScript暴露的异步结构中,任何对象都有一个then()方法。

Promise.prototype.catch()

用于给期约添加处理程序,只接收一个参数:onRejected处理程序

  • 同步代码出错,error会沿着调用栈向上冒泡,直到碰到catch块
  • 异步期约链,error沿着期约链向下,直到碰到一个catch调用

处理错误之后可以从错误中恢复,返回一个期约;如果没有错误就会被跳过

Promise.prototype.finally()

该处理程序在期约转换为解决或拒绝状态时都会执行。可以避免onResolved和onRejected处理程序中出现重复代码,但没有传参-不知道期约的状态是解决/拒绝,所以主要用于添加清理代码

  • 与catch、then一样也会返回一个期约,但是通常被忽略。
  • finally的期约值一般取决于 调用finally的期约,但是如果finally回调抛出错误,就会用此错误拒绝返回的期约(then、catch同样)

promise的静态方法

Promise.resolve、Promise.reject

可以把任何值都转换为一个立即落定(但异步)期约,第一个参数对应解决的期约的值。

const timing = new Promise( (resolve, reject) =>{
    setTimeout(resolve('succ'),1000);   
}).then(() => console.log('p0 succ'),() => console.log('p0 error'));
console.log('sync0');
Promise.reject(timing).then(() => console.log('p1 succ'),() => console.log('p1 error'));
console.log('sync1');
//[Log] sync0
//[Log] sync1
//[Log] p0 succ
//[Log] p1 error

? 只输出2,tem处于pending

const tem = new Promise( (resolve, reject) =>{
    return Promise.reject('erro').then(()=>console.log('1'), ()=>console.log('2'));
    console.log('不会被执行');
    resolve('succ');   
}).then(()=>console.log('3'), ()=>console.log('4'));

? Uncaught (in promise) erro

const tem = new Promise( (resolve, reject) =>{
    return Promise.reject('erro');
    console.log('不会被执行');
    resolve('succ');   
}).then(()=>console.log('3'), ()=>console.log('4')).catch(()=>console.log('5'));

Promise.resolve将promise串连成一个任务队列:如果 Promise.resolve 方法的参数,是具有 then 方法的对象(又称 thenable 对象),即一个期约a,方法返回一个期约b,a被拒绝的时候b也会被拒绝,resolve亦然

var p = Promise.resolve('Hello');
 
p.then(function (s){
  console.log(s)
});
console.log("h")
//h Hello

Promise.resolve()具有幂等性

p === Promise.resolve(p);

Promise.reject(error) 不具有幂等性

Promise.reject(p); // 期约对象成为拒绝理由

throw Error错误处理

拒绝期约类似于throw表达式,在期约的执行函数或处理程序中抛出错误会导致拒绝,对应的错误对象会成为拒绝的理由。

let p1=new Promise((resolve,reject)=>reject(Error('foo')));
let p2=new Promise((resolve,reject)=>{throw Error('foo');});
p1.then(()=>{console.log('1true');},
    ()=>{console.log('1false')});
p2.then(()=>{console.log('2true');},
    ()=>{console.log('2false')});

Promise.all、Promise.race

这两个静态方法接收一个可迭代对象,返回一个新期约

  • Promise.race返回一个期约,是传入的集合中最先解决或者拒绝的期约的镜像
  • Promise.all关联性极强,有一个失败就不会继续等待其他期约的落定,但是可以在promise.all队列中,使用map每一个过滤每一个promise任务,其中任意一个报错后,return一个返回值(视为resolve),确保promise能正常执行走到.then中
getLatestJob(context){
      const result1=api.getJobJsonFromShield(context)
        .then(response => {
          return response.json();
        });
      const result2=api.getJobJson(context)
        .then(response => {
          return response.json();
        });

      Promise.all([result1, result2])
        .then(([shieldData, nbuData])=>{
          context.commit('mergeList',{"shield":shieldData,"nbuData":nbuData})

        });
    }

map:改写传入参数,return === resolve

 
Promise.all([p1, p2, p3].map(p => p.catch(e => return '出错后返回的值' )))
  .then(values => {
    console.log(values);
  }).catch(err => {
    console.log(err);
  })

可以通过这两者模拟实现以下api

none([]);
any([]);
first([]);
last([]);

Promise.allSettled

接收一个可迭代的对象,期约集合,返回一个对象数组,每个对象都有一个status属性(fulfilled、rejected)

手写一个promise

promise可以处理异步问题,其实本质上还是发布订阅模式。 首先是一个类,构造函数里有一个接收两个回调函数的执行函数,实现了then方法获取resolve或者reject的传递的结果

雏形

const PENDING = 'PENDING'; // 等待态
const FULFILLED = 'FULFILLED'; // 成功态
const REJECTED = 'REJECTED'; // 失败态

class Promise1 {
    constructor(executor) {
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined;
            
        const resolve = (value) => {
                if (this.status === PENDING) {
                    this.status = FULFILLED;
                    this.value = value;
                }
            };

        const reject = (reason) => {
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
            }
        };
            
        try {//抛出错误的情况
            executor(resolve, reject);
        } catch (error) {
            reject(error);
        }
    }// constructor
    
    then(onFulfilled, onRejected) {
        if (this.status === FULFILLED) {
            onFulfilled(this.value);
        }
        if (this.status === REJECTED) {
            onRejected(this.reason);
        }
    }
}

支持异步

怎样在resolve或者reject方法执行的时候自动触发then方法中的回调?发布订阅模式

const PENDING = 'PENDING'; // 等待态
const FULFILLED = 'FULFILLED'; // 成功态
const REJECTED = 'REJECTED'; // 失败态

class Promise {
  constructor(executor) {
    this.status = PENDING;
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];// 实现异步,发布订阅模式
    this.onRejectedCallbacks = [];
      
    const resolve = (value) => {
      if (this.status === PENDING) {
        this.status = FULFILLED;
        this.value = value;
        this.onResolvedCallbacks.forEach((fn) => fn());
      }
    };

    const reject = (reason) => {
      if (this.status === PENDING) {
        this.status = REJECTED;
        this.reason = reason;
        this.onRejectedCallbacks.forEach((fn) => fn());
      }
    }

    try {//抛出错误的情况
      executor(resolve, reject);
    } catch (error) {
        reject(error);
    }
  } // constructor
    
  then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value);
    }
    if (this.status === REJECTED) {
      onRejected(this.reason);
    }
    // 实现异步
    if (this.status === PENDING) {
      this.onResolvedCallbacks.push(() => {
        onFulfilled(this.value);
      });
      this.onRejectedCallbacks.push(() => {
        onRejected(this.reason);
      });
    }

  }// then
}

then返回一个promise支持链式调用

x为resolve或者reject的返回值 如果x是一个普通值,调用resolve处理,如果是一个promise,调用then方法。

then(onFulfilled, onRejected) {
    // 每次调用then方法 都必须返回一个全新的promise
    let promise2 = new Promise((resolve, reject) => {
    // x 就是上一个then成功或者失败的返回值,这个x决定promise2 走成功还是走失败
        if (this.status == FULFILLED) {
            try {
                let x = onFulfilled(this.value);
                resolve(x);
            } catch (e) {
                reject(e);
	        }
	    }
        if (this.status == REJECTED) {
            try {
                let x = onRejected(this.reason);
                resolve(x);
            } catch (e) {
                reject(e);
            }
        }
        if (this.status == PENDING) {
            this.onResolvedCallbacks.push(() => {
                try {
                let x = onFulfilled(this.value);
                    resolve(x);
                } catch (e) {
                    reject(e);
                }
            });
            this.onRejectedCallbacks.push(() => {
                try {
                    let x = onRejected(this.reason);
                    resolve(x);
                } catch (e) {
                    reject(e);
                }
            });
        }
    });
    return promise2;
}

防止循环调用

在promise2中获取promise2变量,使用定时器,当promise执行完时在获取

// 替换所有对x的处理 (这里只写一处例子)
setTimeout(() => {
  try {
    let x = onFulfilled(this.value);
    resolvePromise(x, promise2, resolve, reject);
  } catch (e) {
    reject(e);
  }
}, 0);
function resolvePromise(x, promise2, resolve, reject) {
    // If promise and x refer to the same object, reject promise with a TypeError as the reason
    if (x === promise2) {
        return reject(new TypeError('循环引用'));
    }
    if ((typeof x === 'object' && x !== null) || typeof x == 'function') {
    //If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
        let called = false;
        try {
            let then = x.then; 
            if (typeof then == 'function') {
                then.call(x,
                    (y) => {
                        // y有可能还是一个promise ,所以要再次进行解析流程
                        // 我需要不停的解析成功的promise中返回的成功值,直到这个值是一个普通值
                        if (called) return;
                        called = true;
                        resolvePromise(y, promise2, resolve, reject);
                    },
                    (r) => {
                        if (called) return;
                        called = true;
                        reject(r);
                    }
                );
            } else {
                resolve(x);
            }
        } catch (e) {
            if (called) return;
            called = true;
            reject(e); 
        }
    } else {
        resolve(x);
    }
}

异步函数

期约模式在函数中的应用,ES8新增规范

声明异步函数:

async function foo(){}
let foo=async ()=>{};
class P{
    //类方法
    async p(){}
}

async可以让函数具有异步特征
异步函数的返回值会被Promise.resolve()包装成一个期约对象

await

  • 异步函数主要针对不会马上完成的任务,需要一种暂停和恢复执行的能力-await
  • await只能在异步函数中使用。
  • 异步函数运行到await关键字,会记录暂停位置,等到await右边的值可用了,再向消息队列中推送一个任务来恢复异步函数的执行。
async function awaitPro(){
    console.log('awaitPro1');
    console.log(await Promise.resolve(8));
    console.log('awaitPro2');
}

async function awaitIn(){
    console.log('awaitIn1');
    console.log(await 6);
    console.log('waitIn2');
}

console.log(1);
awaitPro();
console.log(2);
awaitIn();
console.log(3);
//1 awaitPro1 2 awaitIn1 3 6 awaitIn2 8  awaitPro2

  1. 打印1
  2. 调用异步函数awaitPro();
  3. (awaitPro中)打印
  4. (awaitPro中)await暂停执行,向消息队列添加一个期约在落定之后执行的任务
  5. 期约立即落定,把await后的任务添加到消息队列
  6. awaitPro退出
  7. 打印2
  8. 调用异步函数awaitIn();
  9. (awaitIn中)打印waitIn1
  10. (awaitIn中)await暂停执行,向消息队列添加一个立即可用的值6的任务
  11. awaitIn退出
  12. 打印3
  13. 顶级线程执行完毕
  14. JavaScript运行时,从队列中取出一个解决期约的处理程序,并将解决值8提供给它
  15. JavaScript运行时,向队列添加一个恢复执行awaitPro函数的任务
  16. JavaScript运行时,从队列中取出恢复执行awaitIn的任务和值6
  17. (awaitIn中)打印6
  18. (awaitIn中)打印waitIn2
  19. waitIn()返回
  20. 异步任务完成,JavaScript运行时,从队列中取出恢复执行awaitPro的任务和值8
  21. (awaitPro中)打印8
  22. (awaitPro中)打印waitPro2

消息队列上一次添加了promise.resolve(8)、6、 执行之后添加了8 异步函数中加入promise相当于在异步中开启了另一个异步操作

使用

实现sleep

类似java中的Thread.sleep()

async function sleep(delay) {
    return new Promise((res) => setTimeout(res,delay));
}

async function fn() {
    const t0 = Date.now();
    await sleep(1500);
    console.log(Date.now()-t0);
}
fn();

平行、串行执行

不是很能理解

栈追踪和内存管理

期约和异步函数的功能有相当程度的重叠,但是两者在内存中的表现差别很大

异步迭代 for wait

重复性异步事件 在异步函数中使用

const urls = [url1, url2, url3];
const promises = urls.map( url => fetch(url) );
for( const p of promises) {
    response = await p;
    handle(response);
}
// 等同于
for await ( const response of promises) {
    handle(response);
}

这种方式会顺序异步吗?

异步迭代器

浏览器

XMLHttpRequest 基于promise的fetch api

fetch(URL)
    .then(response => {
        return response.json();
    })
    .then(profile => {
        displayUserProfile(profile);
    });

node

Node服务器底层就是异步的,定义了很多使用回调和事件的API,例如读取文件内容的默认API fs.readFile()就是异步的。

其他

注意函数式编程与promise结合,并引入响应式编程,可写出优美的回调代码

识别和判断类似于promise的值是否是真正的promise:thenable

instanceof 库或者框架可能会选择实现自己的promise,而不是使用原生ES6实现,实际上,很有可能你是在早期根本没有promise实现的浏览器中使用由库提供的promise。promise值可能是从其他浏览器窗口(iframe等)接收到的,和当前窗口/iframe的不同。

扩展

多值

promise只能有一个完成值,但是在复杂场景中会有需要

分解为多个promise信号:promise数组

promise.all().then(
    function(msg){
        const [pro1, pro2] = msg;// 数组解耦
    }
);

取消promise

很多promise抽象库提高了工具取消promise

别人的实现

这部分参考了

// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会立即执行
function MyPromise(fn) {
  let _this = this;
  _this.currentState = PENDING;
  _this.value = undefined;
  // 用于保存 then 中的回调,只有当 promise
  // 状态为 pending 时才会缓存,并且每个实例至多缓存一个
  _this.resolvedCallbacks = [];
  _this.rejectedCallbacks = [];

  _this.resolve = function (value) {
    if (value instanceof MyPromise) {
      // 如果 value 是个 Promise,递归执行
      return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = RESOLVED;
        _this.value = value;
        _this.resolvedCallbacks.forEach(cb => cb());
      }
    })
  };

  _this.reject = function (reason) {
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = REJECTED;
        _this.value = reason;
        _this.rejectedCallbacks.forEach(cb => cb());
      }
    })
  }
  // 用于解决以下问题
  // new Promise(() => throw Error('error))
  try {
    fn(_this.resolve, _this.reject);
  } catch (e) {
    _this.reject(e);
  }
}

MyPromise.prototype.then = function (onResolved, onRejected) {
  var self = this;
  // 规范 2.2.7,then 必须返回一个新的 promise
  var promise2;
  // 规范 2.2.onResolved 和 onRejected 都为可选参数
  // 如果类型不是函数需要忽略,同时也实现了透传
  // Promise.resolve(4).then().then((value) => console.log(value))
  onResolved = typeof onResolved === 'function' ? onResolved : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

  if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      // 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
      // 所以用了 setTimeout 包裹下
      setTimeout(function () {
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        // 异步执行onRejected
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      self.resolvedCallbacks.push(function () {
        // 考虑到可能会有报错,所以使用 try/catch 包裹
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });

      self.rejectedCallbacks.push(function () {
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
    }));
  }
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
  // 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
  if (promise2 === x) {
    return reject(new TypeError("Error"));
  }
  // 规范 2.3.2
  // 如果 x 为 Promise,状态为 pending 需要继续等待否则执行
  if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
      x.then(function (value) {
        // 再次调用该函数是为了确认 x resolve 的
        // 参数是什么类型,如果是基本类型就再次 resolve
        // 把值传给下个 then
        resolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    } else {
      x.then(resolve, reject);
    }
    return;
  }
  // 规范 2.3.3.3.3
  // reject 或者 resolve 其中一个执行过得话,忽略其他的
  let called = false;
  // 规范 2.3.3,判断 x 是否为对象或者函数
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 规范 2.3.3.2,如果不能取出 then,就 reject
    try {
      // 规范 2.3.3.1
      let then = x.then;
      // 如果 then 是函数,调用 x.then
      if (typeof then === "function") {
        // 规范 2.3.3.3
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            // 规范 2.3.3.3.1
            resolutionProcedure(promise2, y, resolve, reject);
          },
          e => {
            if (called) return;
            called = true;
            reject(e);
          }
        );
      } else {
        // 规范 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 规范 2.3.4,x 为基本类型
    resolve(x);
  }
}