异步操作

同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。异步操作执行顺序可详见进阶知识-事件循环机制

1. 回调函数 Callback

function f1(callback){
  // ...
  callback();
}

function f2(){
  // ...
}

f1(f2); // f1若为异步操作,则f2会等待f1执行完毕才运行,正常js引擎遇到异步操作,会挂起,并运行下一个任务

2. 事件监听

异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

具体详见《异步操作-事件监听》

3. 发布/订阅

事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称“观察者模式”(observer pattern)。

具体详见《异步操作-发布订阅》

4. 定时器

2.1 setTimeout()

var timerId = setTimeout(func|code, delay);

第一个参数 func|code 是将要推迟执行的函数名或者一段代码,第二个参数 delay 是推迟执行的毫秒数。返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。

// 形式1
setTimeout('console.log(2)',1000);

// 形式2
function f() {
  console.log(2);
}
setTimeout(f, 1000);

//形式3 - 后面两个参数为回调函数的参数
setTimeout(function (a,b) {
  console.log(a + b);
}, 1000, 1, 1);

注意,setTimeout 其实还能加入其他的参数,第三个参数及以后的参数都可以作为回调函数的参数。

2.2 setInterval()

与setTimeout()用法一致,setInterval指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。setTimeout函数用来指定某个函数或某段代码,在多少毫秒之后执行。

setTimeoutsetInterval函数都返回一个整数值,表示计数器编号,将该整数传入clearTimeoutclearInterval函数,就可以取消对应的定时器。

5. ES6 - Promise*

参考链接:ES6-promise

// Promise 封装异步操作

// 1. 接口封装
const promise = new Promise((resolve, reject)=>{
  // 异步代码...
  if (/* 异步操作成功 */){
    resolve(value);
  } else { /* 异步操作失败 */
    reject(new Error());
  }
})
// 或者还可以这么写
function promise() {
  return new Promise((resolve, reject)=>{
    // 异步代码...
    if (/* 异步操作成功 */){
      resolve(value);
    } else { /* 异步操作失败 */
      reject(new Error());
    }
  })
}


// 2. 业务调用接口
promise.then(successFunc, errorFunc) 
// 举例
function timeout(ms) {
    return new Promise((resolve, reject) => {
            setTimeout(resolve, ms);
        }
    )
}

timeout(100).then(result => {});

Promise 对象一旦新建它就会立即执行,无法中途取消。

then 方法通过链式方法可读性更强,then 方法可以接受两个回调函数,第一个是异步操作成功时(变为fulfilled状态)的回调函数,第二个是异步操作失败(变为rejected)时的回调函数(一般该参数可以省略)

// 写法一 - f3回调函数的参数,是f2函数的运行结果
f1().then(function () {
  return f2();
}).then(f3);;

// 写法二 - f3回调函数的参数,是undefined
f1().then(function () {
  f2();
}).then(f3);;

// 写法三 - f3回调函数的参数,是f2函数返回的函数的运行结果
f1().then(f2()).then(f3);;

// 写法四 - f3回调函数的参数,是f2函数的运行结果,并且f2会接收到f1()返回的结果
f1().then(f2).then(f3);;

Promise.prototype.catch 方法是 .then(null, rejection).then(undefined, rejection) 的别名,用于指定发生错误时的回调函数。因此在使用上建议 then 就使用单个参数表示成功回调函数,错误回调函数一律通过 catch 调用。

f1().then(function() {
  // ...
}).catch(function(error) {
  console.log('发生错误!', error);
});

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
  • Promise.all()并发处理多个异步任务,所有任务都执行成功,才能得到结果。

  • Promise.race()并发处理多个异步任务,只要有一个任务执行成功,就能得到结果。

Promise.all([promise1, promise2, promise3]).then((result) => {
    console.log(result);
});
Promise.all([promise1, promise2, promise3]).then((result) => {
    console.log(result);
});

链式调用,基于 Promise 处理多层嵌套调用:

const operation1 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(()=>{
      resolve('Operation1 is ok ')
    }, 1000)
  })
}

const operation2 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(()=>{
      resolve('Operation2 is ok ')
    }, 1000)
  })
}


const operation3 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(()=>{
      resolve('Operation3 is ok ')
    }, 1000)
  })
}

operation1().then((res) => {
  console.log(res)
  return operation2()
}).then((res) => {
  console.log(res)
  return operation3()
}).then((res) => {
  console.log(res)
})

return 后面的返回值,有两种情况:

  • 情况 1:返回 Promise 实例对象,则该实例对象会调用下一个 then

  • 情况 2:返回普通值,则直接传递给下一个 then,通过 then 参数中函数的参数接收该值

6. ES6- Generator

参考资料:ES6-Generator

Generator 函数是一个状态机,封装了多个内部状态;Generator 函数也是一个遍历器对象生成函数,返回遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

// 注意function后需要加*,函数体内部使用yield表达式,定义不同的内部状态
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator(); // 该函数有三个状态:hello,world 和 return 语句(结束执行),注意,函数名()并不是函数执行,而是一个指向内部状态的指针对象(遍历器对象)

// 调用遍历器对象的next方法,使得指针移向下一个状态,返回一个有着value和done两个属性的对象,done表示遍历是否结束。yield表达式是暂停执行的标记(还有return语句),next方法可以恢复往下执行。 
hw.next() // { value: 'hello', done: false }
hw.next() // { value: 'world', done: false }
hw.next() // { value: 'ending', done: true }
hw.next() // { value: undefined, done: true }
// yield表达式只能用在 Generator 函数里面,即使是 Generator 函数里面的函数
function* f(a){
    a.forEach((item) => {
        yield 'hello';
    })
}

// yield表达式如果用在另一个表达式之中,必须放在圆括号里面
function* demo() {
    console.log('Hello' + yield 123); // SyntaxError
    console.log('Hello' + (yield 123)); // OK
}

next 方法可以带参数,该参数就会被当作上一个 yield 表达式的返回值(第一次使用 next 方法时无上一个yeild,则传递参数是无效的),若不带参数,yield表达式对应值为undefined。

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false} 
a.next() // Object{value:NaN, done:false} - 无参数返回,则yield表达式对应的值为undefined
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

内部调用的是遍历器接口的都可以将 Generator 函数返回的 Iterator 对象作为参数,但是注意返回对象的done属性为true,遍历循环就会中止,且不包含该返回对象,也就是说return后面所带的不包含在内。

function* numbers () {
  yield 1
  yield 2
  return 3
  yield 4
}

// 扩展运算符
[...numbers()] // [1, 2]

// Array.from 方法
Array.from(numbers()) // [1, 2]

// 解构赋值
let [x, y] = numbers();
x // 1
y // 2

// for...of 循环
for (let n of numbers()) {
  console.log(n)
}
// 1
// 2

Generator 函数返回的遍历器对象的方法:

  • next() - 使得指针移向下一个状态,见上面的一些应用

  • throw() - 在函数体外抛出错误,然后在 Generator 函数体内捕获。

  • return() - 可以返回给定的值,并且终结遍历 Generator 函数。

这三个方法本质上是同一件事,它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式。

// throw 例子
var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('内部捕获', e);
  }
};
var i = g();
i.next();
try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 内部捕获 a - Generator函数内捕获错误
// 外部捕获 b - 由于Generator函数内捕获错误已经执行过了,因而在外部捕获

// return例子
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }

yield* 表达式用来在一个 Generator 函数里面执行另一个 Generator 函数。

function* foo() {
  yield 'a';
  yield 'b';
}

// yield* 表达式等同于部署一个for...of循环
function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}  // 'x' 'a' 'b' 'y' 

// 如果不加*则返回的是一个遍历器对象。
function* bar() {
  yield 'x';
  yield foo();
  yield 'y';
} // 'x' 一个遍历器对象 'y'

yield* 后面的 Generator函数,没有 return 语句时,相当于 for...of 的一种简写形式;在有 return 语句时,则需要用 var value = yield* iterator 的形式获取 return 语句的值。

function* iter1(){
    yield 'x';
    yield 'y';
}

function* iter2() {
    yield 'a';
    yield 'b';
    return 'c';
}

function* concat(iter1, iter2) {
  yield* iter1;
  const value = yield* iter2;
  yield value;
}

const a = concat(iter1(),iter2()); // x y a b c

实际上,任何数据结构只要有 Iterator 接口,就可以被 yield* 遍历。

// 数组原生支持遍历器,因此可以被yield*遍历
function* gen(){
  yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }

// 字符串同样
function* gen(){
    yield* 'hello';
}

gen().next() // { value:"h", done:false }

7. ES6 - async/await*

async 返回的是一个 promise 对象,如果 return 一个直接量,也会通过 Promise.resolve() 封装成 Promise 对象。await 后面接 Promise 对象,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果;如果后面是不是一个 Promise 对象,则表达式的运算结果就是它等到的东西。

// 例子:指定毫秒后输出内容
function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world', 50);

async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

async 函数是 Generator 函数的语法糖,其中几点改进:

  • Generator 函数的执行必须必须靠执行器 next(),而 async 函数的执行与普通函数一样,只要一行。

  • async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。

  • Generator 函数中 yield 命令后面只能是 Thunk 函数或 Promise 对象,async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值(实际会转成立即 resolved 的 Promise 对象)。

语法的注意点:

  • async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。

  • 正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

  • await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中。

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}
// 另一种写法
async function myFunction() {
  await somethingThatReturnsAPromise().catch(function (err){
    console.log(err);
  });
}

链式调用,基于 async/await 处理多层嵌套调用:

const operation1 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(()=>{
      resolve('Operation1 is ok ')
    }, 1000)
  })
}

const operation2 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(()=>{
      resolve('Operation2 is ok ')
    }, 1000)
  })
}


const operation3 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(()=>{
      resolve('Operation3 is ok ')
    }, 1000)
  })
}

async function test() {
  const res = await operation1()
  console.log(res)
  const res2 = await operation2()
  console.log(res2)
  const res3 = await operation3()
  console.log(res3)
}

test()

如果你对内容有任何疑问,欢迎提交 ❕issues✉️ email

最后更新于