js是如何实现异步编程的?

967 阅读5分钟

题目:js是如何实现异步编程的?

解答

如何实现异步io?

1.回调函数callback
  • 优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等待,会拖延整个程序的执行)
  • 缺点:回调地狱,不能用 try catch 捕获错误,不能 return
2.Promise
  • 优点:解决了回调地狱的问题
  • 缺点:无法取消 Promise,错误需要通过回调函数来捕获
3.async/await
  • 优点:代码清晰,不像 Promise 写一堆 then 链,处理了回调地狱的问题
  • 缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能降低

async/await是怎么来的?/ async/await如何通过同步的方式实现异步?

  • async/await 是参照 generator 封装的一套异步处理方案,可以理解为 generator 的语法糖,而 generator 又依赖于迭代器 IteratorIterator的思想又源于单向链表
1.单向链表
  • 链表的优点

    • 无需预先分配内存
    • 插入/删除节点不影响其他节点,效率高
  • 单向链表

    是链表中最简单的一种,包含两个域:信息域与指针域,这个链接指向列表中的下一个节点,而最后一个节点则指向一个空值。

    • 特点:
      1. 节点的链接方向是单向的
      2. 相对于数组来说,单链表的随机访问速度较慢,但单链表删除/添加数据的效率很高
2.Iterator
  • Iterator迭代器 的遍历过程类似于单向链表
3.Generator
  • Generator:生成器对象时生成器函数返回的,它符合可迭代协议和迭代器协议,既是迭代器也是可迭代对象,可以调用 next 方法,但它不是函数,更不是构造函数
本质:暂停

它会让程序执行到指定位置先暂停 (yield),然后再启动(next),再暂停(yield),再启动(next),而这个暂停就很容易让它和异步操作产生联系

  • 处理异步时

    1. 开始异步处理(网络请求、IO操作)
    2. 然后暂停一下
    3. 处理完,再该干嘛干嘛
  • js是单线程的,异步还是异步,callback 还是 callback,不会因为 generator 而有任何改变

4.async/await
  • async/await是 generator 的语法糖,就是一个自执行的 generate函数。利用 generator函数的特性把异步的代码写成同步的形式

async/awaitpromise的用法?

async/await的用法
  • async/await 是函数定义的关键字
  • await 用于等待 promise对象的返回结果,且不能单独使用必须放在 async函数
  • 利用 async定义的函数会返回一个 promise对象
  • async函数的返回值就是 promise状态为 resolved的返回值
promise

promise 是一个构造函数,有all reject resolve这几个方法,原型上有then catch等方法

特点
  • 对象的状态不受外界影响
    • promise对象代表一个异步操作,有三种状态
      • pending(进行中)
      • fulfilled(已成功)
      • rejected(已失败)
  • 一旦状态改变就不会再变,任何时候都可以得到这个结果
    • promise对象的状态改变,只有两种可能
      • pending变为fulfilled
      • pending变为rejected
promise的用法
  • 首先new一个promise
let p = new Promise(function(resolve, reject) {
    // do sth async
    setTimeout(function() {
        console.log('执行完成promise')
        resolve('要返回的数据')
    }, 2000)
})
// 执行完成promise
  • new一个promise对象,不需要调用就执行了
  • 使用promise的时候一般是包在一个函数中,在需要的时候去运行这个函数
const clickFunc = () => {
    console.log('点击方法被调用')
    return new Promise(function(resolve, reject) {
        // do sth async
        setTimeout(function() {
            console.log('执行完成promise')
            resolve('要返回的数据')
        }, 2000)
    })
}
  • 为什么要放在函数里面

    • 函数return出promise对象,执行这个函数我们可以得到一个promise对象。接下来就可以用promise对象上的then catch方法了
  • resolve是什么

    • promise只是能够简化层层回调的写法,实质上,promise的精髓是 状态,用维护状态、传递状态的方式使得回调函数能够及时调用
  • reject的用法

    • then方法可以接受两个参数,第一个对应resolved的回调,第二个对应reject的回调,并且能在回调函数中拿到成功的数据和失败的原因
function promiseClick(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            var num = Math.ceil(Math.random()*20); //生成1-10的随机数
			console.log('随机数生成的值:',num)
			if(num<=10){
				resolve(num);
			}
			else{
				reject('数字太于10了即将执行失败回调');
			}
		}, 2000);
	})
}
promiseClick().then(
	function(data){
		console.log('resolved成功回调');
		console.log('成功回调接受的值:',data);
	}, 
	function(reason){
		console.log('rejected失败回调');
		console.log('失败执行回调抛出失败原因:',reason);
	}
);
  • catch的用法

    • 作用一:用来捕获异常,与then方法中接受的第二参数rejected的回调一样
    • 作用二:再执行resolved的回调时,如果抛出异常,并不会报错卡死js,而是会进入catch方法中
  • all的用法

    • 该方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后并且执行结果都是成功的时候才执行回调
