nwamtf

浏览器事件及任务队列

 

任务队列

线程

一、进程与线程

  1. 一个页面的进程主要有:

Browser进程、GPU进程、Renderer进程--浏览器内核

  1. 浏览器内核

主作用:渲染页面,执行脚本,处理事件

組成:由多个线程同時执行,相互协助

①、主线程:JS引擎

②、辅助线程:其他

二、主要线程:

  1. JS引擎线程

  2. GUI渲染线程

  3. 事件循环

  4. 異步线程

任务队列

  1. 作用:存储需要执行的回调函数

  2. 种类:宏任务与微任务

相互协助

一、主线程:JS引擎线程

  1. 执行同步代码

  2. 遇到異步程序,交给異步线程

二、辅助线程:

Ⅰ、GUI渲染线程

  1. 负责渲染:

    • 解析HTML、CSS

    • 构建DOM树和RenderObject树

    • 布局和绘制

  2. 与主线程

    • GUI引擎在渲染时会阻塞js引擎计算

    • 同一時刻只能執行主线程或者GUI线程

Ⅱ、異步线程:

  1. 种类:很多

    • 定时器触发线程

    • http 异步线程

    • 浏览器事件线程 等等

  2. 执行内容來源:

    • 來自主线程的異步程序

    • 鼠标点击、AJAX异步请求等等事件

  3. 异步代码在其线程中执行

  4. 执行完成后:

    如果该事件有绑定回调函数,按先后完成顺序提交到任务队列中

三、事件循环线程

  1. 主任务:

    在主线程执行完全部代码时--堆栈为空

    从任务队列中获取任务提交给主线程执行

四、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中的事件流:

  1. 三个阶段:捕获 => 目标 => 冒泡

  2. 自body出发,一路朝着并到达目标元素,然后原路返回到body

  3. 一路上,每个元素最多被触发一次

    • 低IE的元素只能在后半程触发:自目标元素到body

    • 其他:可自定义

  4. 可以阻断事件流的传递

注册回调函数

事件处理形式:事件动态绑定、事件静态绑定

事件静态绑定

直接在HTML元素上绑定函数 函数实参由绑定时传入函数括号内的变量 ​ 实参this:该HTML元素 可直接传入 event

动态事件绑定

on+eventType

监听函数的this是绑定该函数的元素 实参:event, 低版本IE 没有实参 on+eventType 只能绑定一个,后面的会覆盖前面的

事件监听

现代浏览器与低IE

  1. 低IE:IE9以下--不支持addEventListener

  2. 方法

    其他:addEventListener/removeEventListener

    低IE:(attachEvent/detachEvent)

  3. 执行顺序: 9个内是倒序 , 超过9个会乱

  4. 方法可被重复绑定

  5. 在所有浏览器中,如果用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;
}

 

分类:

技术点:

相关文章: