JS处理异步的方式(callback、监听事件、promise对象、async )

3,228 阅读9分钟

定义:Javascript语言的执行环境是"单线程"(single thread,就是指一次只能完成一件任务。如果有多个任务,就必须排队,前面一个任务完成,再执行后面一个任务,以此类推)

优缺点:

优点:实现起来比较简单,执行环境相对单纯。

缺点:只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。

结果:浏览器出现假死现象。

解决方案:

Javascript语言将任务的执行模式分成两种:同步(Synchronous)和异步(Asynchronous)

处理异步的方法:

回掉函数(callback)

回调是一个函数被作为一个参数传递到另一个函数里,在那个函数执行完后再执行。

假定有两个函数f1和f2,后者等待前者的执行结果。

f1()
f2()
function f1(callback) {
    setTimeout(function () {
        // f1的任务代码
        callback();
    }, 1000);
}
// 执行
f1(f2)

回调不一定是异步

事件监听

采用事件驱动模式。

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

监听函数有:on,bind,listen,addEventListener,observe。

  1. 首先,为f1绑定一个事件(采用jquery写法)。
f1.on('done',f2); // 当f1发生done事件,就执行f2

  1. f1改写
function f1(){
    settimeout(function(){
       //f1的任务代码
       f1.trigger('done');  // 执行完成后,立即触发done事件,从而开始执行f2.
    },1000);
}
发布/订阅

假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"

例子:

1、f2向"信号中心"jQuery订阅"done"信号

jQuery.subscribe("done", f2);

2、 f1可以如下图所写

function f1(){
    setTimeout(function () {
        // f1的任务代码
       jQuery.publish("done");  // f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行
    }, 1000);
}

3、 f2完成执行后,也可以取消订阅(unsubscribe)

jQuery.unsubscribe("done", f2);

promise

概括:

Promise是异步编程的一种解决方案,可以替代传统的解决方案--回调函数和事件。

主要解决问题:

  • 回调地狱,代码难以维护,常常第一个函数的输出时第二个函数的输入这种现象。
  • promise可以支持多个并发的请求,获取并发请求中的数据

ES6统一了用法,并原生提供了Promise对象。作为对象,Promise有一下两个特点:

(1)对象的状态不受外界影响。

(2)一旦状态改变了就不会在变,也就是说任何时候Promise都只有一种状态。

promise的状态:
  • promise有两种状态:1、等待(pending);2、完成(settled)。

promise会一直处于等待状态,直到它所包装的异步调用返回/超时/结束。

  • 这时候promise状态变成完成状态。完成状态分成两类:1、解决(resolved);2、拒绝(rejected)。

  • promise解决(resolved):意味着顺利结束。promise拒绝(rejected)意味着没有顺利结束。

基本用法:

通过Promise的构造函数创建Promise对象。

var promise = new Promise(function(resolve,reject)
setTimeout(function(){                              
  console.log("hello world");},2000);
});

总结:Promise构造函数接收一个函数作为参数,该函数的两个参数是resolve,reject,它们由JavaScript引擎提供。其中resolve函数的作用是当Promise对象转移到成功,调用resolve并将操作结果作为其参数传递出去;reject函数的作用是单Promise对象的状态变为失败时,将操作报出的错误作为其参数传递出去。

Promise的方法:
  • then

promise的then方法带有以下三个参数:成功回调,失败回调,前进回调,一般情况下只需要实现第一个,后面是可选的。Promise中最为重要的是状态,通过then的状态传递可以实现回调函数链式操作的实现。

function greet(){
var promise = new Promise(function(resolve,reject){
    var greet = "hello  world";
    resolve(greet);
});
return promise;
}
var p = greet().then(v=>{
console.log(v);  // promise 
})

console.log(p); hello world

解析: 可以看出promise执行then还是一个promise,并且Promise的执行是异步的,因为hello world在最后一条输出语句的前面就打印出来,且Promise的状态为pending(进行中)。

  • all

Promise的all方法提供了并行执行异步操作的能力,在all中所有异步操作结束后才执行回调。

function p1(){
    var promise1 = new Promise(function(resolve,reject){
        console.log("p1的第一条输出语句");
        console.log("p1的第二条输出语句");
        resolve("p1完成");
    })
    return promise1;
}

function p2(){
    var promise2 = new Promise(function(resolve,reject){
        console.log("p2的第一条输出语句");
        setTimeout(()=>{console.log("p2的第二条输出语句");resolve("p2完成")},2000);

    })
    return promise2;
}

function p3(){
    var promise3 = new Promise(function(resolve,reject){
        console.log("p3的第一条输出语句");
        console.log("p3的第二条输出语句");
        resolve("p3完成")
    });
    return  promise3;
}

Promise.all([p1(),p2(),p3()]).then(function(data){
    // 三个都成功则成功
    console.log(data);
    },function(){
    // 只要有失败,则失败
    }
})

答案:

p1的第一条输出语句
p1的第二条输出语句
p2的第一条输出语句
p3的第一条输出语句
p3的第二条输出语句
p2的第二条输出语句

['p1完成''p2完成','p3完成']
p的状态由p1、p2、p3决定,分成两种情况。

(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

  • race()
const p = Promise.race([p1, p2, p3]);

解析:只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

  • catch用法

相当于reject状态

//promise
var p=new Promise(function(resolved))
//在这里进行处理。也许可以使用ajax
setTimeout(function(){
   var result=10*5;
   if(result===50){
      resolve(50);
   }else{
     reject(new Error('Bad Math'));
  }
},1000);
});
p.then(function(result){
    console.log('Resolve with a values of %d',result);
});
p.catch(function(){
   console.error('Something went wrong');
});

上述代码的解释:

1、代码的 关键在于setTimeout()的调用。

2、重要的是,他调用了函数resolve()和reject()。resolve()函数告诉promise用户promise已解决;reject()函数告诉promise用户promise未能顺利完成。

3、另外还有一些使用了promise代码。注意then和catch用法,可以将他们想象成onsucess和onfailure事件的处理程序。

4、巧妙地方是,我们将promise处理与状态分离。也就是说,我们可以调用p.then(或者p.catch)多少次都可以,不管promise是什么状态。

5、promise是ES6管理异步代码的标准方式,javascript库使用promise管理ajax,动画,和其他典型的异步交互。

promise代码题解析
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
    reject('error')
  }, 1000)
})
promise.then((res)=>{
  console.log(res)
},(err)=>{
  console.log(err)
})

输出结果:success

解题思路:Promise状态一旦改变,无法在发生变更。

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)

输出结果:1

解题思路:Promise的then方法的参数期望是函数,传入非函数则会发生值穿透。

setImmediate(function(){
    console.log(1);
},0);
setTimeout(function(){
    console.log(2);
},0);
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});
console.log(6);
process.nextTick(function(){
    console.log(7);
});
console.log(8);

答案:3 4 6 8 7 5 2 1

解析: 优先级:

process.nextTick > promise.then > setTimeout > setImmediate
Promise.resolve(1)
    .then((res) => {
        console.log(res);
        return 2;
    })
    .catch((err) => {
        return 3;
    })
    .then((res) => {
        console.log(res);
    });

答案:1,2

解题思路:Promise首先resolve(1),接着就会执行then函数,因此会输出1,然后在函数中返回2。因为是resolve函数,因此后面的catch函数不会执行,而是直接执行第二个then函数,因此会输出2。

async/await

async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

核心原理:

当调用一个 async 函数时,会返回一个 Promise 对象。当这个 async 函数返回一个值时,Promise 的 resolve 方法会负责传递这个值;当 async 函数抛出异常时,Promise 的 reject 方法也会传递这个异常值。

async 函数中可能会有 await 表达式,这会使 async 函数暂停执行,等待 Promise 的结果出来,然后恢复async函数的执行并返回解析值(resolved)。

async的作用:(返回一个Promise对象)
sync function testAsync() {
   return "hello async";
}

const result = testAsync();
console.log(result); // Promise { 'hello async' }

testAsync().then(v => console.log(v))
// "hello async"

async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

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

await的作用:

await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

使用await的注意点
  • await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。
async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}
  • 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

  • await命令只能用在async函数之中,如果用在普通函数,就会报错。
经典面试题:
async function async1(){
   console.log('async1 start')
   await async2()
   console.log('async1 end')
}
async function async2(){
   console.log('async2')
}
console.log('script start')
setTimeout(function(){
   console.log('setTimeout')
},0)  
async1();
new Promise(function(resolve){
   console.log('promise1')
   resolve();
}).then(function(){
   console.log('promise2')
})
console.log('script end')

答案:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

解析:

1、定义一个异步函数 async1

2、定义一个异步函数 async2

3、打印 ‘script start’ // *1

4、定义一个定时器(宏任务,优先级低于微任务),在0ms 之后输出

5、执行异步函数 async1

  • 5.1、打印 'async1 start' // *2

  • 5.2、遇到await 表达式,执行 await 后面的 async2

    5.2.1、打印 'async2' // *3

  • 5.3返回一个 Promise,跳出 async1 函数体

6、执行 new Promise 里的语句

  • 6.1、打印 ‘promise1‘ // *4
  • 6.2、resolve() , 返回一个 Promise 对象,把这个 Promise 压进队列里

7、打印 ’script end' // *5

8、同步栈执行完毕

9、回到 async1 的函数体,async2 函数没有返回 Promise,所以把要等async2 的值 resolve,把 Promise 压进队列

10、执行 new Promise 后面的 .then,打印 ’promise2‘ // *6

11、回到 async1 的函数体,await 返回 Promise.resolve() ,然后打印后面的 ’async1 end‘ // *7

12、最后执行定时器(宏任务) setTimeout,打印 ’setTimeout‘ // *8