学不动了怎么办?换个姿势继续学。
JS引擎
js引擎执行过程分为三个阶段:
- 解释阶段
js代码加载完毕后进入此阶段,此阶段完成由字符串到机器码的转变过程。
- 预处理(编译)阶段与执行阶段
由于js是解释性语言,所以会一边编译一边执行。创建变量对象发生在预编译阶段(编译阶段生成执行上下文,执行上下文包含变量,作用域链和this),变量赋值发生在执行阶段。
同步VS异步
浏览器内核是多线程的,和js执行相关的线程包含:
- js引擎主线程,负责解释和执行js代码。
- ajax请求线程。
- DOM处理线程。
- 定时任务线程。
所有同步任务都是在js引擎主线程上按照顺序一次执行,形成一个执行栈,任务的执行也是任务在执行栈上的不断入栈和出栈。除了执行栈,事件触发线程管理一个任务队列,异步任务有了结果,会向任务队列放入回调。一旦所有同步任务执行完毕之后(js引擎空闲),会读取任务队列,将任务放入执行栈并执行。
**事件循环**(Event Loop)
指的是脚本运行时,先依次运行执行栈,然后从任务队列中取出事件执行来运行任务队列中的任务,这个过程是不断重复的。
异步编程
回调函数
回调函数是js异步编程的常用方式,但是回调函数容易层层嵌套形成回调地狱。
const getData = (data, callback) => {
setTimeout(() => {
callback(data + 1)
}, 1000)
}
getData(1, function (v1) {
getData(v1, function (v2) {
getData(v2)
})
})
层层嵌套的代码,不仅影响代码逻辑,而且可读性非常差,可以用Promise 对象、Generator 函数、async 函数替代,使得回调扁平化。
Promise
Promise对象提供then方法,then方法可以链式调用,链式调用时then方法依次执行。
const getData = function (data) {
return new Promise(resolve => {
setTimeout(() => {
resolve(data + 1)
}, 1000)
})
}
getData(1)
.then(v1 => {
return getData(v1)
})
.then(v2 => {
return getData(v2)
})
Promise对象不仅能解决回调地狱问题,同时还提供了静态的all和race方法。all方法接收一个数组,当数组中存在异步调用的时候,只有异步方法全部执行完成之后,then方法才执行。race方法同样接受一个数组,与all不同的是,参数数组中的多个异步调用,如果其中一个完成或者失败,那么then方法就会执行,可以用race方法实现ajax超时判断。
const getData = function (data) {
return new Promise(resolve => {
setTimeout(() => {
resolve(data + 1)
}, 1000)
})
}
function timeout(duration) {
return Promise.race([
getData(1),
new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('调用超时'))
}, duration)
})
])
}
timeout(500).then(value => {
console.log(value)
}).catch(err => {
console.log(err.message)
})
由于设置的超时时间是500毫秒,此时getData异步函数尚未执行完成,因此race方法会捕获到超时错误。
Generator
Generator生成器,可以利用*和yield关键字解决异步嵌套的回调地狱问题。
const getData = function (data) {
return new Promise(resolve => {
setTimeout(() => {
resolve(data + 1)
}, 1000)
})
}
function* gen() {
let v1 = yield getData(1)
console.log(v1)
// yield会使函数暂停执行,当调用next方法时会继续执行,同时next方法传入的参数会作为yield的返回值
let v2 = yield getData(v1)
console.log(v2)
}
function co(generator) {
let g = generator()
function handleResult(result) {
if (result.done) return
else {
result.value.then((value) => {
handleResult(g.next(value))
}, err => {
console.log(err)
})
}
}
handleResult(g.next())
}
co(gen)
async
通常async和await一起使用,是一种语法糖,可以理解为通过async和await关键字可以更方便的实现上节中的generator。
const getData = function (data) {
return new Promise(resolve => {
setTimeout(() => {
resolve(data + 1)
}, 1000)
})
}
async function main() {
let v1 = await getData(1)
console.log(v1)
// yield会使函数暂停执行,当调用next方法时会继续执行,同时next方法传入的参数会作为yield的返回值
let v2 = await getData(v1)
console.log(v2)
}
main()
微任务vs宏任务
如果执行下面的代码:
setTimeout(() => {
console.log('timeout')
}, 0)
new Promise(resolve => {
resolve('promise')
}).then(v => {
console.log(v)
})
会发现,同样是异步操作,Promise的回调函数会早于setTimeout的回调函数,这就涉及到微任务(Promise等)和宏任务(setTimeout等)。
宏任务和微任务均为异步任务,只是执行顺序不同,通常情况下,会优先执行微任务,当微任务执行完毕之后,再执行宏任务。
盗用网上的执行逻辑图:
常见的宏任务有:setTimeout(同等延迟时间下,setTimeOut的优先级高于setImmediate),setInterval, setImmediate, I/O, UI rendering。
常见的微任务有:Promise,process.nextTick(在node环境中nextTick优先级高于promise)。