涉及到的知识点
- 多线程
- awync await
- vue nextTick
- react事件切片
- node事件循环
事件循环(Event Loop):
JavaScript是单线程的,意味着所有任务需要排队,但如果前一个任务耗时很长,后一个任务就要一直等着,会导致网页卡顿。为了解决这个问题,设计了事件循环机制,就是任务的调度机制,先执行同步任务,再执行异步任务。
- 同步任务: 在主线程上排队执行的任务
- 异步任务: 放到任务队列中,等待执行的任务
异步任务之宏任务和微任务
宏任务
- script
- setTimeout
- setInterval
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
微任务
- Promise.then/resolve/catch/finally
- MutationObserver: 监听dom发生改变(浏览器独有)
- node.nextTick
执行顺序
work1(一次同步任务要干的事): 先执行同步代码,遇到宏任务就放入宏任务队列中,遇到微任务,就放入微任务队列中,
- 执行work1
- 同步任务执行完成后,先检查是否有微任务,有就先执行微任务,执行work1
- 微任务队列已空,执行宏任务,按放入顺序执行, 执行work1
- 渲染
- 渲染过程
- 判断是否调用
requestIdleCallback回调,一般在浏览器空闲时会调用。
渲染过程
下面条件至少成立一个, 否则跳过本步骤
-
浏览器判断更新渲染会带来视觉上的改变
-
map of animation frame callbacks不为空(可以通过requestAnimationFrame添加) 则进行下面几步,- 如果窗口的大小发生了变化,执行监听的
resize方法。 - 如果页面发生了滚动,执行
scroll方法。 - 执行
requestAnimationFrame的回调,并传入now作为时间戳。 - 执行
IntersectionObserver的回调,并传入now作为时间戳。 - 重新渲染绘制用户界面。
- 如果窗口的大小发生了变化,执行监听的
IntersectionObserver
这个方法是用来检查元素是否在视图窗口内,兼容性较差,
- 第一个参数是回调函数,
- 第二个参数中
root属性指定目标元素所在的容器节点threshold决定了什么时候触发回调函数
const intersectionObserver = new IntersectionObserver(
(entries) => {
console.log("触发:");
entries.forEach((item) => {
console.log(item.target, item.intersectionRatio);
});
},
{
threshold: [0.5, 1],
}
);
intersectionObserver.observe(document.querySelector("#box1"));
intersectionObserver.observe(document.querySelector("#box2"));
注意:
- IntersectionObserver是异步的
IntersectionObserver的实现,应该采用requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。
alert阻塞问题
alert会阻塞JS引擎线程和GUI渲染线程, 也就是阻塞代码的执行和页面渲染,
setTimeout执行问题
setTimeout(() => {
console.log("setTimeout");
}, 3000);
alert("haha");
问: 我们都知道点击弹窗确认后才会执行输出,那么setTimeout的计时是从代码执行时开始计时还是从点击确认后开始计时?
答: 从代码执行时开始计时,setTimeout是由定时触发器线程单独计时,但是由JS引擎线程负责执行,所以setTimeout的执行时间往往有误差
渲染进程:
-
GUI渲染线程: 负责渲染浏览器界面,解析和渲染。注意,GUI渲染线程与JS引擎线程是互斥的
-
JS引擎线程: 无论什么时候都只有一个JS线程在运行JS程序, 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
-
事件触发线程: DomEvent,如onclick事件,页面阻塞时,该线程会把事件添加到任务队列末尾,等待JS引擎的处理
-
定时触发器线程:
setInterval与setTimeout所在线程, 只负责计时,有JS线程负责执行, 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。 -
异步http请求线程: 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求,检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JS引擎执行。
debugger会产生阻塞吗?
debugger不会阻塞线程,只是把代码一行一行执行而已,所以可以看到页面的实时更改
async await执行顺序
await同行代码是会立即执行,await后面的代码(当前函数作用域)会变成微任务放入微任务队列中
demo:
const timeoutPromise = (fn, timer = 2000) => {
console.log("timeoutPromise");
return new Promise((resolve) => {
setTimeout(() => {
resolve(fn());
}, timer);
});
};
console.log("start");
const fn = async () => {
console.log("fn start");
await timeoutPromise(() => {
console.log("延迟2秒执行");
});
console.log("fn end");
};
fn();
new Promise((resolve) => {
console.log("Promise");
resolve();
}).then(() => {
console.log("then");
});
setTimeout(() => {
console.log("setTimeout");
});
console.log("end");
输出顺序:
start
fn start
timeoutPromise
Promise
end
then
setTimeout
延迟2秒执行
fn end
微任务嵌套微任务
需要先执行完微任务队列中的任务
console.log("start");
Promise.resolve()
.then(() => {
console.log("111");
})
.then(() => {
console.log("222");
})
.then(() => {
console.log("333");
})
.finally(() => {
console.log("444");
});
setTimeout(() => {
console.log("setTimeout");
});
console.log("end");
// 输出
start
end
111
222
333
444
setTimeout
setTimeout和requestAnimationFrame执行顺序
console.time("test");
setTimeout(() => {
console.log("setTimeout");
}, 13);
requestAnimationFrame(() => {
console.timeEnd("test");
console.log("raf");
});
输出顺序:
不确定, 取决于代码执行到渲染步骤之前setTimeout是否已被推入执行队列
实际应用
vue nextTick
nextTick是使用微任务和宏任务模拟
为什么nextTick可以使用微任务和宏任务模拟
更改dom是同步任务,渲染是异步,正常我们直接修改dom, 不使用nextTick也可以获取更改后的内容。由于vue使用了异步更新策略,我们修改this.data上的数据时,其实会把所有更新放入一个微任务队列中,我们需要借助nextTick确保获取的代码在更改代码后执行,
react 事件切片
react自己模拟实现了requestIdelcallback
let frameDeadline; // 当前帧的结束时间
let channel = new MessageChannel();
// requestAnimationFrame的回调执行是在当前的主线程中,
channel.port2.onmessage = function () {
let timeRema = frameDeadline - performance.now();
if (timeRema > 0) {
// 有空闲时间
}
};
// 计算当前帧的剩余时间
function timeRemaining() {
// 当前帧结束时间 - 当前时间
// 如果结果 > 0 说明当前帧还有剩余时间
return;
}
window.requestIdleCallback = function (callback) {
requestAnimationFrame((rafTime) => {
// 算出当前帧的结束时间 这里按照16.66ms一帧来计算
frameDeadline = rafTime + 16.66;
// MessageChannel是一个宏任务,也就是说上面onmessage方法会在当前帧执行完成后才执行
// 这样就可以计算出当前帧的剩余时间了
channel.port1.postMessage("haha");
});
};
为什么不使用RequestIdelCallback
- 实验 api,兼容情况一般。
- 实验结论: requestIdleCallback FPS只有20ms,
- RequestIdelCallback定位是不紧急或不重要的任务,
为什么使用MessageChannel,为什么不使用微任务
setTimeout有最小延迟4ms, 兜底使用setTimeout,
微任务会优先执行,会导致无法让出线程给浏览器
const box1 = document.getElementById("box1");
console.log("start");
box1.innerHTML = "end";
const callback = (rafTime) => {
console.log("requestAnimationFrame", performance.now());
new Promise(function (resolve) {
resolve();
}).then(function () {
// 还没渲染完成
alert(" promise ui 已经渲染完毕了吗? ");
});
setTimeout(function () {
// 渲染完成
alert("setTimeout ui 已经渲染完毕了吗? ");
});
};
requestAnimationFrame(callback);
判断输出顺序1
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
输出
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
判断输出顺序2
说明: 某厂的面试题,最好知道一下
Promise.resolve()
.then(() => {
console.log(0);
return Promise.resolve(4);
})
.then((res) => {
console.log(res);
});
Promise.resolve()
.then(() => {
console.log(1);
})
.then(() => {
console.log(2);
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(5);
})
.then(() => {
console.log(6);
});
输出顺序:
0
1
2
3
4
5
6
看输出是不是突然怀疑之前学到的知识了?
正常理解 输出: 0 1 2 4 3 5 6
- 执行Promise.resolve(4) 第一个微任务
- 返回值是4, 这是第二个微任务
实际: 输出: 0 1 2 3 4 5 6
- 发现返回值是一个promise, 将Promise.resolve(4)放入一个微任务队列中,这是第一个微任务,
- 执行Promise.resolve(4),会自动调用then方法,这是第二个微任务
- 返回值是4, 这是第三个微任务
参考文章:
www.zhihu.com/question/45…
node事件循环
- process.nextTick:微任务,但是优先级比setImmediate和setTimeout高
- setTimeout: 最小为1,浏览器最小为4ms
- setImmediate: 尽快执行的异步,和延迟为0的setTimeout输出顺序不一定
为什么setImmediate和延迟时间为0的setTimeout执行顺序不一定?
- 执行宏任务的时间不一定,如果小于1ms,先执行setImmediate,如果大于1ms,先执行setTimeout
- 如果在poll回调(I/O相关)中,顺序一定是setImmediate先执行,因为poll阶段后面是check阶段,setTimeout会在下一次事件循环中执行
setTimeout(() => {
setTimeout(() => {
console.log("setTimeout");
}, 0);
setImmediate(() => {
console.log("setImmediate");
});
}, 0);
var fs = require("fs");
fs.readFile(__filename, () => {
setTimeout(() => {
console.log("setTimeout");
}, 0);
setImmediate(() => {
console.log("setImmediate");
});
});