function promiseClick1(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            var num = Math.ceil(Math.random()*20); //生成1-10的随机数
            console.log('随机数生成的值:',num)
            if(num<=10){
                resolve(num);
            }
            else{
                reject('数字太于10了即将执行失败回调');
            }
        }, 2000);
    })
}
function promiseClick2(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            var num = Math.ceil(Math.random()*20); //生成1-10的随机数
            console.log('随机数生成的值:',num)
            if(num<=10){
                resolve(num);
            }
            else{
                reject('数字太于10了即将执行失败回调');
            }
        }, 2000);
    })
}
function promiseClick3(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            var num = Math.ceil(Math.random()*20); //生成1-10的随机数
            console.log('随机数生成的值:',num)
            if(num<=10){
                resolve(num);
            }
            else{
                reject('数字太于10了即将执行失败回调');
            }
        }, 2000);
    })
}
Promise
    .all([promiseClick3(), promiseClick2(), promiseClick1()])
    .then(function(results){
        console.log(results);
    });
  • race的用法
    • 谁先执行完成就先执行回调
    • 先执行完的不管是进行了race的成功回调还是失败回调,其余的将不会再进入race的任何回调
function promiseClick1(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            var num = Math.ceil(Math.random()*20); //生成1-10的随机数
            console.log('随机数生成的值:',num)
            if(num<=10){
                resolve(num);
            }
            else{
                reject('数字太于10了即将执行失败回调');
            }
        }, 2000);
    })
}
function promiseClick2(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            var num = Math.ceil(Math.random()*20); //生成1-10的随机数
            console.log('随机数生成的值:',num)
            if(num<=10){
                resolve(num);
            }
            else{
                reject('数字太于10了即将执行失败回调');
            }
        }, 2000);
    })?
}
function promiseClick3(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            var num = Math.ceil(Math.random()*20); //生成1-10的随机数
            console.log('随机数生成的值:',num)
            if(num<=10){
                resolve(num);
            }
            else{
                reject('数字太于10了即将执行失败回调');
            }
        }, 2000);
    })
}
Promise
    .race([promiseClick3(), promiseClick2(), promiseClick1()])
    .then(function(results){
        console.log(results);
    },function(reason){
		console.log('失败',reason);
	});

async/awaitpromise用法的区别?举例两三个?

  1. promiseES6async/awaitES7
    • async await是基于Promise实现的,可以说是改良版的Promise,它不能用于普通的回调函数
  2. async/await相对于promise来讲,写法更加简洁
    • Promise的出现解决了传统callback函数导致的“地域回调”问题,但它的语法导致了它向纵向发展行成了一个回调链,遇到复杂的业务场景,这样的语法显然也是不美观的。
    • async await代码看起来会简洁些,使得异步代码看起来像同步代码,await的本质是可以提供等同于”同步效果“的等待异步返回能力的语法糖,只有这一句代码执行完,才会执行下一句
  3. reject状态:
    • promise错误可以通过catch来捕捉,建议尾部捕获错误,
    • async/await既可以用.then又可以用try-catch捕捉

async/awaitpromise性能的区别?

async/await优点
  • 它做到了真正的串行的同步写法,代码阅读相对容易
  • 对于条件语句和其他流程语句比较友好,可以直接写到判断条件里面
  • 处理复杂流程时,在代码清晰度方面有优势
async/await缺点
  • 无法处理promise返回的reject对象,要借助try...catch...
  • await只能串行,做不到并行
    • await做不到并行,不代表async不能并行,只要await不在同一个async函数里就可以并行
  • try...catch...内部的变量无法传递给下一个try...catch...
  • async/await无法简单实现Promise的各种原生方法,比如.race()之类

多个await和多个promise执行的区别在哪里?

相关

js执行机制 -- 异步

  • js是一种单线程语言。

  • js的异步是通过回调函数实现的,即通过任务队列,在主线程执行完当前的任务栈,主线程空闲后轮询任务队列,并将任务队列中的任务(回调函数)取出来执行

EventLoop
  1. 同步任务进入主线程,异步任务进入Event Table并注册函数
  2. 当指定事件完成时,Event Table会将这个函数移入Event Queue
    • 不同类型的任务会进入对应的Event Queue
  3. 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数进入主线程执行
  4. 重复上述过程,即Event Loop(事件循环)

promise.nextTick

nextTick的由来
  • 由于vue的数据驱动视图更新是异步的,即修改数据的当下,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。
nextTick的触发时机
  • 在同一事件循环中的数据变化后,DOM完成更新,立即执行nextTick(callback)内的回调。
应用场景
  • 需要在视图更新之后,基于新的视图进行操作。
简单总结事件循环
  • 同步代码执行 -> 查找异步队列,推入执行栈,执行callback1[事件循环1] ->查找异步队列,推入执行栈,执行callback2[事件循环2]...
  • 即每个异步callback,最终都会形成自己独立的一个事件循环。
结合nextTick的由来,可以推出每个事件循环中,nextTick触发的时机
  • 同一事件循环中的代码执行完毕 -> DOM 更新 -> nextTick callback触发

使用promise实现串行

var createPromise = function(time) {
    return (resolve, reject)=>
    new Promise((resolve, reject)=>{
        setTimeout(()=>{
            console.log('timein'+time)
            resolve();
        }, time*1000)
    })
}
 
function serpromise(arr) {
    arr.reduce((pre, next, index, carr)=>{
        return pre.then(next)
    }, Promise.resolve())
}
 
var arr=[createPromise(2), createPromise(1), createPromise(3), createPromise(4), createPromise(5)];
serpromise(arr)

node与浏览器eventLoop的区别

  • 浏览器和node环境下,microtask任务队列的执行时机不同
    • node端,microtask在事件循环的各个阶段之间执行
    • 浏览器端,microtask在事件循环的macrotask执行完之后执行