13-事件循环

1、事件循环原理

事件队列(至少两个队列)

  • 宏任务队列

    • 宏任务:一个个离散的、独立的单元

      • 创建主文档对象
      • 解析html
      • 执行全局js
      • 更改当前url
      • 各种事件
  • 微任务队列

    • 相比宏任务更小的任务,更新应用程序的状态,在浏览器执行其他任务之前执行(重新渲染ui),避免不必要的ui重绘

      • promise
      • 回调函数
      • dom变化

事件循环的实现至少有一个宏任务队列和一个微任务队列

两个基本原则

  • 一次只处理一个任务
  • 一个任务开始后直到运行完成,不会被其他任务打断

工作原理

  • 一次迭代中先检查宏任务队列,有任务就执行,直到该任务执行完成(或队列为空)。
    事件循环移动到微任务队列,如果有任务在等待,依次执行,直到执行完全部的微任务。
    当微任务队列处理完成并清空时,事件循环会检查是否需要更新ui渲染。如果是,更新ui视图。
    至此,当前事件循环结束,之后回到第一个环节,开启新一轮事件循环。

  • 注意宏任务微任务差别:

    • 单次迭代中,宏任务最多只处理一个。而任务中所有微任务队列都会被处理。

细节

  • 1.两类任务队列都是独立与事件循环的,意味着任务队列的添加也发生在事件循环之外,如果不这样,导致在执行js代码时,发生的任何事件都将被忽略?
  • 2.所有微任务会在下一次渲染之前完成,因为他们的目标是在渲染前更新应用程序状态。
  • 3.浏览器通常每秒钟渲染60次页面(帧)。
    60fps/s
    1s=1000ms
    60f/s => 60f/1000ms => 16.6f/ms
    也就是说,理想情况下,单个任务和该任务附属的所有微任务,都应在16ms内完成。

浏览器渲染机制

  • 在浏览器完成界面渲染,
    进入下一轮事件循环后,
    可能发生的3种情况

    • a:在另一个16ms结束前,事件循环执行到“是否进行渲染”环节。因为更新ui是一个复杂操作,所以如果没有显式的指定需要页面渲染,浏览器可能不会选择在当前的循环中执行ui渲染操作。
    • b:在最后一次渲染完成后大约16ms,事件循环执行到“是否进行渲染”环节。在这种情况下,浏览器会进行ui更新,以便用户能感受到顺畅的应用体验;
    • c:执行下一个任务(和相关的所有微任务)超过16ms。在这种情况下,浏览器将无法更新页面。如果代码执行在几百毫秒以内,这样的延时可能察觉不到,尤其当页面中没有过多操作时。反而如果耗时过多,或者页面上有动画时,用户会感觉到卡顿。极端情况下,任务执行超过几秒,用户浏览器将会提示”无响应脚本“提示。

2、代码实例

1、宏任务实例(没有微任务,处理完宏任务就可以进行渲染)

