js这绕绕的事件机制

众所周知,js是单线程,负责页面绘制和用户事件等,即我们通常理解的UI线程,对于页面的绘制,只能在一个线程上更新,这是共识,不然同时有多个线程更新UI,这是不可理解的。虽然android的UI线程也是如此,但是android是支持多线程的,而js就只有单线程,这意味着我们同一时间只能做一件事情,虽说有异步任务,但那也只是把执行时间延后罢了。

当然后面为了适应时代发展,js通过web worker支持了多线程能力,这有一个例子

下面是一些概念:

同步任务

JS 主线程里面立即被推入执行栈且可以被执行的函数。在主线程上排队执行的任务,只有前一个执行完毕,才能执行后一个,代码是阻塞的,顺序执行。要注意的是,click,dispatchEvent等人工合成事件是同步任务,同步调用事件处理程序。可以参考以下链接:

// 下面输出为
// on click
// end

const App = () => {
  return (
    <div  ref={(ref) => {
      if(ref) {
        ref.click()
        console.log('end')
      }
    }} onClick={() =>{
      console.log('on click')
    }}>
    </div>
  );
}
复制代码

异步任务

如果一个函数在调用之后 不能马上得到预期结果 那么就是异步任务,任务是非阻塞的。

每次执行异步任务,就会将任务放进对应的任务队列。

  • setTimeout setInterval
  • promise
  • dom事件
  • 网络请求

事件循环

js的主线程通过等待任务队列,执行任务源源不断地处理用户事件和页面绘制,每次事件循环称为一次tick,包括从任务队列取出任务执行,清空微任务队列,页面重绘(不一定执行)。

宏任务(浏览器发起的)

执行宏任务进入宏任务队列

  • I/O
  • dom事件
  • setTimeout setInterval(web api) 浏览器有个定时器模块,定时器到了执行时间才会把异步任务放到异步队列,setTimeOut setInterval的延时就是指多少时间后回调函数会放入任务队列

微任务(js引擎发起的)

执行微任务会进入当前任务的微任务队列,在下一个事件到来之前会被清空执行。微任务的好处就是优先级高, 但是如果反复执行微任务,会造成下一个事件的处理延后。

  • Promise.then .catch
  • MutationObserver
  • queueMicrotask 把函数当成微任务入队

requestAnimationFrame

requestAnimationFrame也属于异步任务,但是它比较特殊,既不属于宏也不属于微,它是在event下次重绘之前调用,也就是晚于微任务,早于下一次事件。具体可以看这个。但是每个eventloop不一定会进行重绘,所以在不同浏览器中,requestAnimationFrame的执行时机不太一样,这是重绘时机的规范

任务队列

有一个演示效果的demo latentflip.com/loupe

pic2.zhimg.com/v2-edca0b28…

(图片来源zhuanlan.zhihu.com/p/105903652

例子

代码在这

模拟事件机制


  console.log("--- task start ---");

  setTimeout(() => {
    console.log("macro task 1");
  }, 0);

  console.log("wait countdown");

  new Promise((resolved, reject) => {
    let time = Date.now();
    console.log("countdown 500ms start");
    while (Date.now() - time < 500) {}
    console.log("countdown end");
    resolved('success')
  })
  .then((e) => {
      console.log("micro task 1");
  })

  console.log("wait end");

  queueMicrotask(() => {
    console.log("micro task 2");
  });

  queueMicrotask(() => {
    console.log("micro task 3");
    console.log("---micro task end---");
    console.log("---macro task start---");
  });

  setTimeout(() => {
    console.log("macro task 2");
    console.log("---macro task end---");
  }, 0);

  console.log("---task end---");
  console.log("---micro task start---");
复制代码
输出结果如下:

--- task start --- 
wait countdown 
countdown 500ms start 
countdown end 
wait end 
---task end--- 
---micro task start--- 
micro task 1 
micro task 2 
micro task 3 
---micro task end--- 
---macro task start--- 
macro task 1 
macro task 2 
---macro task end---

// 可以看出promise.then是微任务 微任务执行顺序和入队顺序一致
// promise里是同步任务 进入执行栈执行
// setTimeout是宏任务  执行顺序和入队顺序一致 晚于微任务执行
复制代码

模拟dom事件

import "./styles.css";

const onClick = (type:string) => {
   console.log(`${type} on click`)
   Promise.resolve().then(e => {
     console.log(`${type} micro task`)
   })
   setTimeout(() => {
    console.log(`${type} macro task`)
   }, 0);
}

export default function App() {

  return (
    <div className="App">
      <h2>查看点击事件的过程</h2>
      <div className="Parent" onClick={()=>{
          onClick('parent')
        }}>
        <div className="Child" ref={(ref) => {
           if(ref) {
             console.log('---auto click---')
             ref.click()
           }
        }}onClick={()=>{
          onClick('child')
        }}>点我</div>
      </div> 
    </div>
  );
}
复制代码
页面加载成功结果如下:
---auto click--- 
child on click 
parent on click 
child micro task 
parent micro task 
child macro task 
parent macro task 
// 主动进行事件的合成和分发,这时候两个onClick是作为同步事件进入执行栈,
// 两个onClick在同一个事件中,所以会先输出两个on click

点击按钮之后输出结果如下:
child on click 
parent on click 
child micro task 
parent micro task 
child macro task 
parent macro task

// 而手动点击则不一样,这时候两个onClick都是作为异步任务进入宏队列
// 两个onClick不在同一个事件中,所以第一个onClick的promise会先于第二个onClick执行
复制代码

总结

在每次事件循环中,从宏任务队列取出任务t执行,然后把任务t里的同步任务按顺序执行, 异步任务则进入任务队列,如果是宏任务,则进入宏任务队列,如果是微任务,则进入当前微任务队列。接着,等执行栈清空之后,会执行当前微任务队列的所有任务,接着浏览器根据是否重绘页面调用requestAnimationFrame。

参考

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享