7

好好学习JS异步原理

 3 years ago
source link: https://zhuanlan.zhihu.com/p/108652323
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

好好学习JS异步原理

腾讯 高级前端工程师

平常在工作中,我们经常与异步打交道,无论是函数节流、防抖,异步请求,都是异步操作。那么我们会经常使用setTimeout,Promise,Async/Await这三个东西。那么我们是真的了解这些api和语法糖他们的原理以及知识吗?本篇文章将从尽可能的说明白个中的原理和知识。

  1. JavaScript的运行机制
  2. 了解Promise运行机制,以及一些api的实现原理
  3. Async/Await的原理

JavaScript的运行机制

JavaScript的运行机制本质上就是Event loop,这个知识点主要是要搞清楚宏任务与微任务之间的区别。这个知识点不在这里一一说明,想了解可以看看我之前的文章。

了解Promise运行机制,以及一些api的实现原理

我们平常经常使用Promise来进行各种异步操作,无论是单独使用Promise,或者搭配Async/Await。但是我们要搞清楚里面的一些知识点,才能更好的去使用Promise这个api。如果你还没有用过Promise,那么请先去看文档,MDN

深入了解Promise,我们要从以下几个方面去了解Promise。

  1. Promise的运行机制
  2. Promise.all的实现原理
  3. Promise.race的实现原理
  4. Promise.finally的实现原理

Promise的运行机制

const p = new Promise(function(resolve, reject) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
}).then(function () {
    console.log('promise2')
})

Promise本身是同步的立即执行函数, 但是当我们调用resolve或者reject的时候,.then内的回调函数是异步执行,并且.then内的函数会被存放到微任务中,等主栈完成后,才会去运行微任务中的.then的回调函数。

输入结果:promise1 -> promise1 end -> promise2

Promise.all的使用

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then(function(values) {
  console.log(values);
});

Promise.all就是必须等待传入的Promise数组的所有Promise都执行完毕,才会触发then的api。

那么Promise.all我们应该在什么场景下使用呢?如果当前你的异步操作必须依赖另外几个异步操作,并且都需要这几个前置异步操作都要成功的情况下才进行下一步行为,那么就可以使用Promise.all了。

打个比方说,当前页面中,我们需要依赖几个不同的接口来完成当前页面中的渲染,那么我们就可以使用Promise.all来实现对这几个不同的接口都必须返回数据后,我们才开始渲染页面。

Promise.all的实现原理

我们来尝试自己实现一个Promise.all,来了解它的工作原理。

const promise1 = Promise.resolve( 3 );
const promise2 = 42;
const promise3 = new Promise( function ( resolve, reject ) {
    setTimeout( resolve, 100, 'foo' );
} );

function myAll(promiseList) {
    
    return new Promise((resolve, reject) => {
        let count = 0;
        const promiseCount = promiseList.length;
        const resultList = Array(promiseCount);
        promiseList.forEach((promise, key) => {
                Promise.resolve(promise).then((data) => {
                    count += 1;
                    resultList[key] = data;

                    if ( count == promiseCount ) {
                        resolve(resultList);
                    }
                }, (reason) => {
                    return reject(reason)
                });
        });
    });
}

myAll( [promise1, promise2, promise3] ).then( function ( values ) {
    console.log( values );
}, function ( err ) {
    console.log( err );
} );

本质上Promise.all的原理就是将传入的数组全部执行,并且将所有传入的Promise的resolve结果保存在一个与之传入顺序对应的数组当中,并每次有Promise触发resolve检查是否已经是最后一个,当检查到最后一个时候,触发resolve将返回结果数组返回。

Promise.race的使用

Promise.race实际上就是一个变异版的Promise.all,Promise.all是必须等待所有传入的Promise执行完毕才会触发resolve,但是Promise.race不是,Promise.race是传入的Promise中,只要有一个执行完毕,那么将立即返回,其余的Promise的返回结果将会抛弃。

const promise1 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 100, 'two');
});

Promise.race([promise1, promise2]).then(function(value) {
  console.log(value);
  // Both resolve, but promise2 is faster
});
// expected output: "two"

Promise.race的实现原理

我们可以利用之前实现的Promise.all的实现方式,做一些修改。

const promise1 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 100, 'two');
});

function myRace(promiseList) {
    return new Promise((resolve, reject) => {
        promiseList.forEach(promise => {
            promise.then(resolve, reject)
        })
    });
}

myRace( [promise1, promise2] ).then( function ( values ) {
    console.log( values );
}, function ( err ) {
    console.log( err );
} );

实际上就是通过对传入的每个promise执行一个then,将myRace的resolve传递给每个promise的then中的回调函数,从而实现那个promise先执行完毕,就返回那个promise的运行结果。

Promise.finally的使用

Promise.finally代表的是一连串的promise和then的操作都执行完毕后,无论是否报错,都会执行的函数。

const p = new Promise( ( resolve, reject ) => {
    console.info( 'starting...' );

    setTimeout( () => {
        resolve( 'success' );
    }, 1000 );
} );

p.then( ( data ) => {
    console.log( `%c resolve: ${data}`, 'color: green' )
} )
    .catch( ( err ) => {
        console.log( `%c catch: ${err}`, 'color: red' )
    } )
    .finally( () => {
        console.info( 'finally: completed' )
    } );

Promise.finally的实现原理

实现finally的原理,我们首先要清楚,finally后面,其实是可以继续带有.then的,而且无论是否触发catch,都会执行finally的。

其实实际上finally和then没有太大的区别,只是finally不会接收任何参数,但是可以return回一个promise,可以让后续继续执行then操作。

Promise.prototype.finally = function(callback) {
    const constructor = this.constructor;
    return this.then(
        (data) => {
            return constructor.resolve(callback()).then((callbackData) => {
                return data;
                // 如果扩展,可以将finally的回调函数返回的promise的resolve传递到之后的then中
                // return callbackData
            })
        },
        (err) => {
            return constructor.resolve(callback()).then(() => {
                throw err
            })
        }
    )
}

实际上就是调用Promise的then,注册多一个then的函数,并且返回一个Promise对象,在Promise的执行体中执行finally的回调函数,最后通过将上一个then或者catch中resolve返回的值转入到一下个then中。

小结

通过这几个源码的实现原理,我们大概就知道了Promise中的这些api的运行原理,那么我们将可以更好的在不同场景下,合理利用Promise的特性来处理异步逻辑了。如果说对于Promise的实现原理有兴趣,我之后有时间会单独对Promise的实现原理做文章,这里先不细说Promise的内部实现原理。

Async/Await的原理

首先我们要知道一些概念,async/await实际上是Generator封装的一套异步处理方案,实际上就是Generator的语法糖,而Generator又依赖一个Iterator(迭代器)。所以要搞清楚async,就要先搞清楚Iterator和Generator。

Iterator迭代器

Iterator的思想来源于一种数据结构,单向链表。下面简单说一说单向链表是什么东西。

单向链表是一种基本的数据结构,其中包含着两个重要的参数,一个是当前节点的值,一个是当前节点的一下个节点的指向。

单向链表有以下优点:

  1. 无需预先分配内存
  2. 插入删除节点速度快
  1. 查询速度慢,需要逐个查询

Iterator的思想也是借鉴了单向链表的设计,每个节点都有一个next函数,用于返回当前节点的信息,并且内部指针+1。next函数必须返回一个对象,对象包含value和done属性。

// 迭代生成器
const makeIterator = arr => {
    let nextIndex = 0;
    return {
      next: () =>
        nextIndex < arr.length
          ? { value: arr[nextIndex++], done: false }
          : { value: undefined, done: true },
    };
  };
  const it = makeIterator(['Hello', 'world']);
  console.log(it.next()); // { value: "Hello", done: false }
  console.log(it.next()); // { value: "world", done: false }
  console.log(it.next()); // {value: undefined, done: true }

根据规范,每个对象如果要变成一个可迭代对象,那么必须拥有[Symbol.iterator]参数,Iterator 接口主要供for...of消费。