<button id="button1"></button>
<button id="button2"></button>
button1.addEventListener('click',function(){...//8ms})
button2.addEventListener('click',function(){...//5ms})

- 执行主线程代码需要15ms
- 第一个单击事件处理器需要运行8ms
- 第一个单击事件处理器需要运行5ms

假设用户手快5ms内点击第一个按钮,12ms点击第二个按钮
1、宏任务队列处理第一个任务:运行js代码,注册两个事件处理器
2、第5、12ms顺序点击两个按钮的操作:添加宏任务处理单击事件1和2,因为当前任务正在进行中,所以不会立即处理点击事件,但是因为添加事件监测和事件添加是独立的,所以不会影响事件添加到宏任务中。
3、15ms后,js运行完毕,渲染操作;执行第一个事件处理器:执行8ms,渲染;执行第二个事件处理器5ms,渲染;

2、宏任务和微任务实例

<button id="button1"></button>
<button id="button2"></button>
button1.addEventListener('click',function(){
    Promise.resolve().then(()=>{
        //do something 4ms
    })
//8ms})
button2.addEventListener('click',function(){
     Promise.resolve().then(()=>{
        //do something 5ms
    })
//5ms})

- 执行主线程代码需要15ms
- 第一个单击事件处理器需要运行8ms
- 第一个单击事件处理器需要运行5ms

假设用户手快5ms内点击第一个按钮,12ms点击第二个按钮
1、宏任务队列处理第一个任务:运行js代码,注册两个事件处理器
2、第5、12ms顺序点击两个按钮的操作:添加宏任务处理单击事件1和2,因为当前任务正在进行中,所以不会立即处理点击事件,但是因为添加事件监测和事件添加是独立的,所以不会影响事件添加到宏任务中。
3、15ms后,js运行完毕,渲染操作;
4、执行第一个事件处理器:执行8ms, ,该宏任务执行完毕后,转到微任务队列中,将微任务全部执行完毕后,本例是4ms,进行是否需要渲染操作;
5、27ms,执行第二个事件处理器5ms,同样产生回调置于微 任务中,宏任务处理完毕,微任务队列处理完毕,进行渲染操作;
注意:在微任务执行之前,不允许重新渲染页面(在处理promise之前)。当微任务队列没有等待当任务时,才可以重新渲染页面。 

3、计时器

1、概念

// 计时器是挂载在window对象的方法,与事件类型不同,
// 这些方法不是js本身定义的,而是宿主环境提供的(浏览器或nodejs)
// 计时器作用是将长时间运行的任务分解为不阻塞事件循环的小任务,
// 以阻止浏览器渲染,浏览器渲染过程会讲程序运行缓慢没有反应。

// 延时计时器 
// 设置延时计时器
setTimeout: id = setTimeout(fn,delayTime)
// 当延时计时器未触发时,取消计时器
clearTimeout: clearTimeout(id) 

// 周期计时器
// 设置周期计时器
setInterval: id = setInterval(fn,delayTime) 
// 取消周期计时器
clearInterval: clearInterval(id) 

// 注意:delayTime指在指定时间后计时器回调被加入到宏队列等待执行的时间。实际执行时间不确定

2、在事件循环中执行计时器

<!-- 例子 -->
<button id="button"></button>
<script type="text/javascript">
    
    setTimeout(funtcion timeoutHandle(){
        // do something... needs 6ms
    },10)

    setInterval(funtcion ingtervalHandle(){
        // do something... needs 8ms
    },10)
    
    const button=document.getElementById('button')
    button.addEventListener('click',funtcion(){
        // do something... needs 10ms
    })

    // code that runs for 18ms
    //假设手快的用户在6ms点击按钮
    
    // 宏队列事件
    // 1 执行js主代码 耗时18ms
    // 2 6ms 按钮单击处理事件被添加
    // 3 10ms 处理延时计时器、周期处理器被添加
    
    // 时间
    // -----6ms-----10ms-----18ms-----20ms-----28s-----34ms-----40ms--。。。
    // 执行js主代码时,第6ms单击事件被添加
    // 10ms,延时、周期处理器被添加
    // 18ms,主代码js执行完毕,开始执行微任务,假设当前没有微任务;渲染;执行下一宏任务
    // 20ms,执行单击事件处理,此时周期计时器又被触发,但宏任务队列已经有一个周期计时器,浏览器不会创建两个相同的周期计时器;
    // 28ms,单击处理事件执行完毕;开始执行延时计时器;18+10
    // 30ms,周期计时器又被触发,同样不被添加。
    // 34ms,延时计时器执行完毕;开始周期计时器执行 28+6
    // 40ms,周期计时器正在执行,不是在等待,一个新的周期计时器被添加到队列中;
    // 42ms,周期计时器执行完毕;注意执行空出来了;34+8
    // 50ms,添加周期计时器,并执行,58ms执行完毕;
    // 60ms,添加周期计时器,并执行,68ms执行完毕;
    // 70ms,添加周期计时器,并执行,78ms执行完毕;
    // 总结,在10-70ms里,计时器被添加7次,但是实际执行只有5次被执行;
    
    
    // 延时执行和间隔执行的区别:
    // 没搞明白

    // 处理计算复杂度高的任务
    // 比如创建一个20000行,每行6列的表格,这个操作是惊人的,会导致浏览器挂起一段时间,导致用户无法正操操作。
    const tbody = document.querySelector("tbody");//←---查找tbody元素,我们将在其中创建大量的行数
    for (let i = 0; i < 100000; i++) { //---- 创建10000行,浏览器空白10s
        const tr = document.createElement("tr") ; //←---创建---行
        for (let t = 0; t < 6; t++) { //---- 为每一 .行创建6个单元格,每一个单元格有一个文本节点
            const td = document.createElement("td");
            td.appendChild ( document.createTextNode(i + "," + t));
            tr.appendChild(td);
        }
        tbody.appendChild(tr);//←---将新创建的行添加到父元素中
    }

    // 使用计时器:将一次性导致浏览器空白的复杂度高的任务分段进行,因为是计时器宏任务,每段执行完毕后可渲染页面;不会导致浏览器挂起,使性能得到优化
    const rowCount = 100000; //←— 初始化数据
    const divideInto = 4;
    const chunkSize = rowCount / divideInto;
    let iteration = 0;
    const table = document.getElementsByTagName("tbody")[0];
    setTimeout(function generateRows() {
        const base = chunkSize * iteration; // 计算上一次离开的地方
        for (let i = 0; i < chunkSize; i++) {
            const tr = document.createElement("tr");
            for(let t=0;t<6;t++){
                const td = document.createElement("td"); 
                td.appendChild(document.createTextNode((i + base) + "," +t + "," + iteration));
                tr.appendChild(td);
            }
            // console.log(table)
            table.appendChild(tr);
        }
        iteration++;
        if (iteration < divideInto) //—— 安排下一个阶段
            setTimeout(generateRows, 0);
    }, 0); // ←—将超时延迟设置为0来表示下一次迭代应该“尽快”执行,但仍然必须在U I更新之后执行
</script>

3、处理事件

1、事件执行顺序

    button.addEventListener('click',function(event){
        event.target //指向发生事件的元素
        this //指向事件处理器注册的元素
        // 一般来说 注册的元素就是事件发生的元素,但总有例外
    })
  <body id='body'>         <!-- 最外层盒子 -->
    <div id="out1">      <!-- 外层盒子 -->
        <div id="out2"/> <!-- 里层盒子 -->
    </div>
  </body>
    
  <script type="text/javascript">
    body.addEventListener('click',function(event){},false) //1
    out1.addEventListener('click',function(event){}) //2
    out2.addEventListener('click',function(event){}) //3

  // 3个元素注册了3个事件处理器,假设用户点击了里面盒子,因为层级嵌套,显然会触发3个事件处理器,输出3条信息,但3个处理器的执行顺序是怎么样的呢?   
  // 两大浏览器厂商,Netscape与Microsoft 
  // 在Netscape事件模型中,事件处理器从顶部元素开始,直到事件目标元素。--捕获
  // 在Microsoft中则相反,从目标元素开始,按Dom树向上冒泡。
  // W3C委员会设立标准,同时包含两种方式,一个事件处理有两种方式:
  // 1、捕获-首先被顶部元素捕获,并依次向下传递(先找true,捕获)
  // 2、冒泡-目标元素捕获之后,事件处理转向冒泡,从目标元素向上冒泡(false,冒泡,默认)
  // 先上往下捕获,再下往上冒泡

  // example
  body.addEventListener('click',function(event){}) 
  out1.addEventListener('click',function(event){},true) 
  out2.addEventListener('click',function(event){},false) 
  //事件从捕获开始上往下,找到out1,在找到目标out2,转向冒泡找到body。

  body.addEventListener('click',function(event){},true) 
  out1.addEventListener('click',function(event){},false) 
  out2.addEventListener('click',function(event){},true) 
  //事件从捕获开始找body,在找到目标out2,最后找到out1

  // 点击out2,在out2事件处理器里,this是out2,event.target是out2;
  // 在out1处理器里,this是out1,event.target是out1。
  // 原因是,this是当前处理器注册的元素;event.target是点击的元素。
  // 在嵌套盒子中,单击内部元素,event.target可以检测到实际点击的内部元素,为什么大盒子嵌套小盒子,大盒子没被点击到呢,因为盒子嵌套相当于透明图层的嵌套,内部盒子是在最上层,最前面一层,个人理解;
  </script>

2、在祖先元素上代理事件

需求:指出用户在表格中单击的是哪个单元格,并置为红色

  • 遍历所有单元格,分别建立处理器,单击时修改颜色,可以解决,但是优雅吗?no
  • 优雅的处理方式是,建立唯一的处理器,注册到比单元格更高级的元素上,通过冒泡处理所有的单元格单击时间,单元格是表格的后代元素,通过event.target可以获得被单击的元素,将事件处理器代理到表格上要优雅的多。
<table id="table"  style="width:600px" border="1">
    <caption><b>我的个人传记</b></caption>
    <tr>
        <!-- td th 区别???????? -->
        <td>书名</td>
        <td>作者</td>
    </tr>       
    <tr>
        <td>好人好事10000000万篇</td>
        <td>禽兽</td>
    </tr>       
    <tr>
        <td>学雷锋</td>
        <td>禽兽</td>
    </tr>
</table>
<script type="text/javascript">

    const table=document.getElementById('table');
    
    table.addEventListener('click',function handler2(event){
        
        if(event.target.tagName.toLowerCase()==='td'){
            
            event.target.style.backgroundColor="yellow"
        }
    })
</script>

3、自定义事件的使用与触发

自定义事件是模拟真实的事件,为了共享代码,创建松耦合的函数。
先定义 再注册 最后触发 也不一定。。。

    // 创建方式1: CustomEvent 构造器
    var myEvent = new CustomEvent('eventName', {
        bubbles: 'true', // 是否冒泡
        cancelable: 'true', //是否可以取消默认行为
        detail: { title: 'This is title!'},
        // 将需要传递的数据写在detail中,以便在EventListener中获取
        // 数据将会在event.detail中得到
        
    });
    
    // 事件的监听 创建了事件,就需要对应的事件监听器
    document.addEventListener('eventName', function(event){
        console.log('数据为:', event.detail); //CustomEvent创建事件,用event.detail取数据
    });
    
    // 自定义的事件不是内置事件有操作去触发,需要显式触发
    if(window.dispatchEvent) {  
        window.dispatchEvent(myEvent);
    } else {
        window.fireEvent(myEvent); // fireEvent函数兼容IE8低版本
    }
    // 需要特别注意的是,当一个事件触发的时候,如果相应的element及其上级元素没有对应的EventListener,就不会有任何回调操作。 

    // 创建方式2:
    // CustomEnent的另种写法
    // 首先创建自定义事件对象
    let event = document.createEvent('CustomEvent');
    //初始化事件对象
    event.initCustomEvent(in String type,in boolean canBuble,in boolean cancelable,in any detail);
    /*
        param1:要处理的事件名;
        param2:事件是否冒泡
        param3:是否可以取消默认行为
        param4:细节参数
        例如:event.initCustom('test',true,true,{a:1,b:2});
    */
    window.addEventListener('event_name', function(event){
        console.log('得到数据为:', event.detail);
    });
    // 随后在对应的元素上触发该事件
    if(window.dispatchEvent) {
        window.dispatchEvent(myEvent);
    } else {
        window.fireEvent(myEvent);
    }
    // 书中例子思路
    // 封装自定义事件的创建和分发为一个函数,可以调用函数即可创建和触发。
    // 调用函数模拟ajax的请求和结束,开始和结束时分别调用创建新的自定义事件
    // 监听开始和结束事件,创建业务代码,如风车转动和结束。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容