【JS】每日一题:异步解决方案

🌰 异步解决方案

同步操作:顺序执行,同一时间只能做一件事情。缺点是会阻塞后面代码的执行。

异步:指的是当前代码的执行作为任务放进任务队列。当程序执行到异步的代码时,会将该异步的代码作为任务放进任务队列,而不是推入主线程的调用栈。等主线程执行完之后,再去任务队列里执行对应的任务。优点是:不会阻塞后续代码的运行。

异步场景?

  • 定时任务:setTimeout、setInterval
  • 网络请求:ajax请求、动态创建img标签的加载
  • 事件监听器:addEventListener

回调

回调函数就是我们请求成功后需要执行的函数。

实现了异步,但是带来一个非常严重的问题——回调地狱。

事件发布/订阅

image-20220518074849335

Promise

const promise = new Promise((resolve, reject) => {
	resolve('a');
});
promise
    .then((arg) => { console.log(`执行resolve,参数是${arg}`) })
    .catch((arg) => { console.log(`执行reject,参数是${arg}`) })
    .finally(() => { console.log('结束promise') });

Promise.reject(2)
//.catch(err=>console.log("err1,",err))
.then(null,err=>console.log("err1,",err)) //因为是rejected状态,执行then的第二个callback,改变状态为fulfilled
.then(res=>{console.log("then1",res)},null)//因为是fulfilled,于是执行第一个回调,不会去到下一步catch
//.catch(err=>console.log("err2,",err))
.then(null,err=>console.log("err2,",err)) 

then实现链式操作减低代码复杂度,增强代码可读性。

image-20220518074949866Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。

每个Promise都会经历的生命周期是:

  • 进行中(pending) - 此时代码执行尚未结束,所以也叫未处理的(unsettled)
  • 已处理(settled) - 异步代码已执行结束 已处理的代码会进入两种状态中的一种:
    • 已完成(fulfilled) - 表明异步代码执行成功,由resolve()触发
    • 已拒绝(rejected)- 遇到错误,异步代码执行失败 ,由reject()触发

方法

  1. Promise.all 传入多个异步请求数组,若all成功,进入fulfilled状态,若一个失败,则立即进入rejected状态。

  2. Promise.allSettled 传入多个异步请求数组,无论失败还是失败,都会进入fulfilled状态。

  3. Promise.race 以一个Promise对象组成的数组作为参数,只要当数组中一个Promsie状态变成resolved或者rejected时,就调用.then方法。

事件循环

Generator

iterator

ES6推出,用于更方便创建iterator.Generator是一个返回值为iterator对象的函数.

iterator中文名叫迭代器。它为js中各种不同的数据结构(Array、Set、Map)提供统一的访问机制。任何数据结构只要部署了Iterator接口,就可以完成遍历操作。 因此iterator也是一种对象,不过相比于普通对象来说,它有着专为迭代而设计的接口。

iterator 的作用:

  • 为各种数据结构,提供一个统一的、简便的访问接口;
  • 使得数据结构的成员能够按某种次序排列;
  • ES6 创造了一种新的遍历命令for…of循环

iterator的结构: 它有next方法,该方法返回一个包含valuedone两个属性的对象(我们假设叫result)。value是迭代的值,后者是表明迭代是否完成的标志。true表示迭代完成,false表示没有。iterator内部有指向迭代位置的指针,每次调用next,自动移动指针并返回相应的result。

原生具备iterator接口的数据结构如下:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数里的arguments对象
  • NodeList对象

这些数据结构都有一个Symbol.iterator属性,可以直接通过这个属性来直接创建一个迭代器。也就是说,Symbol.iterator属性只是一个用来创建迭代器的接口,而不是一个迭代器,因为它不含遍历的部分。
使用Symbol.iterator接口生成iterator迭代器来遍历数组的过程为:

let arr = ['a','b','c'];

let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

for … of的循环内部实现机制其实就是iterator,它首先调用被遍历集合对象的 Symbol.iterator 方法,该方法返回一个迭代器对象,迭代器对象是可以拥有.next()方法的任何对象,然后,在 for … of 的每次循环中,都将调用该迭代器对象上的 .next 方法。然后使用for of打印出来的i也就是调用.next方法后得到的对象上的value属性。

对于原生不具备iterator接口的数据结构,比如Object,我们可以采用自定义的方式来创建一个遍历器。

Generator

function* createIterator() {
    yield 1;
    yield 2;
    yield 3;
}

// generators可以像正常函数一样被调用,不同的是会返回一个 iterator
let iterator = createIterator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3

Generator 函数是一个普通函数,但是有两个特征:

  • function关键字与函数名之间有一个星号
  • 函数体内部使用yield语句,定义不同的内部状态

Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object

function* generator(){
    yield 1;
}
console.dir(generator);

generator函数的返回值的原型链上确实有iterator对象该有的next,充分说明generator的返回值是一个iterator。除此之外还有函数该有的return方法和throw方法。

generator和普通的函数完全不同。当以function*的方式声明了一个Generator生成器时,内部可以有许多状态,以yield进行断点间隔。期间我们执行生成的Generator,他会返回一个遍历器对象,用这个对象上的方法,获得一个yield后面输出的结果。

yield和return的区别:

  • 都能返回紧跟在语句后面的那个表达式的值
  • yield相比于return来说,更像是一个断点。遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆功能。
  • 一个函数里面,只能执行一个return语句,但是可以执行多次yield表达式。
  • 正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield

语法注意点:

  • yield表达式只能用在 Generator 函数里面
  • yield表达式如果用在另一个表达式之中,必须放在圆括号里面
  • yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
  • 如果 return 语句后面还有 yield 表达式,那么后面的 yield 不生效

使用Generator的其余注意事项:

  • 需要注意的是,yield 不能跨函数。并且yield需要和*配套使用,别处使用无效
function* createIterator(items) {
  items.forEach(function (item) {
    // 语法错误
    yield item + 1;
  });
}
复制代码
  • 箭头函数不能用做 generator

那么Generator到底有什么用呢?

  • 因为Generator可以在执行过程中多次返回,所以它看上去就像一个可以记住执行状态的函数,利用这一点,写一个generator就可以实现需要用面向对象才能实现的功能。
  • Generator有另一个巨大的好处,就是把异步回调代码变成“同步”代码

async/await

ES7提出的关于异步的终极解决方案

  • 第一个版本说async/await是Generator的语法糖
  • 第二个版本说async/await是Promise的语法糖

关于async/await是Generator的语法糖: 所谓Generator语法糖,表明的就是aysnc/await实现的就是generator实现的功能。但是async/await比generator要好用。因为generator执行yield设下的断点采用的方式就是不断的调用iterator方法,这是个手动调用的过程。针对generator的这个缺点,后面提出自动执行next,但是仍然麻烦。而async配合await得到的就是断点执行后的结果。因此async/await比generator使用更普遍。

async函数对 Generator函数的改进:

  • 内置执行器:Generator函数的执行必须靠执行器,因为不能一次性执行完成。但是,async函数和正常的函数一样执行,也不用使用 next方法,而 async函数自带执行器,会自动执行。
  • 适用性更好:,yield命令后面只能是 Thunk函数或 Promise对象,但是 async函数的 await关键词后面,可以不受约束。
  • 可读性更好:async和 await,比起使用 *号和 yield,语义更清晰明了。

关于async/await是Promise的语法糖: 如果不使用async/await的话,Promise就需要通过链式调用来依次执行then之后的代码:

async/await的出现,就使得我们可以通过同步代码来达到异步的效果

由此可见,Promise搭配async/await的使用才是正解!

async/await其实是基于Promise的.async把promise包装了一下,使用async函数可以让代码简介很多,不需要像promise一样需要写then,不需要写匿名函数处理promise的resolve值,也不需要定义多余的data变量,还避免了嵌套代码.

async函数时Generator函数的语法糖,async函数的返回值是promise对象,generator函数的返回值是 iterator对象方便多了,同时,还可以使用 await 代替then 方法指定下一步操作.

function f() {
    return Promise.resolve('TEST');
}

// asyncF is equivalent to f!
async function asyncF() {
    return 'TEST';
}

await 会阻塞下面的代码(即加入微任务队列),先执行 async外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码

定时器原理

  • var id=setTimeout(fn,delay); 初始化一个只执行一次的定时器,这个定时器会在指定的时间延迟 delay 之后调用函数 fn ,该 setTimeout 函数返回定时器的唯一 id ,我们可以通过这个 id 来取消定时器的执行。
  • var id=setInvertal(fn,delay);setTimeout 类似,只是它会以 delay 为周期,反复调用函数 fn ,直到我们通过id取消该定时器。
  • clearInterval(id),clearTimeout(id); 这两个函数接受定时器的 id,并停止对定时器中指定函数的调用。

定时器指定的延迟时间不能得到保证

在浏览器中,因为所有的JS代码都运行在单一线程中,异步事件只有在被触发时,其回调才会得到执行。
image-20220518122019456

这个图是一维的,垂直方向是时间,以毫秒为单位。蓝色的盒子代表正在执行的javascript代码所占时间片段。

因为单线程的缘故,在同一时间只能执行一条 JavaScript 代码,每一个代码块(蓝色盒子)都会阻塞其他异步事件的执行。

这就意味着,当一个异步事件发生的时候(例如鼠标点击,定时器触发,一个 XMLHttpRequest 请求完成),它进入了代码的执行队列,执行线程空闲时会依照该执行队列中顺序依次执行代码。

总结:

  • JavaScript 引擎是单线程的,会迫使异步事件进入执行队列,等待执行。
  • setTimeoutsetInterval 在执行异步代码时从根本上是有所不同的。
  • 如果一个定时器事件被阻塞,使得它不能立即执行,那么它会被延迟,直到下一个可能的时间点,才被执行(这可能比指定的 delay 时间要长)
  • Interval 的回调有可能‘背靠背’无间隔的执行,这种情况是说 interval 的回调函数的执行时间比指定的 delay 时间还要长

参考文档

JavaScript中定时器的工作原理

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值