任务队列
线程
-
一个页面的进程主要有:
Browser进程、GPU进程、Renderer进程--浏览器内核
-
浏览器内核
主作用:渲染页面,执行脚本,处理事件
組成:由多个线程同時执行,相互协助
①、主线程:JS引擎
②、辅助线程:其他
二、主要线程:
-
JS引擎线程
-
GUI渲染线程
-
事件循环
-
異步线程
任务队列
-
作用:存储需要执行的回调函数
-
种类:宏任务与微任务
相互协助
一、主线程:JS引擎线程
执行同步代码
遇到異步程序,交给異步线程
二、辅助线程:
Ⅰ、GUI渲染线程
负责渲染:
解析HTML、CSS
构建DOM树和RenderObject树
布局和绘制
与主线程
GUI引擎在渲染时会阻塞js引擎计算
同一時刻只能執行主线程或者GUI线程
Ⅱ、異步线程:
种类:很多
定时器触发线程
http 异步线程
浏览器事件线程 等等
执行内容來源:
來自主线程的異步程序
鼠标点击、AJAX异步请求等等事件
异步代码在其线程中执行
执行完成后:
如果该事件有绑定回调函数,按先后完成顺序提交到任务队列中
三、事件循环线程
-
主任务:
在主线程执行完全部代码时--堆栈为空
从任务队列中获取任务提交给主线程执行
四、GUI与主线程的切换执行时间
主要情況:
在事件循环从任务队列提交任务前:
先查看是否需要重新渲染
如果需要重新渲染,触发GUI渲染頁面
等渲染完再执行主线程
其他情況:强制GUI渲染
如 getComputedStyle等
会触发UI强制重绘,js引擎停下,等待GUI渲染
五、程序的初执行
主线程一开始是空的,任务队列不为空
如其中宏任务有: 执行主线 (或全局)JS代码
这些任务会交给主线程执行
宏任务与微任务
1、宏任务macro-task
执行主线 (或全局)JavaScript代码、
更改当前URL以及各种事件,如页面加载、 输入、网络事件
setImmediate、MessageChannel、定时器事件
2、微任务micro-task
process.nextTick (node)、提案回调函数、MutationObserver(h5新特性)--监听一个DOM变动、Object.observe(已废弃)
不能产生全新的微任务
Ⅴ、进出顺序
1、进入:按異步线程提交到任务队列的先后
2、出去:先进先出
轮回tick
個人意译
⑴、主线程的执行过程
Ⅰ、首先执行一個宏任务队列
Ⅱ、在一個宏任务执行完,去执行微任务
Ⅲ、把微任务队列所有微任务执行完
Ⅳ、浏览器可以继续其他调度: 垃圾回收、頁面渲染等等
Ⅴ、执行下一個宏任务
与性能优化:
在操作数据保护实现重新渲染页面时
可以先把部分数据变化放到下一次轮回
在下一次轮回中进行数据去重
对于避免不必要的计算和 DOM 操作
⑷、Vue的nextTick
①、2.5前:
优先级
Promise.then => MutationObserver => setImmediate => setTimeout(fn, 0)
MutationObserver是微任务兼容性高
所以nextTick几乎是微任务
②、2.5+:移除了MutationObserver,使用MessageChannel
优先级
Promise => setImmediate || MessageChannel || setTimeout
1、默认是微任务方法Promise
2、对于一些节点交互事件,如 v-on 绑定的事件回调函数的处理,
会强制封为宏任务:调用 withMacroTask 方法包装
withMacroTask实现的优先级:
原生 setImmediate(IE 10 )
原生MessageChannel
setTimeout 0
③、2.6.0 beta回退
例
(0)、Vue发送XHR请求
可以在beforecreted中
-
XHR的回调函数是添加在微任务的
-
通常是在完成Vue的
mounted时,才是完成一个宏任务,才去取微任务的回调函数
⑴、定時器与提案
setTimeout(() => { console.log(1); Promise.resolve().then(() => { console.log(2) }); }, 100); new Promise((x, y) => { console.log(3); x(); console.log(4); }).then(() => { console.log(5); setTimeout(() => { console.log(6) }, 0); }); console.log(7);
执行过程
①、当前脚本代码是一个宏任务
执行该宏任务
②、该宏任务中:
Ⅰ、定时器
按代码先后顺序添加入定时器异步线程
在其异步线程执行完毕后,提交回调函数
该回调函数是宏任务
Ⅱ、执行提案构造器,其中在循环中,该提案状态变为完成状态
Ⅲ、提案回调函数:交到提案线程
在提案状态改变时,回调函数提交到任务队列
提案回调函数是微任务,放入微任务队列中
③、当前宏任务执行完毕,进入微任务
Ⅰ、执行提案回调
其中又提交一个定时器
④、当前微任务执行完毕,浏览器继续其他调度
如重新渲染页面的UI或执行垃圾回收
⑤、进入宏任务:执行第二个定时器回调函数
Ⅰ、第一个定时器,还没有完成时间限制,其回调函数不会添加到任务队列
Ⅱ、产生新微任务--提案回调
⑥、定时器回调函数执行完毕,进入微任务
执行提案回调
⑦、浏览器其他调度
⑧、第一个定时器回调函数
⑨、若在执行第一个宏任务--脚本任务时
Ⅰ、花费的时间足够第一个定时器完成计时
Ⅱ、则第一个定时器回调函数会优先于第二个提交到宏任务队列
Ⅲ、并且优先执行
⑩、提案回调的执行
Ⅰ、只能在微任务中
Ⅱ、在状态改变时,其回调函数立马添加进微任务队列
Ⅲ、若在一个提案的状态在执行微任务中改变
1、该提案其回调函数会在执本轮微任务中行,如:提案链和内嵌提案
2、按改变先后添加入微任务队列
Ⅳ、详情 => 提案章节
⑵、提案与生成器
Ⅰ、生成器和迭代器
冻结程序,直到解冻时再执行
其在任务队列中,由其解冻情况决定
若其在冻结后,在此次宏/微任务中解冻
会直接添加到该次宏/微任务的末尾
Ⅱ、async
1、此是生成器与提案的封装,原理不变
2、初次解冻直接执行,往后是在该提案的回调函数中解冻
3、 await 的右值,会使用Promise.resolve进行封装
Ⅲ、如例
console.log(\'首輪宏任務開始\');
Promise.resolve().then(() => console.log(\'最先進微任務隊列 => 開始微任務\'));
setTimeout(() => console.log(\'二輪宏任務開始!!\'), 0)
fnA();
console.log(\'首輪宏任務結束???\');
Promise.resolve().then(() => console.log(\'首輪微任務結束??\'));
async function fnA() {
console.log(\'fnA開始\');
await 1;
console.log(\'fnA解凍\');
await fnB();
console.log(\'fnA結束\');
}
function fnB() {
new Promise(x => console.log("fnB ~ promise",x()))
.then(() => console.log("fnB的提案鏈直接添加到该次微任务中并執行???"));
}
执行流程
1、async函数
首次正常解冻,
并且执行到第一个await右边冻结,
同时 其右值1 使用Promise.resolve进行封装,会在微任务中激活该async
后面的解冻将在提案回调函数中实现
2、进入微任务
①、按添加入微任务队列的先后顺序执行
②、async函数的提案回调函数在第二个
Ⅰ、执行fnB会生成一个fnB的提案回调函数
Ⅱ、fnB执行完毕时,async函数又会生成提案回调函数
Ⅲ、此两个提案回调函数按顺序添加到微任务队列中
③、执行在宏任务时添加的第三个提案回调
④、执行fnB的提案回调函数
⑤、执行async函数的提案回调函数
3、此例關於提案,涉及到内嵌提案回調,會大幅度提高代碼理解与调试難度
動畫的渲染
requestAnimationFrame 和 宏任务以及微任务是没有关系
渲染周期
60 fps:
浏览器会尝试每秒渲染60次页面,即:每秒60帧(60 fps) 的速度
渲染與帧
渲染的过程在每一帧发生之前
理想情况: 单个任务和该任务附属的所有微任务,都应在16ms内完成
window.requestAnimationFrame
簡述:
⑴、内容:希望执行一个动画,
并且要求浏览器在下次重绘之前调用指定的回调函数更新动画
参数:一个回调函数
该回调函数会在浏览器下一次重绘之前执行
⑵、requestAnimationFrame 和宏任务以及微任务是没有关系
與定時器動畫
渲染的过程在每一帧发生之前
⑴、执行完一个任务(JS)后,浏览器是不能保证会重新渲染的
可能执行了多次任务之后,浏览器才会渲染一次
⑵、setTimeout(fn, 0)
其回調函數执行了多次
而浏览器可能只会渲染一次
在浏览器在后台运行时,执行回調函數的動畫是看不見的
如從坐標0到100,再回到50,
在一個幀之内完成,只會顯示0到50的動畫,
而沒有0到100的動畫
或者setTimeout(fn, 0)的回調函數帧内不执行
下一个帧内执行两次
另外,在谷歌瀏覽器挂起當前頁面時,會帶來額外的錯誤
涉及到的數值的條件判斷盡量使用大小判斷,而不是相等判斷
⑶、requestAnimationFrame在每一帧渲染前执行其回調函數
⑷、强制浏览器重绘
getBoundingClientRect, clientWidth 等 API 强制浏览器重绘
事件处理
事件流
JS中的事件流:
-
三个阶段:捕获 => 目标 => 冒泡
-
自body出发,一路朝着并到达目标元素,然后原路返回到body
-
一路上,每个元素最多被触发一次
-
低IE的元素只能在后半程触发:自目标元素到body
-
其他:可自定义
-
-
可以阻断事件流的传递
注册回调函数
事件处理形式:事件动态绑定、事件静态绑定
事件静态绑定
直接在HTML元素上绑定函数 函数实参由绑定时传入函数括号内的变量 实参this:该HTML元素 可直接传入 event
动态事件绑定
on+eventType
监听函数的this是绑定该函数的元素 实参:event, 低版本IE 没有实参 on+eventType 只能绑定一个,后面的会覆盖前面的
事件监听
现代浏览器与低IE
-
低IE:IE9以下--不支持addEventListener
-
方法
其他:addEventListener/removeEventListener
低IE:(attachEvent/detachEvent)
-
执行顺序: 9个内是倒序 , 超过9个会乱
-
方法可被重复绑定
-
在所有浏览器中,如果用DOM0的方式来绑定,方法里面用return false也可以阻止默认行为的;这个是可以阻止DOM0的,如果是DOM2级的就不可以了
var event = (function () { var listener = function (ele, type, fn) { (listener = ele.addEventListener ? niceListener : IEListener )(ele, type, fn); }; var remove = function (ele, type, fn) { (remove = ele.removeEventListener ? niceRemove : IERemove )(ele, type, fn); }; return { listener: listener, remove: remove } })(); function IERemove(ele, type, fn) { var eventCbArr = ele["handleEvent" + type], i = 0, len = eventCbArr.length; if (eventCbArr && len) { for (; i < len; i++) { if (eventCbArr[i] == fn) { return eventCbArr.splice(i, 1); } } } } function IEListener(ele, type, fn) { eventPush(getEventList(ele, type), fn); return ele; } function eventPush(eventCbArr, fn) { for (var i = 0; i < eventCbArr.length; i++) { if (eventCbArr[i] == fn) { return; }; } eventCbArr.push(fn); } function getEventList(ele, type) { if (!ele["handleEvent" + type]) { ele.attachEvent("on" + type, function () { runCb(ele, eventHandle(e)); }); } return ele["handleEvent" + type]; } function runCb(elm, event) { var ary = elm["handleEvent" + event.type]; for (var i = 0; i <= ary.length;) { var cb = ary[i]; if (typeof cb === "function") { cb.call(elm, e); // 监听函数的this i++; } else { ary.splice(i, 1); } } } //不再額外考虑 IE 678 的兼容 function eventHandle(e) { e = window.event; e.target = e.srcElement; e.pageX = (document.documentElement.scrollLeft || document.body.scrollLeft) + e.clientX; e.pageY = (document.documentElement.scrollTop || document.body.scrollTop) + e.clientY; e.stopPropagation = function () { e.cancelBubble = true; } // 阻止事件进一步传播 e.preventDefault = function () { e.returnValue = false; } // 阻止事件的默认行为: return e; } function niceRemove(ele, type, fn) { ele.removeEventListener(type, fn); return ele; } function niceListener(ele, type, fn) { ele.addEventListener(type, fn, false); return ele; }