单线程与异步
js是单线程的,也就是一次只能执行一段代码。但是浏览器是事件驱动的,浏览器中很多行为都是异步的,会创建事件并放入执行队列中。
首先了解下同步与异步的区别
- 同步任务:直接在主线程上排队执行
- 异步任务:放到任务队列中,如果有多个异步任务则要在队列中排队等待
js是单线程的,如何实现异步操作
js的异步是通过回调函数实现的,即通过任务队列(任务队列类似一个缓冲区,任务下一步会被移到调用栈(call stack),然后主线程执行调用栈的任务),在主线程执行完当前的任务栈(所有的同步操作),主线程空闲后轮询任务队列,并将任务队列中的任务取出来执行。
异步运行机制如下: 1.所有同步任务都在主线程上执行,形成一个执行栈 2.主线程之外,还存在一个任务队列(task queue)
,只要异步任务有了运行结果,就在任务队列
中放置一个事件 3.一旦执行栈
中的所有同步任务执行完,系统就会读取任务队列
,对应的异步任务就会结束等待,进入执行栈开始执行 4.主线程不断重复第三步
执行栈
每一个函数执行的时候,都会生成新的execution context(执行上下文)
,执行上下文会包含一些当前函数的参数、局部变量之类的信息,它会被推入栈中。当函数执行完后,它的执行上下文会从栈中弹出。
js中有三种执行上下文:
- 全局执行上下文,默认的,在浏览器中是 window 对象,并且 this 在非严格模式下指向它
- 函数执行上下文,js的函数每当被调用时会创建一个上下文
- Eval 执行上下文
举个简单的例子:
function foo() {
console.log(1)
bar()
console.log(3)
}
function bar() {
console.log(2)
}
foo()
- 当
foo()
函数被调用时,将 foo 函数的执行上下文压入执行栈,接着执行输出1
- 当
bar()
函数被调用时,将bar函数的执行上下文压入执行栈,接着输出2
bar()
函数执行完毕,被弹出执行栈,foo()
函数接着执行,输出3
foo()
函数执行完毕,被弹出执行栈
调用栈
调用栈是一个具有LIFO(后进先出)结构的堆栈,用于存储在代码执行期间创建的所有执行上下文。与执行栈一样,函数调用会形成一个栈帧,帧中包含了当前执行函数的执行上下文,函数执行完后,它的执行上下文会从栈中弹出。
宏任务(macrotask)
script
全部代码、setTimeout
、setInterval
、setImmediate
、I/O
、UI Rendering
微任务(microtask)
Process.nextTick
、Promise
、Object.observe(废弃)
、MutationObserver
事件循环
HTML规范中介绍:
- There must be at least one event loop per user agent, and at most one event loop per unit of related similar-origin browsing contexts.
- An event loop has one or more task queues.
- Each task is defined as coming from a specific task source.
浏览器至少有一个事件循环,一个事件循环至少一个任务队列(macrotask),每个任务都有自己的分组,浏览器会为不用的任务组设置优先级。
一次事件循环包括: 1. 检查macrotask队列是否为空,非空则到步骤2,为空则到步骤3
2. 执行macrotask中的一个任务
3. 继续检查microtask队列是否为空,非空则到步骤4,为空则到步骤5
4. 取出microtask中的任务执行,执行完成后返回到步骤3
5. 执行视图更新
宏任务与微任务执行顺序: