关于前端:Javascript异步的发展与6种解决方案总结

31次阅读

共计 10921 个字符,预计需要花费 28 分钟才能阅读完成。

异步(Asynchronous)指同一时间不止一个事件产生,或者说是多个相干事件不期待前一事件实现就产生。异步解决不必阻塞以后线程来期待解决实现,而是容许后续操作,直至其它线程将解决实现,并回调告诉此线程。

当初与未来

一个残缺的 javascript 程序,简直肯定是由多个块形成的。这些块中只有一个是当初执行,其余的则会在未来执行。最常见的块单位是函数。

举个例子

function now() {return 21;}
function later() {
 answer = answer * 2;
 console.log("Meaning of life:", answer);
}
var answer = now();
setTimeout(later, 1000);

下面的例子两个块:当初执行的局部,以及未来执行的局部。

当初执行局部

function now() {return 21;}
function later() { ..}
var answer = now();
setTimeout(later, 1000);

未来执行局部

answer = answer * 2;
console.log("Meaning of life:", answer);

任何时候,只有把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点击、Ajax 响应等)时执行,你就是在代码中创立了一个未来执行的块,也由此在这个程序中引入了异步机制。程序中当初运行的局部和未来运行的局部之间的关系就是异步编程的外围。

事件循环

异步与事件循环密切相关,在理解解决方案前,倡议先看下并发模型与事件循环。

异步编程解决方案

  • 回调
  • 事件监听
  • 公布订阅
  • Promise
  • Generator
  • async/await

留神:Promise、Generator、async/await 在 IE 浏览器都不反对,须要做兼容解决。

上面介绍罕用的解决方案。

1. 回调

JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把工作的第二段独自写在一个函数外面,等到从新执行这个工作的时候,就间接调用这个函数。

毛病:大量的嵌套回调会造成回调天堂,难以保护。

