JS异步原理以及解决方案

624 阅读5分钟

JS是单线程的

JavaScript最大的特点就是他是单线程的,即同一时间只能做一件事。

为什么js被设计成单线程的语言呢?

js作为一门脚本语言,主要的用途是完成用户交互以及DOM操作,而假如js同时拥有多个线程,其中一个线程在修改DOM节点,而另外一个线程也在修改这个DOM节点,那么这时候就发生了冲突。单线程的特点,避免了js的复杂性,也是js的核心特征。

补充 进程与线程
进程:是CPU资源分配的最小单位,即能够拥有资源和独立运行的最小单位
线程:CPU调度的最小单位,线程建立在进程的基础上的一次程序运行单位,一个进程可以有多个线程。

浏览器是多进程的

同步与异步

程序的执行模式分为同步执行和异步执行。

同步执行

即顺序的执行代码, 从上到下,依次执行语句,如下:

let a = 1
console.log(a)
let b = 2
console.log(b)

输出结果为:1 2

异步执行

不会等待任务结束才开始下一个任务,对于耗时操作,在任务开启后立即执行下一个任务,耗时任务的后续逻辑会通过回调函数的方式定义,如下:

console.log(‘start’)
setTimeout(() => {
    console.log('hello') 
}, 100)
console.log('end')

输出结果为:start end hello

在这里setTimeout开启了一个异步任务,在同步代码执行完毕后,100ms后执行了setTimeout的回调函数。

执行过程

单线程意味着,所有的任务需要排队执行,前一个结束,才会执行下一个,如果任务耗时很长,后一个任务就不得不一直等着。 而这种排队等待并不是因为计算量大,而是因为一些IO设备很慢(如ajax读取数据),不得不等到结果出来,在往下执行,所以js语言设计者意识到,这时的主线程可以挂起等待中的任务,先执行排在后面的任务,等到挂起的任务有结果了,在把它执行下去,因而js中的任务也分成了两类:同步任务异步任务

任务运行机制

  • 1、所有同步任务都在主线程上执行,形成一个执行栈。
  • 2、主线程之外,还存在一个“任务队列”,只要异步任务有了结果,就在“任务队列”里注册一个事件。
  • 3、当执行栈中所有的任务都执行完毕(执行栈清空),系统会读取“任务队列”中的事件,对应事件的异步任务,进入结束等待状态,然后进入执行栈,开始执行。
  • 4、主线程不断的重复第三步。

这个主线程循环读取事件的运行机制,也被称作事件循环。

浏览器中js代码的执行过程

  • js代码执行时,创建内存堆和执行栈
  • 顺次将js脚本中的代码压入执行栈,执行后弹出
  • 当遇到异步任务时,调用相应的webAPI,并开启对应的线程去执行异步任务
  • 当异步任务执行完毕,则在任务队列中注册事件
  • 执行栈清空时,读取任务队列中的事件,并将回调函数压入执行栈执行,重复读取执行,直到任务队列中没有等待的任务。

宏任务 微任务

宏任务 参与了事件循环的任务,需要排队的执行的任务,如任务队列中的任务。 创建宏任务的操作:I/O、setTimeout、setInterval、setImmediate(node), requestAnimationFram(浏览器)

微任务 不参与事件循环,能够跟在宏任务后面执行的任务,不需要重新排队。 创建微任务的操作:process.nextTick(node)、mutationObserver(浏览器)、Promise.then catch finally

宏任务在程序执行过程中创建,微任务可以在宏任务中创建也可以在微任务中创建,微任务会被单独注册到微任务队列中,在当前宏任务执行完毕后,会立即执行当前微任务列表中的微任务。

异步解决方案

//当前有一个ajax调用封装方法
function sendRequest(url, callback) {
      let xhr = new XMLHttpRequest()
      xhr.open('GET', url)
      xhr.onload = callback
 }

回调函数

sendRequest('/api/user', (response) => {
    //处理response
})

问题:容易引发回到地狱,使得代码难以维护,如下

sendRequest('/api/user', (response1) => {
    sendRequest('/api/user', (response2) => {
        sendRequest('/api/user', (response3) => {
            //处理response
        })
    })
})

pormise

处理多个异步

let p1 = new Promise((resolve, reject) => {
    sendRequest('/api/user', (response) => {
    //resolve or reject
    })
})
let p2 = new Promise((resolve, reject) => {
    sendRequest('/api/user', (response) => {
    //resolve or reject
    })
})
Promise.all([p1, p2]).then(([res1, res2]) => {
    //处理response
})

promise的then catch finally 都是微任务

promise特性
  • promise有三种状态,pending,fulfilled,rejected,成功状态和失败状态不可互相转变
  • promise.then会返回一个新的promise,then的链式调用中,后面的then方法就是在为上一个then方法返回的promise注册回调,前面的then方法中回调的返回值,会作为后面then方法回调的参数,如果回调中返回了一个promise,则后面的then方法的回调会等待它的结果。
  • then的第二个参数,是捕获promise异常的回调,它不能捕获then中第一个回调函数中的错误,catch方法可以捕获promise的异常,也可以捕获then中回调函数的异常。
  • 在promise的then和catch方法中,参数期望是函数,当传入的参数不是函数的时候会发生值透传,并将该值传给下一个then或catch方法

generator

function *gen () {
    let res = yield  sendRequest('/api/user', (response) => response)
    //处理res
}
let runGen = gen()
runGen.next()
生成器函数特性
  • 函数名前有一个*
  • 通过调用函数生成一个控制器,如runGen
  • 调用next()方法开始执行函数
  • 遇到yield 函数执行暂停
  • 再次遇到next()继续执行函数

async/await

ES7中的新特性

async callUser() {
    const result = await sendMessage('/api/user', (response) => response)
    //处理 result
}
特性
  • async定义一个异步函数,返回一个隐式的promise
  • await必须在async定义的函数中使用
  • await指令会暂停函数的执行,并等待Promise执行,然后继续执行异步函数,返回结果
  • async/await是genterator的语法糖

——————————————————————个人杂记—————————————————————