重温 JavaScript 系列(4):实现异步的方法、 promise实现文件读取、Promise的并发处理?

实现异步的方法

回调函数(Callback)、事件监听、发布订阅、Promise/A+、生成器Generators/ yield、async/await

async/await函数对 Generator 函数的改进,体现在以下三点:

  • 内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行
  • 更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)
  • 更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。

异步考题总结:

  1. 所有任务都在主线程上执行,形成一个执行栈(Execution Context Stack)
  2. 在主线程之外还存在一个任务队列(Task Queen),系统把异步任务放到任务队列中,然后主线程继续执行后续的任务
  3. 一旦执行栈中所有的任务执行完毕,系统就会读取任务队列。如果这时异步任务已结束等待状态,就会从任务队列进入执行栈,恢复执行
  4. 主线程不断重复上面的第三步

在执行任务队列中任务,包括宏任务和微任务。

宏任务 Macrotask 宏任务是指Event Loop在每个阶段执行的任务

微任务 Microtask 微任务是指Event Loop在每个阶段之间执行的任务

在node V8中,这两种类型的真实任务顺序如下所示:

宏任务 Macrotask队列真实包含任务:

script(主程序代码),setTimeout, setInterval, setImmediate, I/O, UI rendering

注意:setTimeout 如果设置了大于0的延时,就会在 setImmediate后面执行。

微任务 Microtask队列真实包含任务:

process.nextTick, Promises, Object.observe, MutationObserver

PS:先执行主线程中的同步任务。再按照宏任务和微任务的顺序执行,一个await()、then()、catch() 都加一级,碰到其他宏任务,就按序添加在其他宏任务后面。从一级到最后一级,从上到下按序运行。

例子:

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

// 综合的执行顺序就是: 3——>4——>6——>8——>7——>5——>start: 7.009ms——>1——>2。

浏览器和 Node 环境下,microtask 任务队列的执行时机不同

  • Node 端,microtask 在事件循环的各个阶段之间执行
  • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

(参考 帖子:https://blog.csdn.net/Fundebug/article/details/86487117)

var p = Promise.all([p1, p2, p3]);

Promise.all方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)
p的状态由p1p2p3决定,分成两种情况。
(1) 只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。
(2) 只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

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

Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例。只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为Promise实例,再进一步处理。

Promise.resolve(value) ;
// 等价于
new Promise(resolve => resolve(value));

var p = Promise.reject(reason);
// 等同于
var p = new Promise((resolve, reject) => reject(reason));

Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。

const thenable = {
    then(resolve) {
        resolve('ok');
    }
};

Promise.resolve(thenable)
    .then(e => {
        console.log(e === 'ok'); //true
    });

Promise.reject(thenable)
    .catch(e => {
        console.log(e === thenable); // true
    });

promise实现文件读取:

const fs = require('fs');
const path = require('path');

function asyncGetFile(p){
    return new Promise(function(resolve, reject){
        fs.readFile(path.join(__dirname, p), 'utf-8', (err, data) => {
            if (err)    reject(err);
            else    resolve(data);
        })
    })
}
asyncGetFile('./some.txt')
    .then(data => {……})
    .catch(err => {……});

用js实现sleep,用promise

function sleep(time){
    return new Promise(resolve => setTimeout(resolve, time))
}
sleep(2000).then(()=>{……})
// 这种方式实际上是用了 setTimeout,没有形成进程阻塞,不会造成性能和负载问题。

实现一个 Scheduler 类,完成对Promise的并发处理,最多同时执行2个任务

(??此点好好思考)

class Scheduler {
    constructor() {
        this.tasks = [], // 待运行的任务
        this.usingTask = [] // 正在运行的任务
    }
    // promiseCreator 是一个异步函数,return Promise
    add(promiseCreator) {
        return new Promise((resolve, reject) => {
            promiseCreator.resolve = resolve
            if (this.usingTask.length < 2) {
                this.usingRun(promiseCreator)
            } else {
                this.tasks.push(promiseCreator)
            }
        })
    }

    usingRun(promiseCreator) {
        this.usingTask.push(promiseCreator)
        promiseCreator().then(() => {
            promiseCreator.resolve()
            this.usingMove(promiseCreator)
            if (this.tasks.length > 0) {
                this.usingRun(this.tasks.shift())
            }
        })
    }

    usingMove(promiseCreator) {
        let index = this.usingTask.findIndex(promiseCreator)
        this.usingTask.splice(index, 1)
    }
}

const timeout = (time) => new Promise(resolve => {
    setTimeout(resolve, time)
})

const scheduler = new Scheduler()

const addTask = (time, order) => {
    scheduler.add(() => timeout(time)).then(() => console.log(order))
}

addTask(400, 4) 
addTask(200, 2) 
addTask(300, 3) 

 

总结:

1、不得不说,异步这个功能在程序的世界里,都是拔尖的复杂。还有一个也挺复杂的:多线程,这让我想起了大学学习java的时候,Thread多线程,有两个线程 A和B,其中A先调用加一,B再调用减一……

有问题反馈加微信:mue233 私聊问我 微信公众号:焦虑自愈教程,分享过去走出来的经验
52软件资源库 » 重温 JavaScript 系列(4):实现异步的方法、 promise实现文件读取、Promise的并发处理?