// 如果下一申请依赖上一个申请的返回值,就须要一直嵌套
$.ajax({ 
  url: "url-1", 
  success: function(){
    $.ajax({ 
      url: "url-2", 
      success: function(){
        $.ajax({ 
          url: "url-3", 
          success: function(){// ...}
        });
      }
    });
  }
});

2.promise(ES6)

介绍

  • promise 是一个代表了异步操作最终实现或者失败的对象。
  • 实质上,promise 是一个函数返回的对象, 它能够绑定回调函数
  • Promise 对象是一个构造函数,用来生成 Promise 实例。
  • 应用 promise 最间接的益处就是可能应用 then 进行链式调用

创立

Promise 对象是由关键字 new 及其构造函数来创立的。

var p = new Promise((resolve, reject) => {
    // 一系列异步操作
    // resolve()
    // reject()});
console.log(p) // Promise

想要某个函数领有 promise 性能,只需让其返回一个 promise 即可。

function myAsyncFunction(url) {return new Promise((resolve, reject) => {const xhr = new XMLHttpRequest();
        xhr.open("GET", url);
        xhr.onload = () => resolve(xhr.responseText);
        xhr.onerror = () => reject(xhr.statusText);
        xhr.send();});
  };

状态

一个 Promise 有以下几种状态:

  • pending: 初始状态,既不是胜利,也不是失败状态。
  • fulfilled: 意味着操作胜利实现。状态:pengding=>fulfilled
  • rejected: 意味着操作失败。状态:pending=>rejected

一个 promise 对象处在 fulfilled 或 rejected 状态而不是 pending 状态,那么它也能够被称为 settled 状态。你可能也会听到一个术语 resolved,它示意 promise 对象处于 settled 状态。然而在平时大家都习惯将 resolved 特指 fulfilled 状态

var p1 = new Promise((resolve, reject) => {});
console.log(p1); // pending

var p2 = new Promise((resolve, reject) => {resolve('胜利');
});
console.log(p2); // fulfilled

var p3 = new Promise((resolve, reject) => {reject('失败');
});
console.log(p3); // reject

留神:promise 状态是不可逆的。

promise 的状态 只有从 pengding =》fulfilled 或者 pending =》rejected,而不会反过来。并且曾经 resolve 的数据,前面无论如何批改,都不会扭转 then 中承受到的数据。

new Promise((resolve, reject) => {
    var num = 100;
    resolve(num);
    num = 999;
    resolve(num); // resolve 也不会扭转已传出去的 num 100
    console.log(num) // 999
}).then(result => {console.log(result) // 100
});

属性

  • Promise.length:length 属性,值总是为 1
  • Promise.prototype:结构器原型

办法

iterable:一个可迭代对象,如 Array 或 String。

办法名 性能 返回后果
Promise.all(iterable) 所有传入的 promise 胜利才触发胜利,只有有一个失败就会触发失败 蕴含所有 promise 值的数组
Promise.allSettled(iterable) 所有传入的 promise 都实现(胜利 / 失败)后实现 蕴含所有 promise 值的数组
Promise.any(iterable) 当第一个胜利的 promise 胜利时返回 第一个胜利的 promise 的值
Promise.race(iterable) 当第一个 promise 胜利 / 失败返回 第一个实现的 promise 的值
Promise.reject(reason) 返回一个状态为失败的 Promise 对象 状态为失败的 Promise
Promise.resolve(value) 返回一个状态由给定 value 决定的 Promise 对象 状态为胜利的 Promise

留神:Promise.any() 办法尚未被所有的浏览器齐全反对。它以后处于 TC39 第四阶段草案(Stage 4)

原型属性

  • Promise.prototype.constructor:返回被创立的实例函数. 默认为 Promise 函数.

原型办法

办法名 性能 返回后果 阐明
Promise.prototype.then(resolFun, rejecFun) 增加 fulfilled 和 rejected 回调到以后 promise 返回新的 promise 当回调函数被调用,新 promise 都将以它的返回值来 resolve
Promise.prototype.catch(Fun) 增加一个 rejection 回调到以后 promise 返回新的 promise 返回的 promise 会以 rejection 的返回值 resolve
Promise.prototype.finally(Fun) 当其中的一个 promise 胜利时返回 返回新的 promise 无论是 fulfilled 还是 rejected,都会执行

promise/A+ 标准

Promise 标准有很多,如 Promise/A,Promise/B,Promise/D 以及 Promise/A 的升级版 Promise/A+。

ES6 中采纳了 Promise/A+ 标准。

Promises/A+ 标准总结:

  1. 一个 promise 的以后状态只能是 pending、fulfilled 和 rejected 三种之一。状态扭转只能是 pending 到 fulfilled 或者 pending 到 rejected。状态扭转不可逆。
  2. promise 的 then 办法接管两个可选参数,示意该 promise 状态扭转时的回调。then 办法必须返回一个 promise。then 办法能够被同一个 promise 调用屡次。

3.Generator(ES6)

介绍

  • Generator 函数是一个 状态机, 封装了多个外部状态
  • 执行 Generator 函数会返回一个遍历器对象,能够顺次遍历 Generator 函数外部的每一个状态
  • Generator 函数 只有调用 next() 办法才会遍历下一个外部状态

要害标识和关键字

  • function*:要害标识
  • yield:暂停执行
  • yield*:语法糖,在 Generator 函数中执行另一个 Generator 函数

办法

办法名 性能 备注
Generator.prototype.next() 返回一个由 yield 表达式生成的值
Generator.prototype.return() 返回给定的值并完结生成器
Generator.prototype.throw() 向生成器抛出一个谬误

上面对关键字和各个办法做具体介绍

yield 表达式

  • yield 示意暂停执行
  • yield 表达式前面的表达式,只有当调用 next()、外部指针指向该语句时才会执行
  • yield 表达式的值会作为返回的对象的 value 属性值
  • 调用 next() 之前,yield 后面的语句不会执行
function* helloWorldGenerator() {console.log('aaa')
  yield 'hello';
  console.log('bbb')
  yield 'world';
  console.log('ccc')
  return 'ending';
}

var hw = helloWorldGenerator();
console.log(hw); // helloWorldGenerator {<suspended>}  状态:suspended

hw.next()
// aaa {value: 'hello', done: false}

hw.next()
// bbb {value: 'world', done: false}

hw.next()
// ccc {value: 'ending', done: true}

hw.next()
// {value: undefined, done: true}

console.log(hw); // helloWorldGenerator {<closed>}   状态:closed

对于 号。尽管 ES6 没有规定,function 关键字与函数名之间的星号必须连在一起,但咱们还是依据规范的写法。即 function funName(){}

应用留神:

  • yield 只能在 Generator 函数中应用,在其余中央应用会报错。就算是在 Generator 函数内,但处于一个一般函数内,也会报错
var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a) {a.forEach(function (item) {if (typeof item !== 'number') {yield* flat(item);
    } else {yield item;}
  });
};

for (var f of flat(arr)){console.log(f);
}
// Uncaught SyntaxError: Unexpected identifier

// 革新下
var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a) {
  var length = a.length;
  for (var i = 0; i < length; i++) {var item = a[i];
    if (typeof item !== 'number') {yield* flat(item);
    } else {yield item;}
  }
};

for (var f of flat(arr)) {console.log(f);
}
// 1, 2, 3, 4, 5, 6
  • yield 表达式如果用在另一个表达式之中,必须放在圆括号外面
function* demo() {console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}
  • yield 表达式用作函数参数或放在赋值表达式的左边,能够不加括号
function* demo() {foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}

next()办法

  • next() 示意复原执行
  • next() 可承受参数,并且 该参数示意上一个 yield 表达式的返回值
  • 第一次调用 next() 时,传递参数有效。(可封装函数,先执行一次无参 next())
function* G() {
    const a = yield 100
    console.log('a', a)
    const b = yield 200
    console.log('b', b)
    const c = yield 300
    console.log('c', c)
}
var g = G()

g.next(); // {value: 100, done: false}
g.next(); // a undefined  {value: 200, done: false}
g.next(); // b undefined  {value: 300, done: false}
g.next(); // c undefined  {value: undefined, done: true}

g.next();      // {value: 100, done: false}
g.next('aaa'); // a aaa   {value: 200, done: false} 
g.next('bbb'); // b bbb   {value: 300, done: false}
g.next('ccc'); // c ccc   {value: undefined, done: true}

throw()办法

throw() 办法用来向生成器抛出异样,并复原生成器的执行,返回带有 done 及 value 两个属性的对象。

应用

  • throw 办法,能够在函数体外抛出谬误,而后在 Generator 函数体内捕捉
  • 如果 Generator 函数外部没有部署 try…catch 代码块,那么 throw 办法抛出的谬误,将被内部 try…catch 代码块捕捉
  • 如果 Generator 函数外部和内部,都没有部署 try…catch 代码块,那么程序将报错,间接中断执行
  • throw 办法能够承受一个参数,该参数会被 catch 语句接管
  • catch 语句只能捕捉到第一个 throw 办法
  • throw 办法不会中断程序执行,并且会主动执行下一次 next()

长处

多个 yield 表达式,能够只用一个 try…catch 代码块来捕捉谬误

var g = function* () {
  try {yield;} catch (e) {console.log('外部捕捉', e); 
  }
};

var i = g();
i.next();

try {var err = i.throw('参数 aaa');
  console.log(err)
  i.throw('b');
} catch (e) {console.log('内部捕捉', e);
}
// 外部捕捉 参数 aaa
// {value: undefined, done: true}
// 内部捕捉 b

如果 Generator 函数外部没有部署 try…catch 代码块,那么 throw 办法抛出的谬误,将被内部 try…catch 代码块捕捉

var g = function* () {
  yield;
  console.log('外部捕捉', e);
};

var i = g();
i.next();

try {i.throw('参数 aaa');
  i.throw('b');
} catch (e) {console.log('内部捕捉', e); // 内部捕捉 参数 aaa
}

如果 Generator 函数外部和内部,都没有部署 try…catch 代码块,那么程序将报错,间接中断执行。

var g = function* () {
  yield;
  console.log('外部捕捉', e);
};

var i = g();
i.next();
i.throw();
// VM9627:2 Uncaught undefined

throw 办法不会中断程序执行,并且会主动执行下一次 next()

var gen = function* gen(){
  try {yield console.log('a');
  } catch (e) {console.log('error')
  }
  yield console.log('b');
  yield console.log('c');
}

var g = gen();
g.next() // a
g.throw() // error b
g.next() // c

return()办法

return 办法能够返回给定的值,并且终结遍历 Generator 函数。

  • 如果 return 办法调用时,不提供参数,则返回值的 value 属性值为 undefined
  • 如果 Generator 函数外部有 try…finally 代码块,且正在执行 try 代码块,那么 return 办法会立即进入 finally 代码块,执行完当前,整个函数才会完结。
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false}
g.return('foo') // {value: "foo", done: true} =》前面的 done 始终为 true
g.next()        // { value: undefined, done: true}