const array = [1,2];
console.log(array[Symbol.iterator])
for ( let item of array ) {
    console.log(item);
}

const set = new Set([1,2]);
console.log(set[Symbol.iterator])
for ( let item of set ) {
    console.log(item);
}

// 报错
const map = new Map({a:1, b:2});
console.log(map[Symbol.iterator])
for ( let item of map ) {
    console.log(item);
}

// 报错
const object = {a: 1, b: 2};
console.log(object[Symbol.iterator])
for ( let item of object ) {
    console.log(item);
}

默认Array和Set数据格式都内置了[Symbol.iterator]接口,但是Map和Object是没有的,所以调用for...of的时候将会报错。但是我们可以实现自定义的迭代器。

const object = {
    data: ['hello', 'world'],
    [Symbol.iterator]() {
        let index = 0;
        return {
            next: () => {
                if (index < this.data.length) {
                    return {
                      value: this.data[index++],
                      done: false
                    };
                  } else {
                    return { value: undefined, done: true };
                  }
            }
        }
    }
};
console.log(object[Symbol.iterator])
for ( let item of object ) {
    console.log(item);
}

有什么时候会调用Iterator迭代器呢?

  1. 扩展运算符
  2. yield*
  3. ......

大概说明了一下Iterator迭代器到底是一个怎样的东西。接下来开始学习一下Generator,以及Generator依赖Iterator迭代器做了什么?

Generator

上面已经了解Iterator迭代器的原理,那么其实Generator实际上就是生成迭代器的语法。具体语法就是声明一个function*的函数,例如:

  • function* gen() {}
  • function *gen() {}
function* gen() {
    console.log('运行gen')
    yield 1;
    console.log('运行第一次')
    yield 2;
    yield 3;
}

const g = gen();

console.log(g.next());
// 运行gen
// { value: 1, done: false } 
console.log(g.next());
// 运行第一次
// { value: 2, done: false }
console.log(g.next());
// { value: 3, done: false }
console.log(g.next());
// { value: undefined, done: true }

在gen函数中,首次调用并不会执行函数中的任何代码,每次执行next的时候,程序会运行至相应的yield就暂停等待第二次的next调用。

下面我用代码模拟使用Generator来实现异步。

function ajax1 () {
    return new Promise( ( resolve ) => {
        setTimeout( resolve, 500, 'ajax1' )
    } );
}

function ajax2 () {
    return new Promise( ( resolve ) => {
        setTimeout( resolve, 500, 'ajax2' )
    } );
}

function* gen () {
    yield ajax1();
    yield ajax2();
}

const g = gen();

const g1 = g.next();
g1.value
    .then( ( data ) => {
        console.log( data );
        const g2 = g.next();
        g2.value
            .then( ( data2 ) => {
                console.log( data2 );
            } )
    } );

从例子中可以看到,每次yield执行一个函数,返回的是Promise,所以每次调用next返回的value都是一个Promise对象。所以就可以实现类似async/await的执行方式。

但是有一个问题,同样的代码,如果使用async/await来实现,俾比较简单:

const data1 = await ajax1();
const data2 = await ajax2();

但是现在我们要每次触发next都需要对next的value手动调用then,这样非常麻烦,所以我们需要一个自动迭代器,帮我们自动完成迭代的过程。

function ajax1 () {
    return new Promise( ( resolve ) => {
        setTimeout( resolve, 500, 'ajax1' )
    } );
}

function ajax2 () {
    return new Promise( ( resolve ) => {
        setTimeout( () => {resolve('ajax2')}, 500 )
    } );
}

function run(gen) {
    const g = gen();
    function _next(data) {
        const res = g.next(data);
        if (res.done) return res.value;
        res.value.then((data) => {
            _next(data);
        });
    }
    _next();
}

function* gen () {
    const res1 = yield ajax1();
    console.log(res1);
    const res2 = yield ajax2();
    console.log(res2);
}

run(gen);

到这里是否开始已经有一点像async/await的语法了。

Async/Await

async/await其实实际上就是Generator的语法糖,本质上就是使用Generator来实现的,我们可以看看对比

// Generator
function* gen () {
    const res1 = yield ajax1();
    console.log(res1);
    const res2 = yield ajax2();
    console.log(res2);
}

// async/await
async function gen2() {
    const res1 = await ajax1();
    console.log(res1);
    const res2 = await ajax2();
    console.log(res2);
} 

async就等于function*,await就等于yield,而且使用async/await无需自己写手动迭代器,它会自动帮你完成。

async/await还有一些不一样的点,例如await如果调用的Promise,才会异步执行,否则将会同步执行,gen2是一个Promise对象,如果在gen2最后执行return,那么将会触发gen2的then。

async function gen2() {
    const res1 = await ajax1();
    console.log(res1);
    const res2 = await ajax2();
    console.log(res2);
    return 'done'
} 

gen2()
.then((data) => {
    console.log(data); // done
})

JavaScript的异步主要分为setTimeout,Promise,aysnc/await这三个技术。setTimeout的异步操作更多是作为对一些渲染操作以及函数节流/防抖的时候进行使用,随着ES6的成熟,Promise和async/await越来越多使用,而async/await一般都是搭配Promise一起使用的,而Promise还可以解决回调地狱的问题。

async/await实际上是Generator的语法糖,让开发者更方便的进行异步处理,无需手动迭代,带来更好的开发体验。而Generator依赖了Iterator迭代器来实现迭代,Iterator的思想是利用单向链表的设计。


Recommend

  • 106

    不好好学习长大了要给油腻中年大叔当小动物 ​

  • 29

    程序员 - @fl2d - 工作也一直在用 ubuntu,一直是摸索型的使用,基本上就是出了啥 error 或者困难,google 一下,然后上 stackoverflow 上复制粘贴一个命令过来, 顶多自己照猫

  • 54

    笔者对“好好学习”app需不需要增加“做笔记”这个功能点进行了一次调研。本文将比较不同有“笔记”功能点的app,根据这些app可以借鉴的点来结合“好好学习”app本身特质,对该app是否添加“做笔记”功能点和如何设计该功能点提出建议。 一、调研目的 据反馈:“

  • 24
    • 微信 mp.weixin.qq.com 4 years ago
    • Cache

    图解 Vue 异步更新原理

    上一篇《图解 Vue 响应式原理》中,我们通过 9 张流程图,理解了 Vue 的渲染流程,相信大家对整个 Vue 的渲染流程有了一定的了解,这一篇我们来重点关...

  • 3
    • lanbing510.info 3 years ago
    • Cache

    好好学习-读书简记

    好好学习-读书简记 2017年06月08日 读起来很过瘾的一本书,也是箭箭穿心的一本书。知识不等于信息,刻意练习,学习深度,元认知,认知效率,反思,提问,记录,清单,临界知识,复利,概...

  • 8
    • lutaonan.com 3 years ago
    • Cache

    Svelte 的异步更新实现原理

    Svelte 的异步更新实现原理 2021-04-11 在 我对 Svelte 的看法 一文里,我分析了 Svelte 在编译时实现 Reactive 的原理。在这篇文章,我将分析在 S...

  • 8
    • yjalifebook.com 3 years ago
    • Cache

    好好学习 - 成甲 - 不点语书

    Categories 读书笔记 好好学习 — 成甲 好好学习是一本给我启发非常非常大的书...

  • 6

    JAVA语言异步非阻塞设计模式(原理篇)本系列文章共2篇,对 Java 语言的异步非阻塞模式进行...

  • 3

    Future 模式介绍以及核心思想 核心线程数、最大线程数的区别,队列容量代表什么; ThreadPoolTaskExecutor 饱和策略; SpringBoot 异步编程实战,搞懂代码的执行逻辑。 Future 模式

  • 5

    高二(下)有必要抛弃所有人际来好好学习吗?我现在身边有一个玩伴 但是对于我而言就是那种时而玩的开心时而很郁闷 甚至有时候说什么还会去在意她的表情 感觉就有点累趴 我现在成绩极差三四十几名(46…

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK