异步的开端-promise

119 阅读6分钟

通过上一节我们可知:

  • 异步的实现方式是通过事件循环(事件轮循),而事件循环的核心是回调函数

下面简单阐述下异步执行机制:

  • 调用异步线程,当事件被触发以后,将对应的回调函数推入到任务队列当中,当主线程的任务结束之后,将回调函数的任务通过事件轮询的方式推到主线程当中

1. 异步回调造成的问题

1.1 回调地狱

【回调地狱】 一句话概括:异步回调函数的嵌套。缺点: 难于维护,不便拓展,可读性差

下面看一下Node 中异步函数的回调地狱:

我们在同级目录下创建 app.js、name.txt、getSource.text 和 source.txt。

image.png

下面为 app.js 中的代码

// 引入 node 中的 fs 模块,调用fs中的 readFile 方法来读取文件的内容,该方法传入一个路径、字符编码和回调函数
let fs = require("fs");

fs.readFile("./text.txt", "utf-8", (error, data) => {
  if (error) {
    console.log(error);
  }
  fs.readFile(data, "utf-8", (error, data) => {
    if (error) {
      console.log(error);
    }
    fs.readFile(data, "utf-8", (error, data) => {
      if (error) {
        console.log(error);
      }
      console.log(data);
    });
  });
});

控制台 输出 node app.js 执行,可以看到此时输出了 99,也就是拿到了 source 中的数据,但此时如果 source.txt 中依然有另外的地址信息,那么这个上面这个 JS 代码将会像套娃一样一层一层向前推进,如果其中的某一环节处了问题,我们是很难确定问题具体出现在哪一环的

image.png

我们再来看看普通回调函数的回调地狱:

setTimeout(()=> {
  console.log(1);
  setTimeout(()=> {
    console.log(2);
    setTimeout(()=> {
      console.log(3);
    },1000)
  },2000)
},3000)

与上面类似,当代码像套娃一样以层层递进的模样出现时,就陷入了回调地狱

1.2. try catch 不能捕获异常

参考文章:juejin.cn/post/702188…

【 try catch 的捕捉机制 】

  • 能捕捉到的异常必须是线程执行已经进入 try catchtry catch 未执行完的时候抛出来的

  • try catch 只能捕获同步代码的异常,不能捕获异步代码的异常

原因:当异步函数抛出异常时,对于宏任务而言,执行函数时已经将该函数推入栈,此时并不在 try catch 所在的栈(主线程已经离开了try catch),所以 try catch 并不能捕获到错误

// try catch 捕获同步代码错误,不会报错
try {
  console.log(a);
} catch(e) {
  console.log(e);
}
// 无法捕获到,会直接报错
try {
    setTimeout(() => { 
        console.log(a)
    })
} catch (e) {
    console.log(e);
}

1.3. 并列的多个异步任务

【 并列的多个异步 】

如下面的代码,异步不会阻塞当前线程,而是直接向下执行;所以可以直接认为当前的所有异步代码是同一时机注册的异步代码。而产生的问题是:并不能确定每个异步任务什么时候都完成运行,可能会存在相互竞争的状态

解决方案:

  • 每个异步代码里都加一个判断条件。缺点:很笨重,代码重复
  • ES5发布订阅模式
  • Promise
// 并不能确定文件什么时候读取完
let arr = [];

function show(data) {
    console.log(data);
}

fs.readFile('./name.txt', 'utf-8', (err, data) => {
    if (data) {
        arr.push(data)
    }
    arr.length === 3 && show(arr);
});
fs.readFile('./number.txt', 'utf-8', (err, data) => {
    if (data) {
        arr.push(data)
    }
    arr.length === 3 && show(arr);
});
fs.readFile('./score.txt', 'utf-8', (err, data) => {
    if (data) {
        arr.push(data)
    }
    arr.length === 3 && show(arr);
});

2. Promise

3.1 Promise 定义

【 理解 】 存放异步操作的容器,存放一个以后才会结束的事件(异步操作)

  • 如KFC点餐,先给了一个小票,然后等汉堡做好,这个小票就是Promise