Generator 函数外部有 try…finally 代码块

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally { // return 后仍然执行
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false}
g.next() // { value: 2, done: false}
g.return(7) // {value: 4, done: false}
g.next() // { value: 5, done: false}
g.next() // { value: 7, done: true}

yield* 表达式

yield* 相当于一个语法糖 (给前面的可迭代对象部署一个 for…of 循环), 用于执行 Generator 函数内的 Generator 函数

  • yield* 等同于在 Generator 函数外部,部署一个 for…of 循环
  • yield* 返回一个遍历器对象
  • 任何可迭代对象都可被 yield* 遍历
// 执行 Generator 函数内的 Generator 函数
function* foo() {
  yield 2;
  yield 3;
  return "foo";
}
function* bar() {
  yield 1;
  var v = yield* foo();
  console.log("v:" + v);
  yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}


// 任何可迭代对象都可被 yield* 遍历
let read = (function* () {
  yield* 'he';
  yield* [1, 2, 3];
})();

read.next().value // "h"
read.next().value // "e"
read.next().value // 1
read.next().value // 2
read.next().value // 3

利用

待更新 …

4.async/await(ES7)

介绍

  • async 函数是 Generator 函数的语法糖,它的写法更趋向于同步。
  • async 函数返回一个 promise 对象
  • async 函数能够蕴含 0 个或者多个 await 指令
  • await 会暂停异步函数的执行,并期待 Promise 执行,而后继续执行异步函数,并返回后果
  • await 表白之后的代码能够被认为是存在在链式调用的 then 回调办法中

