陈晨,微医云服务团队前端工程师,一位“生命在于静止”的程序员。javascript
JavaScript 是单线程语言,浏览器只分配了一个主线程执行任务,意味着若是有多个任务,则必须按照顺序执行,前一个任务执行完成以后才能继续下一个任务。html
这个模式比较清晰,可是当任务耗时较长的时候,好比网络请求,定时器和事件监听等,这个时候后续任务继续等待,效率比较低。咱们常见的页面无响应,有时候就是由于任务耗时长或者无限循环等形成的。那如今是怎么解决这个问题呢。。。。前端
首先维护了一个“任务队列”。JavaScript 虽然是单线程的,但运行的宿主环境(浏览器)是多线程的,浏览器为这些耗时任务开辟了另外的线程,主要包括 http 请求线程,浏览器定时触发器,浏览器事件触发线程。这些线程主要把任务回调,放在任务队列里,等待主线程执行。
简单介绍以下图: java
这样就实现了 JavaScript 的单线程异步,任务被分为同步任务和异步任务两种:
同步任务:排队执行的任务,后一个任务等待前一个任务结束。
异步任务:放入任务队列的任务,将来才会触发执行的事件。程序员
异步任务分为宏任务和微任务。es6
宏任务,其实就是标准机制下的常规任务,即”任务队列中“等待被主线程执行的事件,是由浏览器宿主发起的任务,例如:编程
宏任务会被放在宏任务队列里,先进先出的原则,两个宏任务中间可能会被插入其余系统任务,间隔时间不定,效率较低 。数组
因为宏任务间隔不定,时间颗粒大,对于实时性要求比较高的场景就须要更精确地控制,须要把任务插入到当前宏任务执行,从而产生了微任务的概念。
微任务是 JavaScript 引擎发起的,是须要异步执行的函数。例如:promise
在执行 JavaScript 脚本,建立全局执行上下文的时候,JavaScript 引擎就会建立一个微任务队列,在执行当前宏任务时,产生的微任务都会保存到微任务队列里。在宏任务主函数执行结束以后,宏任务结束以前,清空微任务队列。
微任务和宏任务是绑定的,每一个宏任务都会建立本身的微任务: 浏览器
主线程运行 JavaScript 代码时,会生成个执行栈(先进后出),管理主线程上函数调用关系的数据结构。
当执行栈中的全部同步任务执行完毕,系统就会不断的从"任务队列"中读取事件,这个过程是循环不断的,称为 Event Loop(事件循环)。
事件循环机制调度宏任务和微任务,机制以下:
由于微任务自身能够入列更多的微任务,且事件循环会持续处理微任务直至队列为空,那么就存在一种使得事件循环无尽处理微任务的真实风险。如何处理递归增长微任务是要谨慎而行的。
回调函数是一个函数被当作参数传递给另外一个函数,另外一个函数完成以后执行回调。好比 Ajax 请求、IO 操做、定时器的回调等。
下面是 setTimeout 例子:
console.log('setTimeout 调用以前')
setTimeout(() => {console.log('setTimeout 输出')}, 0);
console.log('setTimeout 调用以后')
// 结果
setTimeout 调用以前
setTimeout 调用以后
setTimeout 输出
复制代码
setTimeout 回调放入任务队列中,当主线程的同步代码执行完以后,才会执行任务队列的回调,因此是如上的输出结果。
优势:回调函数相对比较简单、容易理解。
缺点:不利于代码的阅读和维护,各个部分之间高度耦合,流程会很混乱,并且每一个任务只能指定一个回调函数,易造成回调函数地狱。以下:
setTimeout(function(){
let value1 = step1()
setTimeout(function(){
let value2 = step2(value1)
setTimeout(function(){
step3(value2)
},0);
},0);
},0);
复制代码
Promise 是 ES6 新增的异步编程的方式,在必定程度上解决了回调地域的问题。简单说就是一个容器,里面保存着某个将来才会结束的事件(一般是一个异步操做)的结果。从语法上说,Promise 是一个对象,从它能够获取异步操做的消息。
使用 Promise 首先要明白如下特色:
下面的例子就是常见的异步操做,主要是使用的 then 和 catch:
new Promise((resolve) => {
resolve(step1())
}).then(res => {
return step2(res)
}).catch(err => {
console.log(err)
})
复制代码
step1 和 step2 是异步操做,step1 执行完以后的返回值会透传给 then 回调,当作 step2 的入参,经过 then 一层层的代替回调地域。其中 then 的回调会加入微任务队列。
当 Promise 入参是同步代码时:
console.log('start')
new Promise((resolve) => {
console.log('开始 resolve')
resolve('resolve 返回值')
}).then(data => {
console.log(data)
})
console.log('end')
// 原生 promise 输出结果
start
开始 resolve
end
resolve 返回值
复制代码
首先看下 Promise 的极简实现:
class Promise {
constructor (executor) {
// 回调值
this.value = ''
// 成功的回调
this.onResolvedCallbacks = []
executor(this.resolve.bind(this))
}
resolve (value) {
this.value = value
this.onResolvedCallbacks.forEach(callback => callback())
}
then (onResolved, onRejected) {
this.onResolvedCallbacks.push(() => {
onResolved(this.value)
})
}
}
// 此时上面例子执行结果以下
start
开始 resolve
end
复制代码
因为 Promise 是延迟绑定机制(回调在业务代码的后面),executor 是同步代码时,在执行到 resolve 的时候,尚未执行 then,因此 onResolvedCallbacks 是空数组。这个时候须要让 resolve 延后执行,能够先加一个定时器。以下:
resolve (value) {
setTimeout(() => {
this.value = value
this.onResolvedCallbacks.forEach(callback => callback())
})
}
复制代码
输出结果和预期是一致的,这里使用 setTimeout 来延迟执行 resolve。可是 setTimeout 是宏任务,效率不高,这里只是用 setTimeout 代替,在浏览器中,JavaScript 引擎会把 Promise 回调映射到微任务,既能够延迟被调用,又提高了代码的效率。
优势:
缺点:
Generator 是 ES6 提供的异步解决方案,其最大的特色就是能够控制函数的执行。整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器,异步操做须要暂停的地方,都用 yield 语句注明。
Generator 函数的特征:
function* getData () {
let value1 = yield 111
let value2 = yield value1 + 111 // 这里的 value1 就是下面传入的 val1.value
return value2
}
let meth = getData()
let val1 = meth.next()
console.log(val1) // { value: 111, done: false }
let val2 = meth.next(val1.value)
console.log(val2) // { value: 222, done: false }
let val3 = meth.next(val2.value)
console.log(val3) // { value: 222, done: true }
复制代码
{value: 111, done: false}
;{value: 222, done: false}
;{ value: 222, done: true }
。Generator 是协程的一种实现方式。
协程:协程是一种比线程更加轻量级的存在,协程处在线程的环境中,一个线程能够存在多个协程,能够将协程理解为线程中的一个个任务。经过应用程序代码进行控制。
上面的例子中协程具体流程以下:
meth 协程和父协程在主线程上交替执行,经过 next() 和 yield 进行控制,只有用户态,切换效率高。
优势:Generator 是以一种看似顺序、同步的方式实现了异步控制流程,加强了代码可读性。
缺点:须要手动 next 执行下一步。
async/await 将 Generator 函数和自动执行器,封装在一个函数中,是 Generator 的一种语法糖,简化了外部执行器的代码,同时利用 await 替代 yield,async 替代生成器的(*)号。
async 和 Generator 相比改进的地方:
下面来看个 sleep 的例子:
function sleep(time) {
return new Promise((resolve, reject) => {
time+=1000
setTimeout(() => {
resolve(time);
}, 1000);
});
}
async function test () {
let time = 0
for(let i = 0; i < 4; i++) {
time = await sleep(time);
console.log(time);
}
}
test()
// 输出结果
1000
2000
3000
复制代码
执行结果每隔一秒会输出 time,await 是等待的意思,等待 sleep 执行完毕后经过 resolve 返回,才会继续执行,间隔至少一秒。
把 async/await 转成 Generator 和 Promise 来实现。
function test () {
let time = 0
// stepGenerator 生成器
function* stepGenerator() {
for (let i = 0; i < 4; i++) {
let result = yield sleep(time);
console.log(result);
}
}
let step = stepGenerator()
let info
return new Promise((resolve) => {
// 自执行 next()
function stepNext () {
info = step.next(time)
// 执行结束则返回 value
if (info.done) {
resolve(info.value)
} else {
// 遍历没有结束 ,继续执行
return Promise.resolve(info.value).then((res) => {
time = res
return stepNext()
})
}
}
stepNext()
})
}
test()
复制代码
优势:是 Generator 更简化的方式,至关于自动执行 Generator,代码更清晰,更简单。
缺点:滥用 await 可能会致使性能问题,由于 await 会阻塞代码,非依赖代码失去并发性。
多个异步的执行顺序问题是很考验对异步的理解的。下面咱们把 setTimeout、Promise、async/await 放在一块儿,看下返回结果和预想的是否一致:
console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0);
async function test () {
let a = await 'await-result'
console.log(a)
}
test()
new Promise(function(resolve) {
console.log('promise-resolve')
resolve()
}).then(function() {
console.log('promise-then')
})
console.log('end')
//执行结果
start
promise-resolve
end
await-result
promise-then
setTimeout
复制代码
上述例子中,外层主程序 和 setTimeout 都是宏任务,Promise 和 async/await 是微任务,因此整个流程以下:
前端程序员平常代码常常会用到异步编程,了解异步运行的机制和顺序有助于更流畅清晰的实现异步代码,这里主要分析了异步的由来和异步代码实现,可结合不一样的场景和要求进行选择。