10 分钟掌握浏览器运行 JS 的顺序

前言

不知道你有没有遇到过类似这样的问题,某些代码乱序执行或样式的更改后不生效?你是不是曾经把代码包在 setTimeout 里面来解决类似的问题?是不是这种方式不太可靠?然后你就不断调试 timeout 值以至于看起来好像没问题了?接下来我们将一起来看一下这其中到底发生了什么。

进程和线程

我们先来区分一下进程和线程

  • 进程是 cpu 资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是 cpu 调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

通俗地讲:进程就像是一个有边界的生产厂间,而线程就像是厂间内的一个个员工,可以自己做自己的事情,也可以相互配合做同一件事情,所以一个进程可以创建多个线程

理解了进程与线程了区别后,接下来对浏览器进行一定程度上的认识:

浏览器的多进程

它主要包括以下进程:

浏览器进程.png

  • 浏览器是多进程的
  • 浏览器之所以能够运行,是因为系统给它的进程分配了资源(CPU、内存)
  • 简单点理解,每打开一个 Tab 页,就相当于创建了一个独立的浏览器进程。

渲染进程(浏览器内核)

对于普通的前端操作来说,最重要的是渲染进程,页面的渲染,JS 的执行,事件的循环,都在这个进程内进行。接下来我们重点分析这个进程。注意:浏览器的渲染进程是多线程的。
接下来看看它都包含了哪些线程(列举一些主要常驻线程):

渲染线程.png

单线程的 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 引擎线程空闲),事件触发线程才会从任务队列取出一个任务(即异步的回调函数)放入执行栈中执行。具体的流程可以参考下图:
事件循环.png

运行机制

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
  • 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从事件队列中获取)

事件循环流程图.png

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
复制代码

完结

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享