前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >好好学习JS异步原理

好好学习JS异步原理

作者头像
LamHo
发布2022-09-26 10:42:55
1.3K0
发布2022-09-26 10:42:55
举报
文章被收录于专栏:小前端看世界小前端看世界

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

目录

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

JavaScript的运行机制

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

Lam:JavaScript各种定时器总结

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

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

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

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

Promise的运行机制

代码语言:javascript
复制
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的使用

代码语言:javascript
复制
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,来了解它的工作原理。

代码语言:javascript
复制
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的返回结果将会抛弃。

代码语言:javascript
复制
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的实现方式,做一些修改。

代码语言:javascript
复制
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的操作都执行完毕后,无论是否报错,都会执行的函数。

代码语言:javascript
复制
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操作。

代码语言:javascript
复制
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属性。

代码语言:javascript
复制
// 迭代生成器
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消费。

代码语言:javascript
复制
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的时候将会报错。但是我们可以实现自定义的迭代器。

代码语言:javascript
复制
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. 扩展运算符
  3. yield*
  4. ......

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

Generator

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

  • function* gen() {}
  • function *gen() {}
代码语言:javascript
复制
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来实现异步。

代码语言:javascript
复制
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来实现,俾比较简单:

代码语言:javascript
复制
const data1 = await ajax1();
const data2 = await ajax2();

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

代码语言:javascript
复制
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来实现的,我们可以看看对比

代码语言:javascript
复制
// 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。

代码语言:javascript
复制
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的思想是利用单向链表的设计。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-02-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 目录
  • JavaScript的运行机制
  • 了解Promise运行机制,以及一些api的实现原理
  • Async/Await的原理
  • Iterator迭代器
  • Generator
  • Async/Await
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档