理解js异步编程

Posted 戴杭林

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了理解js异步编程相关的知识,希望对你有一定的参考价值。

Promise

  1. 背景
    • javascript语言的一大特点就是单线程,在某个特定的时刻只有特定的代码能够被执行,并阻塞其它的代码,也就是说,同一个时间只能做一件事。
    • 怎么做到异步编程?回调函数。直到nodejs的出现,开始将回调模式的异步编程机制发挥的淋漓尽致,这种机制开始在前端变得非常流行,但是慢慢也体现出了回调函数在错误处理嵌套上的副作用。
    • 因为存在上面的不足,所以异步解决方案一直在发展中,从 callback => promise => generator => async/await => rx => .....
  2. 简单了解event loop

    +javascript上, 所有同步任务都在主线程上执行,也可以理解为存在一个“执行栈”。
    • 主线程外,还有一个“任务队列”,任务队列的作用,就在等待异步任务的结果,只要异步任务有了运行结果,就会加入到“任务队列”中。
    • 一旦执行栈中所有同步任务执行完毕,就从 任务队列 中读取“任务”加入到“执行栈”中。
    • 主线程不断的在循环上面的步骤。

      
      (function() {
      
        console.log('这是开始');
      
        setTimeout(function cb() {
          console.log('这是来自第一个回调的消息');
          setTimeout(function cb3() {
            console.log("这是来自第三个回调的消息");
          })
        });
      
        console.log('这是一条消息');
      
        setTimeout(function cb1() {
          console.log('这是来自第二个回调的消息');
          setTimeout(function cb3() {
            console.log("这是来自第四个回调的消息");
          })
        });
      
        console.log('这是结束');
      
      })();
      
  3. 什么是Promise

    • Promise代指那些尚未完成的一些操作,但是其在未来某个时间会返回某个特定的结果,成功或者失败。
    • 语法上来讲,promise是一个对象,代表一个未知的值,当值返回时,Promise总是处于下面的三种状态之一:
      • pending:等待。
      • resolved: 已完成。
      • rejected: 已拒绝。
    • 状态只可能从pening -> resolved | pending -> rejected,并且一旦改变,就不会再发生变化了。
    • promise对象必须是thenable的,而且then必须返回一个promise。
  4. 为什么要使用Promise

  • 代码看起来更符合逻辑,可读性更强。
  • 解决回调地狱
  • 更好的捕获错误
  1. 举几个栗子
  • 最简单的Promise

    这里看起来很简单,但有两点是要注意的
    1. 一定要resolve或者reject,否则你的then是永远也执行不到的。
    2. promise的状态一定改变后,就再也不会发生变化了。
  let promise = new Promise(function(resolve, reject) {
    resolve("success");
    reject("fail");
  });
 promise.then((value) => {
    console.log(value);
  }).catch((reason) => {
    console.log(reason);
  });

输出结果

这个例子也充分证明了Promise只有一个状态结果,并且是不可变的

技术图片

  • 经典的回调地狱

    • 回调函数的写法

      多个异步事务多级依赖,回调函数会形成多级的嵌套,代码就会变成金字塔结构,不仅可读性不高,而且在后期的维护,调试或者重构上,都充满了风险。

    doSomething(function(result) {
    doSomethingElse(result, function(newResult) {
      doThirdThing(newResult, function(finalResult) {
        console.log('Got the final result: ' + finalResult);
      }, failureCallback);
    }, failureCallback);},
    failureCallback);
  • promise的写法

    解决了嵌套问题,thenable可以更好的支持链式调用,但还是能看到回调的影子
    更加简便的错误处理


doSomething()
????.then(function(result)?{
????????return?doSomethingElse(result);
????})
????.then(function(newResult)?{
????????return?doThirdThing(newResult);
????})
????.then(function(finalResult)?{
????????console.log('Got?the?final?result:?'?+?finalResult);
????})
????.catch(failureCallback);

// 配合箭头函数


doSomething()
????.then(result?=>?doSomethingElse(result))
????.then(newResult?=>?doThirdThing(newResult))
????.then(finalResult?=>?{
????console.log(`Got?the?final?result:?${finalResult}`);
????})
????.catch(failureCallback);
  • generator写法

    有一个暂停状态,只有我们激活next,才会去执行下一个异步任务

  function*?fuc()?{
          const?result?=?yield?doSomething()
          const?newResult?=?yield?doSomethingElse(result)
          const?finalResult?=?yield?doThirdThing(newResult)
    }
    const?test?=?fuc()
    const result = test.next()?//?{value:?1,?done:?false}
    const newResult = test.next(result.value)?//?{value:?1,?done:?false}
    const finalResult = test.next(newResult.value)?//?{value:?1,?done:?true}
    test.next()?//?{value:?undefined,?done:?true}
  • async/await的写法

    这个语法上更加简单,看起来更像同步任务,而且不需要关心执行状态。
    我理解这里只是对generator的一个包装,里面应该有个递归函数,在执行next,执行done。


async?function?useAsyncAwait()?{
????try?{
????????const?result?=?await?doSomething()
????????const?newResult?=?await?doSomethingElse()
????????const?finalResult?=?await?doThirdThing()
????????console.log('Got?the?final?result:?'?+?finalResult)
????}?catch?(e)?{
????????Console.error('exception:?',?e)
????}
}
  • 如何包装promise
 function getJSON(url) {
    return new Promise(function(resolve, reject){
      let xhr = new XMLHttpRequest();
      xhr.open('GET', url);
      xhr.onreadystatechange = handler;
      xhr.responseType = 'json';
      xhr.setRequestHeader('Accept', 'application/json');
      xhr.send();
      function handler() {
        if (this.readyState === this.DONE) {
          if (this.status === 200) {
            resolve(this.response);
          } else {
            reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']'));
          }
        }
      };
    });
  }
  
  getJSON('/posts.json')
  .then((json) =>{
    // on fulfillment
  })
  .catch((error) => {
   // on rejection
   console.log(error)
  });
  
  • 多个promise顺序执行

    promise的then,是可以保证按照你看到的顺序执行的

  getJSON('/ray/api/a/server/ping')
  .then((json) =>{
    // on fulfillment
    console.log('ping a result: ' + JSON.stringify(json));
  })
.then(() => getJSON('/ray/api/b/server/ping'))
  .then(json => {
    console.log('ping b result: ' + JSON.stringify(json)) 
  })
  .catch((error) => {
   // on rejection
   console.log(error)
  });
  
  // ping a result: {"status":"OK","serverTime":1573554742633}
 // ping b result: {"status":"OK","serverTime":1573554742667}
  • Promise 的链式调用

    举一个更具体的例子,体现thenable, promise的状态不可变


var p = new Promise(function(resolve, reject){
  resolve(1);
});
p.then(function(value){               //第一个then
  console.log(value); // 1
  return value*2;
}).then(function(value){              //第二个then
  console.log(value); // 2
}).then(function(value){              //第三个then
  console.log(value); // underfined
  return Promise.resolve('resolve'); 
}).then(function(value){              //第四个then
  console.log(value); // 'resolve'
  return Promise.reject('reject');
}).then(function(value){              //第五个then
  console.log('resolve: '+ value); // 不到这里,没有值
}, function(err){
  console.log('reject: ' + err);  // 'reject'
})
  • 引用一些第三方库 比如 Bluebird

    比如
    mapSeries => 同步的执行所有异步任务,
    all => 等待并发的任务全部执行完毕,
    any => 多个异步任务中,有一个执行完毕就结束了。
    ==


npm install bluebird

import * as Promise from "bluebird";

let data = ['a', 'c', 'b', 'e', 'd']
Promise.mapSeries(data, (d) => getJSON(d) ).then((result) => {console.log(result)})

// 执行结果应该是什么

1. 如果没有修改代码 => 只执行了第一个a,后面的都应该第一个a出错,所以不继续执行了

2. 如果将getJSON里的reject改成resoleve => a c b e d的出错log会按顺序打印
  • 多个promise并发执行

Promise.all([ getJSON('/ray/api/a/server/ping'), getJSON('/ray/api/b/server/ping')]).then((result) => {
    console.log('ping result: ' + JSON.stringify(result));
})

// ping result: [{"status":"OK","serverTime":1573554934072},{"status":"OK","serverTime":1573554934070}]
  • 多个promise之间的竞争

promise 无法取消执行
new Promise 里的任务会立即执行


const delay = new Promise((resolve, reject) => { 
    setTimeout(() => {
        console.log('timeout');
        resolve('timeout');
    }, 3000)
})

Promise.race([ getJSON('/ray/api/a/server/ping'), delay]).then((result) => {
    console.log('ping result: ' + JSON.stringify(result));
})

// 思考下这里的timeout会不会打印
  • Promise 的穿透

promise 的then必须接收函数,否则会穿透。


// 这里`Promise.resolve(2)`并不是函数,所以上一个函数的结果会穿透到下一个
Promise.resolve(1).then(Promise.resolve(2)).then((v) => {
  console.log(v)
})

// 语法错误
Promise.resolve(1).then(return Promise.resolve(2)).then((v) => {
  console.log(v)
})

// 穿透
Promise.resolve(1).then(null).then((v) => {
  console.log(v)
})

// 语法错误
Promise.resolve(1).then(return 2).then((v) => {
  console.log(v)
})

// then会返回新的promise,并且带上他返回的结果
Promise.resolve(1).then(() => {
  return 2
}).then((v) => {
  console.log(v)
})

当then()受非函数的参数时,会解释为then(null),这就导致前一个Promise的结果穿透到下面一个Promise。所以要提醒你自己:永远给then()传递一个函数参数。

  • 错误的捕获

    一旦捕获到错误,promise的then会继续执行
    catch会检查promis链上位于它之前的每个地方(then或者其他异步操作)。如果在它之前还有其他catch,那么起点就是上一个catch。

var p1 = new Promise( function(resolve,reject){
  foo.bar();
  resolve( 1 );   
});

p1.then(
  function(value){
    console.log('p1 then value: ' + value);
  },
  function(err){
    console.log('p1 then err: ' + err);
  }
).then(
  function(value){
    console.log('p1 then then value: '+value);
  },
  function(err){
    console.log('p1 then then err: ' + err);
  }
);

轻喷!

以上是关于理解js异步编程的主要内容,如果未能解决你的问题,请参考以下文章

JS异步编程,1/3

深入理解JS异步编程

Node.js&Promise的新理解&记一次异步编程的错误尝试

深入理解node.js异步编程

深入理解node.js异步编程:基础篇

JavaScript异步编程