【 用法 】 Promise 是一个系统内置的构造函数,使用时需要被实例化

  • let promise = new Promise( executor )

  • 它的参数是一个函数 (executor) 执行者;该函数又有两个参数 为:resolve reject分别对应成功和失败各自回调

  • Promise本身是一个异步操作,但它的函数里是同步执行的

console.log(new Promise(function(resolve, reject) {})); 

// 参数中的函数,本质上是同步执行的
new Promise(function(resolve, reject) {
    console.log('promise')
});
console.log(1)

3.2 特征

3.2.1 三种状态

Promise本身就代表一个异步操作,所以它有三种状态

  1. pending(进行中)
  2. fulfilled(resolve) (已成功)
  3. reject(已失败)

Tip:对象的这三种状态不受外界影响

3.2.2 状态的不可逆

pending转为 fulfilled reject,但不会反向转换

【 与事件中的异步不同 】 Promise 状态固化以后,再对 Promise 对象添加回调,是可以直接拿到这个结果的;如果说是事件的话,一旦错过了,就是真的错过了,再也不会监听到了。

3.3 参数

Promise的参数是一个函数,这个函数叫 executor( 执行者),这个函数的参数又是两个函数:

  • resolve:调用resolve()能够传参, 可以将 promise 状态改为 fulfilled 接下来就可以执行成功所对应的回调函(then())数拿到值
  • reject:调用reject(),可以将 promise 状态改为 reject ,其余相同

可以通过参数的执行方式改变 promise 的状态

let promise = new Promise((resolve, reject) => {
        Math.random() * 100 > 60 ? resolve('及格') : reject('不及格');
});

// 绑定回调成功和失败的处理函数
promise.then((value) => { // 第一个参数是注册成功的回调函数
    console.log(value);
}, (reason) => {          // 第二个参数是注册失败的回调函数
    console.log(reason)
});

image.png

思考下面的代码会输出什么

setTimeout(function() {
    console.log('setTime'); // 异步代码
}, 30)
let promise = new Promise(function(resolve, reject) { 
    // 同步代码
    console.log(0);
    resolve(1);		// 调用异步的回调函数
});

// 异步代码,推入任务队列,主线程任务结束后推入执行栈,微任务 > 宏任务
promise.then((value) => { 
    console.log(value);
}, (reason) => {
    console.log(reason);
});

console.log(2);

3.4 链式调用

【 原理 】 .then 方法在原型上的,所以可以链式调用

let promise = new Promise(function(resolve, reject) {
    resolve(1);
    // reject(10);
});


// 第一次 then 的返回值作为下一次 then 执行的参数
promise.then((value) => {
    console.log(value);
    // 可以手动返回一个new Promise,这个时候同第一次new Promise是一致的
    return new Promise((resolve, reject) => { 
        resolve('newPromise ok')
    });
}, (reason) => {
    console.log(reason);
    return 2;
}).then((value) => { 
    /*
    	 第二次链式调用无法像第一次调用时直接拿到值,
  		 而是需要在上面手动添加 return
     */
    console.log('ok then2:' + value);
}, (reason) => {
    console.log('no then2:' + reason)
});

3. 宏任务、微任务

JS异步代码中,分为:

  1. 宏任务:宏任务队列 -> 除了下面两种以外的所以异步任务,都可以认为是宏任务
  2. 微任务:微任务队列 -> promise, Node中的process.nextTick() -> 微任务的优先级更高

先同步任务,然后微任务,再宏任务。 在异步代码中说宏任务才有意义

宏任务和微任务之间的嵌套

思考下面的代码会输出什么

Promise.resolve().then(() => { // 微任务
    console.log('promise1');
    setTimeout(() => { // 宏任务
        console.log('setTimeout2')
    })
})

setTimeout(() => { // 宏任务
  	// 第一轮循环,微任务执行结束,开始执行宏任务
    console.log('setTimeout1'); 
  
    // 微任务 -> 第二轮循环中,微任务优先执行
    Promise.resolve().then(() => { 
        console.log('promise2');
    })
})

4. Promise A+ 规范

Tip: 定义了promise相关的行为和方法,ES6 promise 是Promise A+ 规范的实现