浏览器的进程与线程
- 浏览器是多进程的,比如打开多个窗口可能就对应着多个进程,这样可以确保页面之间相互没有影响,一个页面卡死也不会影响其他的页面
- 对于浏览器进程来说,它是多线程的:GUI渲染线程、JS引擎线程、事件触发线程、定时触发器线程、异步http请求线程
1、GUI渲染线程
负责渲染浏览器页面,解析html+css,构建DOM树,进行页面的布局和绘制操作,同事页面需要重绘或者印发回流时,都是该线程负责执行
2、JS引擎线程
负责解析和运行JS脚本,一个页面中永远都只有一个JS线程来负责运行JS程序,这就是我们常说的JS单线程
注意:JS引擎线程和GUI渲染线程永远都是互斥的,所以当我们的JS脚本运行时间过长时,或者有同步请求一直没返回时,页面的渲染操作就会阻塞,就是我们常说的卡死了
3、事件触发线程
接受浏览器里面的操作事件响应。如在监听到鼠标、键盘等事件的时候, 如果有事件句柄函数,就将对应的任务压入队列
4、定时触发器线程
定时触发器线程 浏览器模型定时计数器并不是由JavaScript引擎计数的, 因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 它必须依赖外部来计时并触发定时
5、异步http请求线程
异步http请求线程 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行
JS单线程
因为只有JS引擎线程负责处理JS脚本程序,所以说JS是单线程的。可以理解的是js当初设计成单线程语言的原因是因为js需要操作dom,如果多线程执行的话会引入很多复杂的情况,比如一个线程删除dom,一个线程添加dom,浏览器就没法处理了。虽然现在js支持webworker多线线程了,但是新增的线程完全在主线程的控制下,为的是处理大量耗时计算用的,不能处理DOM,所以js本质上来说还是单线程的。
1、同步异步
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
2、任务队列
- 任务队列就是用来存放一个个带执行的异步操作的队列,是一个先进先出的数据结构,排在前面的事件,优先被主线程读取
- 每完成一个任务,都会去检查任务队列中还有没有任务需要执行,直到任务队列被清空,这个操作叫做事件轮询(Event Loop)
- Event Loop只是负责告诉还有没有任务待执行,该执行哪些任务,真正的逻辑还是在主线程逐段逐段去中执行的
- 任务队列中的都是已经完成的异步操作,而不是说注册一个异步任务就会被放在这个任务队列中
在ES6中又将任务队列分为宏任务队列和微任务队列
宏任务队列(macrotask queue)
macrotask是由宿主环境分发的异步任务,事件轮询的时候总是一个一个任务队列去查看执行的。一次宏任务结束之后总会去检查还有没有宏任务需要处理。
微任务队列(microtask queue)
是由js引擎分发的任务,总是添加到当前宏任务末尾执行。如果在处理microtask期间,如果有新添加的microtasks,会被添加到当前微任务队列的末尾执行,在当前的微任务队列没有被清空时,是不会执行下一个宏任务的
宏任务清单和微任务清单
宏任务:
(1)定时器(setTimeout、setInterval)、异步请求(ajax/fetch/axios)、DOM事件(click)
它们分属于三个线程来分别处理
- 对于定时器,定时触发器线程在接收到代码时就开始计时,时间到了将回调函数扔进队列
- 对于异步请求,异步http请求线程立即发起http请求,请求成功后将回调函数扔进队列
- 对于DOM事件,事件触发线程会先监听dom,直到dom被点击了,才将回调函数扔进队列
(2)I/O(比如node中的fs.readFile())、requestAnimationFrame
- requestAnimationFrame 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,你可以传这个值给 window.cancelAnimationFrame() 以取消回调函数。
const element = document.getElementById('some-element-you-want-to-animate');
let start;
function step(timestamp) {
if (start === undefined)
start = timestamp;
const elapsed = timestamp - start;
// `Math.min()` is used here to make sure that the element stops at exactly 200px.
element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';
if (elapsed < 2000) { // Stop the animation after 2 seconds
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
微任务:process.nextTick(node环境)、MutationObserver、Promise.then catch finally
- process.nextTick用于node环境
- MutationObserver接口提供了监视对DOM树所做更改的能力,它是一个构造函数
// 选择需要观察变动的节点
const targetNode = document.getElementById('some-id');
// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 当观察到变动时执行的回调函数
const callback = function(mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for(let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);
// 之后,可停止观察
observer.disconnect();
- Promise.then catch finally
new Promise里面的代码是在主线程中直接执行的,只有then/catch/finally会被加入微任务队列,它在什么时候执行,取决于他被哪个宏任务resolve或者reject
JS异步处理方式
1、callback
将函数作为参数传递,在异步任务执行环境中调用,但是多层嵌套容易引起回调地狱
2、Promise
使用Promise可以解决回调地狱的问题,是一种以广度换深度的解决方式
可以参考Promise/A+规范来了解Promise的解析过程
- Promise.resolve() 可以有四种传参方式
/* 跟 Promise 对象 */
Promise.resolve(Promise.resolve(1))
// Promise {state: "resolved", data: 1, callbackQueue: Array(0)}
/* 跟 thenable 对象 */
var thenable = {
then: function(resolve, reject) {
resolve(1)
}
}
Promise.resolve(thenable)
// Promise {state: "resolved", data: 1, callbackQueue: Array(0)}
/* 普通参数 */
Promise.resolve(1)
// Promise {state: "resolved", data: 1, callbackQueue: Array(0)}
/* 不跟参数 */
Promise.resolve()
// Promise {state: "resolved", data: undefined, callbackQueue: Array(0)}
- Promise.reject() 原封不动地返回参数值
- Promise.all(arr) 在参数数组中所有元素都变为决定态后,然后才返回新的 promise
const p1 = request(`http://some.url.1`)
const p2 = request(`http://some.url.2`)
Promise.all([p1, p2])
.then((datas) => { // 此处 datas 为调用 p1, p2 后的结果的数组
return request(`http://some.url.3?a=${datas[0]}&b=${datas[1]}`)
})
.then((data) => {
console.log(msg)
})
- Promise.race 只要参数数组有一个元素变为决定态,便返回新的 promise
// race 译为竞争,同样是请求两个 url,当且仅当一个请求返还结果后,就请求第三个 url
const p1 = request(`http://some.url.1`)
const p2 = request(`http://some.url.2`)
Promise.race([p1, p2])
.then((data) => { // 此处 data 取调用 p1, p2 后优先返回的结果
return request(`http://some.url.3?value=${data}`)
})
.then((data) => {
console.log(data)
})
- Promise.wrap(fn) 将一个普通函数promise化,使用到了闭包
function foo(a, b, cb) {
ajax(
`http://some.url?a=${a}&b=${b}`,
cb
)
}
const promiseFoo = Promise.wrap(foo)
promiseFoo(1, 2)
.then((data) => {
console.log(data)
})
.catch((err) => {
console.log(err)
})
- then/catch/done
Promise.resolve(1)
.then((data) => {console.log(data)}, (err) => {console.log(err)}) // 链式调用,可以传一个参数(推荐),也可以传两个参数
.catch((err) => {console.log(err)}) // 捕获链式调用中抛出的错误 || 捕获变为失败态的值
.done() // 能捕获前面链式调用的错误(包括 catch 中),可以传两个参数也可不传
3、async/await
假设一个业务需要分步完成,每个步骤都是异步的,而且依赖上一步的执行结果,甚至依赖之前每一步的结果,就可以使用Async Await来完成 可以解决promise无限then的问题,还可以实现try/catch
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(m, n) {
console.log(`step2 with ${m} and ${n}`);
return takeLongTime(m + n);
}
function step3(k, m, n) {
try{
let content1 = await read('./2.promise/100.txt', 'utf8');
let content2 = await read(content1, 'utf8');
return content2;
} catch(e) { // 如果出错会catch
console.log('err', e)
}
}
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time1, time2);
const result = await step3(time1, time2, time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt().then(function(data){
console.log('data',data);
},function(err){
console.log('err1',err);
})
如果用promise来实现
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => {
return step2(time1, time2)
.then(time3 => [time1, time2, time3]);
})
.then(times => {
const [time1, time2, time3] = times;
return step3(time1, time2, time3);
})
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
3、Generator/co
直接参考 Generator用法详解+co