async/await 的目标是简化应用多个 promise 时的同步行为,并对一组 Promises 执行某些操作。正如 Promises 相似于结构化回调,async/await 更像联合了 generators 和 promises。

async 对 Generator 的改良

改良点 async Generator
内置执行器 调用即执行 调用 next() 办法才执行
更好的语义 async(外部有异步操作)、await(期待异步操作实现后) Generator(生成器)、yield(产出)
更广的适用性 await 后能够是 Promise 对象和原始数据类型(会被主动转成 Promise.resolve()的值) yield 命令前面只能是 Thunk 函数或 Promise 对象
返回值是 Promise 返回 Promise 对象 返回 Iterator 对象

根本应用

// 异步申请
function resolveAfter1Seconds() {
  return new Promise(resolve => {setTimeout(() => {resolve('1s resolved');
    }, 1000);
  });
}
function resolveAfter3Seconds() {
  return new Promise(resolve => {setTimeout(() => {resolve('3s resolved');
    }, 3000);
  });
}


async function asyncCall() {console.log('calling');
  const result1 = await resolveAfter3Seconds();
  console.log(result1)
  const result2 = await resolveAfter1Seconds();
  console.log(result2);
  return 'ending'
}

asyncCall().then(res => {console.log(res)
});
// calling
// 3s resolved => 执行 3 秒后输入
// 1s resolved => 执行 4 秒后输入
// ending

await 命令

  • await 命令后是一个 Promise 对象,返回该对象的后果,如果不是则间接返回对应值
  • await 命令后的代码能够被认为是存在在链式调用的 then 回调办法中
async function foo() {await 1 // 原始数据类型会被主动转成 Promise.resolve()的值
}
// 等价于
function foo() {return Promise.resolve(1).then(() => undefined)
}

错误处理

  • await 命令后代码执行失败,并且有 try…catch 捕获谬误,则不会影响接下来的代码执行。
function step(val){if (val === 2) {return Promise.reject('出错了')
  } else {return  val}
}

async function main() {
  try {const val1 = await step(1);
    const val2 = await step(2);
    const val3 = await step(3);
    console.log('Final:', val1, val2, val3);
  } catch (err) {console.error('error', err);
  }
  console.log('继续执行')
}
main();
// error 出错了
// 继续执行
  • await 命令没有 try…catch, async 函数返回的 Promise 对象会被 reject,程序中断
function step(val){if (val === 2) {return Promise.reject('出错了')
  } else {return  val}
}

async function main() {const val1 = await step(1);
  const val2 = await step(2);
  const val3 = await step(3);
  console.log('继续执行')
}

main().then().catch(err => {console.log('error', err)
})
// error 出错了

async 函数的实现原理

async 函数的实现原理,就是将 Generator 函数和主动执行器,包装在一个函数里

async function fn(args) {// ...}

// 等同于

function fn(args) {return spawn(function* () {// ...});
}

function spawn(genF) {return new Promise(function(resolve, reject) {const gen = genF();
    function step(nextF) {
      let next;
      try {next = nextF();
      } catch(e) {return reject(e);
      }
      if(next.done) {return resolve(next.value);
      }
      Promise.resolve(next.value).then(function(v) {step(function() {return gen.next(v); });
      }, function(e) {step(function() {return gen.throw(e); });
      });
    }
    step(function() {return gen.next(undefined); });
  });
}

总结

  1. JS 异步编程进化史:callback -> promise -> generator -> async + await
  2. async/await 的实现,就是将 Generator 函数和主动执行器,包装在一个函数里。
  3. async/await 绝对于 Promise,劣势体现在:

    • 解决 then 的调用链,可能更清晰精确的写出代码并且也能优雅地解决回调天堂问题。
  4. async/await 对 Generator 函数的改良,体现在:

    • 内置执行器
    • 更好的语义
    • 更广的适用性
    • 返回值是 Promise 对象
  5. async/await 的毛病:

    • 如果多个异步代码没有依赖性却应用了 await 会导致性能上的升高。代码没有依赖性的话,能够应用 Promise.all 的形式代替。

正文完
 0