前言
不知道你有没有遇到过类似这样的问题,某些代码乱序执行或样式的更改后不生效?你是不是曾经把代码包在 setTimeout 里面来解决类似的问题?是不是这种方式不太可靠?然后你就不断调试 timeout 值以至于看起来好像没问题了?接下来我们将一起来看一下这其中到底发生了什么。
进程和线程
我们先来区分一下进程和线程
- 进程是 cpu 资源分配的最小单位(是能拥有资源和独立运行的最小单位)
- 线程是 cpu 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
通俗地讲:进程就像是一个有边界的生产厂间,而线程就像是厂间内的一个个员工,可以自己做自己的事情,也可以相互配合做同一件事情,所以一个进程可以创建多个线程。
理解了进程与线程了区别后,接下来对浏览器进行一定程度上的认识:
浏览器的多进程
它主要包括以下进程:
- 浏览器是多进程的
- 浏览器之所以能够运行,是因为系统给它的进程分配了资源(CPU、内存)
- 简单点理解,每打开一个 Tab 页,就相当于创建了一个独立的浏览器进程。
渲染进程(浏览器内核)
对于普通的前端操作来说,最重要的是渲染进程,页面的渲染,JS 的执行,事件的循环,都在这个进程内进行。接下来我们重点分析这个进程。注意:浏览器的渲染进程是多线程的。
接下来看看它都包含了哪些线程(列举一些主要常驻线程):
单线程的 JS
JavaScript 单线程指的是浏览器中负责解释和执行 JavaScript 代码的只有一个主线程,即为 JS 引擎线程,每次只能做一件事。
我们知道一个 AJAX 请求,主线程在等待它响应的同时是会去做其它事的,浏览器先在事件表注册 AJAX 的回调函数,响应回来后回调函数被添加到任务队列中等待执行,不会造成线程阻塞,所以说 JS 处理 AJAX 请求的方式是异步的。
JavaScript 的单线程,与它的用途有关。作为浏览器脚本语言,其主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。假设同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,那就乱套了。
我们可以来看下面的例子:
function foo() {
bar()
console.log('foo')
}
function bar() {
baz()
console.log('bar')
}
function baz() {
console.log('baz')
}
foo()
// baz
// bar
// foo
复制代码
function foo() {
console.log('foo');
}
setTimeout(() => {
console.log('setTimeout');
}, 0);
foo();
// foo
// setTimeout: 0s
复制代码
任务队列
任务队列是指一个事件的队列(消息队列),先进先出的数据结构,排在前面的事件,优先被主线程读取。只要执行栈上任务一清空,就会被主线程读取,任务队列上首位的事件就自动进入主线程。
任务又分成两种,一种是同步任务,另一种是异步任务。
- 同步任务,在主线程形成一个执行栈,排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
- 异步任务,不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
另外事件循环中的异步任务队列有两种:macroTask(宏任务)队列和 microTask(微任务)队列。
宏任务队列
- 执行整体的 JS 代码
- DOM 事件
- XHR
- 定时器(setTimeout / setInterval / setImmediate)
可以通过 setTimeout(func) 即可将 func 添加到宏任务队列中(使用场景:将计算耗时长的任务切分成小块,以便于浏览器有空处理用户事件,以及显示耗时进度)。
微任务队列
- Promise 事件
- MutationObserver
- process.nextTick 事件(Node.js)
可以通过 queueMicrotask(func) 将 func 添加到微任务队列中。
事件循环
JS 主线程循环往复地从任务队列(callback queue / task queue)中读取任务,执行任务,其中运行机制称为事件循环(event loop)。
在事件表 Web APIs 中会注册各类事件线程处理各种事件,然后将处理好的回调事件放入对应的任务队列(宏任务、微任务)中。如果执行栈里的任务执行完成,即执行栈为空的时候(JS 引擎线程空闲),事件触发线程才会从任务队列取出一个任务(即异步的回调函数)放入执行栈中执行。具体的流程可以参考下图:
运行机制
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
- 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从事件队列中获取)
setTimeout
我们一起来看下面的代码:
console.log('script start');
setTimeout(() => {
console.log('setTimeout'); // 调用 setTimeout 函数,将其回调函数放入宏任务队列
}, 0);
console.log('script end');
// script start
// script end
// setTimeout
复制代码
Promise
- Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候,此时是异步操作,会先执行 then/catch 等(即 then/catch 里面的东西才是异步执行的部分)。当主栈完成后,才会去任务队列中调用 resolve/reject 中存放的回调方法执行,返回一个 Promise 实例。
多说无益,我们来看下面的代码:
console.log('script start');
Promise.resolve().then(() => {
console.log('promise1'); // Promise 将其回调函数 then 放入微任务队列
}).then(() => {
console.log('promise2'); // 同上
});
console.log('script end');
// script start
// script end
// promise1
// promise2
复制代码
async/await
- async/await 本质上还是基于 Promise 的封装。
- async 总是返回一个 Promise。
- async 函数在 await 之前的代码都是同步执行的,可以理解为 await 之前的代码相当于 Promise executor 中的代码,await 之后的所有代码都是在 Promise.then 中的回调。
async function foo() {
console.log(1)
await 2
console.log(3)
}
等价于
function foo() {
console.log(1)
return Promise.resolve(2).then(console.log(3))
}
复制代码
接着,我们来看一下下面的代码:
console.log('script start');
async function async1() {
console.log('async1 start')
await async2() // 执行 async2,暂停整个 async 函数的执行并让出执行栈
console.log('async1 end') // Promise.then 将其加入微任务队列
}
async function async2() {
await console.log('async2')
}
async1()
console.log('script end');
// script start
// async1 start
// async2
// script end
// async1 end
复制代码
牛刀小试
最后放几道题大家一起来考察一下自己的掌握程度吧。
第一题
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
}).then(() => {
console.log('promise2');
});
async function async1() {
console.log('async1 start');
await async2()
console.log('async1 end');
}
async function async2() {
await console.log('async2');
}
async1();
console.log('script end');
// script start
// async1 start
// async2
// script end
// promise1
// promise2
// async1 end
// setTimeout
复制代码
第二题
console.log('script start');
setTimeout(() => {
console.log('setTimeout');
}, 0)
new Promise((resolve) => {
console.log('promise3');
resolve();
}).then(() => {
console.log('promise4');
});
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
}
async1();
console.log('script end');
// script start
// promise3
// async1 start
// promise1
// script end
// promise4
// promise2
// async1 end
// setTimeout
复制代码
第三题
console.log('script start')
setTimeout(() => {
console.log('setTimeout3')
}, 0)
Promise.resolve().then(() => {
console.log('promise1')
})
async function async1() {
console.log('async1')
await setTimeout2()
setTimeout(() => {
console.log('setTimeout1')
}, 0)
}
async function setTimeout2() {
setTimeout(() => {
console.log('setTimeout2')
},0)
}
async1()
let promise2 = new Promise((resolve) => {
resolve('promise2.then')
console.log('promise2')
})
promise2.then((res) => {
console.log(res)
Promise.resolve().then(() => {
console.log('promise3')
})
})
console.log('script end')
// script start
// async1
// promise2
// script end
// promise1
// promise2.then
// promise3
// setTimeout3
// setTimeout2
// setTimeout1
复制代码
完结