你这里为什么要加 timeout(0)?知识穿刺 - EventLoop(事件循环)从理论到实践

前言

今天同事看我代码问我,你这个地方的 timeout(0) 是干啥用的? 我说为了在下一个事件循环再执行该任务,以避免其他异步操作导致的这里取的变量还没有声明。

她又问:什么是事件循环?

我答:EventLoop

她又问:什么是 EventLoop

我答:这讲起来就不是一句两句说的清楚了,抽个时间专门做个培训吧。

于是便有了这篇文章。

什么是 EventLoop?

JavaScriptEventLoop 是一种用于处理异步操作的机制。

事件循环的基本思想是,JavaScript 运行时包含一个主线程,负责执行同步任务,同时维护一个任务队列( Task Queue )用于存放异步任务。异步任务完成后,会被放入任务队列中,等待主线程空闲时执行。

Node 也是单线程,但是由于 Node 是服务端语言,要处理更多的事情。因此在事件循环上有很大差异,不能一概而论。

为什么要有 EventLoop?

JavaScript 是一门单线程的语言,单线程意味着它不能同一事件处理多个任务。举个例子,我们发起了一个网络请求,不可能等着请求返回成功再接着执行其他代码。

不只是网络请求,再比如用户交互、网络请求、定时器等。为了有效地处理这些异步任务,JavaScript 才引入了事件循环的概念。

为什么 JavaScript 是单线程语言?

JavaScript 采用单线程有很多原因,但其中最重要是还是为了避免线程间的冲突,比如两个线程操作同一个 DOM 应该怎么办?

同时,多线程还有很多缺陷:

  • 竞态竞争、死锁
  • 复杂性增加
  • 共享状态问题
  • 复杂的 API 设计

与之对应的,多线程也有很多优点,像是 提高性能、响应性、并行计算 等优点。

但总的来说,在 JavaScript 设计之初,单线程还是更优的选择。

但随着发展的需要,JavaScript 还是在持续的通过不同的方法来处理单线程带来的一些问题。

虽然大多数后端语言也可以写 UI,且也是多线程的,且也有 竞态条件( Race Condition )、 死锁( Deadlock ) 等问题。但与 JavaScript 不同的是, JavaScript 本就诞生于浏览器,且初衷是做一个浏览器的脚本语言,单线程才是优先的解决方案。

Web Worker

JavaScript 在 2011 年引入了 Web Worker ,本质上还是为了解决 JavaScript 单线程带来的问题。 Web Worker 可以在后台运行 JavaScript 代码,可以使用多核处理器中的其他核心的算力,进而实现并行计算的功能。可以让一些计算密集任务后台运行、不阻塞主线程。

但在大多数情况下,这类操作是不必要的,前端的性能瓶颈主要还是在 DOM 以及 NetWork 侧的问题。

但它提供了一些可能性,我们可以将一些大型的计算放在前端,进而节省服务器算力,且可以一定程度上节省网络带宽资源。

微任务和宏任务

JavaScript 的异步任务分为两种,分别是 宏任务( macrotask ) 、 微任务( microtask ) ,不同的任务会压到不同的任务队列中,然后等待 EventLoop 依次从中取出需要执行的任务逐个执行。

微任务:

  • Promise callback(then、catch、all ...)
  • process.nextTick(node)
  • Object.observe 、 MutationObserver(是否已经废弃?

宏任务:

  • script tag
  • setTimeout
  • setInterval
  • setImmediate(node)
  • I/O(node)
  • UI render

关于 UI render ,其实并不是传统 JavaScript 中的代码执行,而是浏览器的操作。

那我为什么把他放在宏任务中呢,具体可以查看以下代码:

JS 复制代码
setTimeout(() => {
requestAnimationFrame(() => {
  requestAnimationFrame(() => {
    document.querySelector( 'p' ).textContent = "modified";
    const start = performance.now();
    (function lockUI() {
      if( performance.now() - start  < 5000 ) {
        return Promise.resolve().then( lockUI );
      }
    })();
  });
});
}, 0);

<p>Initial (please wait 5s)</p>

代码来源:stackoverflow.com/questions/6...

该段代码执行后表示,我们的可以通过微任务加锁,阻塞 UI 渲染 5s 的时间。

因此这里把 UI render 放在宏任务中,以提示我们执行顺序。

EventLoop 的执行流程

EventLoop 主要的执行流程:

  1. 执行主线程任务,生成任务队列,直到结束
  2. 从任务队列( task queue )中取出所有需要执行的微任务,执行直到结束
  3. 从任务队列( task queue )中取出第一个需要执行的宏任务,执行
  4. 从任务队列( task queue )中该宏任务执行产生的微任务,执行直到结束
  5. 重复直到结束

EventLoop 遵循先进先出的策略(队列 Queue

简单的执行流程可以查看下图:

实践

在我们的开发中,经常会遇到数据不更新的情况,通常情况下,我们可以根据实际的执行,来通过压入宏任务、微任务以更改执行顺序。

React

jsx 复制代码
const [cnt, setCnt] = useState(0)

const add = () => {
    setCnt(cnt + 1)
    console.log(cnt) // 0
    setTimeout(() => {console.log(cnt)}) // 1
    console.log(cnt) // 0
}

众所周知,ReactsetState ,为了实现批量更新,转换成了异步的。由于这个机制,导致我们常常在获取属性值的时候,获取到的是修改之前的值。

因此我们可以通过压入新的宏任务打印,或者通过 useEffect 监听更新实现打印新值,这就是 EventLoop 的应用之一。

Vue

JS 复制代码
this.message = 'Hello';

console.log(this.$el.textContent) // ''
this.$nextTick(() => {
  console.log(this.$el.textContent) // 'Hello'
});
console.log(this.$el.textContent) // ''

众所周知,Vue 的响应式,也采用了批量更新的方式,但它和 React 有所不同,在更新后马上打印,也是新值。那是不是我们就没有这个问题了?

不是的,我们如果在更新数据后,想要马上拿到 DOM 的值,会发现 DOM 的值依旧没有更新,因为它会在下一个 tick 执行更新,因此我们需要通过 $nextTick 来实现获取新值,这实际上也是 EventLoop 的一种体现。

Svelte

JS 复制代码
await import('aframe');

await onAframeImported();
await timeout(0);
isImported = true;
await onAframeRendered();

这里我们在导入 aframe 执行了一个 callback,这个 callback 中注册了一些 aframe component ,这时候我们如果立即 isImported = true , 就会出现一种情况,部分基元在使用 aframe component 时,报错该组件未注册。

这实际上就是注册时,aframe 内部的部分任务是压入宏任务执行的,进而导致未注册成功时,aframe component 被引用。

因此,我们通过再压入一个靠后的宏任务 await timeout(0) ,就可以实现在它内部代码注册完成之后,再执行我们的其他代码。

提问1:这里的 await timeout(0) 转换为非 await 的原生 setTimeout 代码,应该是什么样?

提问2:这里我如果要放弃写 async、await ,应该是什么样的?
解析

提问1转换:

JS 复制代码
await import('aframe');

await onAframeImported();
setTimeout(async () => {
    isImported = true;
    await onAframeRendered();
}, 0);

提问2转换:

JS 复制代码
import('aframe')
.then(() => {
    return onAframeImported();
})
.then(() => {
    return timeout(0);
})
.then(() => {
    isImported = true;
    return onAframeRendered();
})
.catch(error => {
    console.error('Error:', error);
});

原生 JS

原生 JS 时间原因我就不举正常的例子了,这里简单讲讲,如果我们开发一个前端框架,我们希望框架在一定时间检测数据与 DOM 的区别,进而更新 DOM,同样我们需要通过一个 tick 来实现非阻塞更新,类似 react 的 fiber,实际上 react fiber 也考虑过 settimeout 实现,但由于可能会和浏览器渲染冲突,最终采用了 requestMessageCallback、requestAnimationFrame 等方式实现。

刻意练习

打印顺序题1:

JS 复制代码
setTimeout(function () {
  console.log(1);
});
new Promise(function (resolve, reject) {
  console.log(2);
  resolve(3);
}).then(function (val) {
  console.log(val);
});
console.log(4);

解析

答案:2431

原因:

  1. 主线程执行,从上至下,分别打印: 2 4
  2. 查微任务队列,打印:3
  3. 查宏任务队列,打印:1

打印顺序题2:

JS 复制代码
console.log('starting');
setTimeout(()=>{
    console.log('settimeout');
    setTimeout(()=>{
        console.log('inner');
    })
    console.log('end');
},1000)
new Promise((resolve,reject)=>{
    console.log('promise');
    resolve()
})
.then(()=>{
    console.log('then');
})
.then(()=>{
    console.log('then2');
})
.then(()=>{
    console.log('then3');
})

解析

答案:

  • starting
  • promise
  • then
  • then2
  • then3
  • settimeout
  • end
  • inner

原因:

  1. 执行主线程:starting、promise
  2. 执行主线程的微任务:then、then2、then3
  3. 取出宏任务执行,并压入新的宏任务:settimeout、end
  4. 取出宏任务执行:inner

打印顺序题3:

JS 复制代码
console.log('1');

setTimeout(() => {
  console.log('2');

  Promise.resolve().then(() => {
    console.log('3');

    Promise.resolve().then(() => console.log('4')).then(() => console.log('4.1'));
    Promise.resolve().then(() => console.log('4.2'));

    setTimeout(() => {
      console.log('5');
      Promise.resolve().then(() => console.log('6'));
    });
  });

  Promise.resolve().then(() => console.log('7'));
}, 0);

setTimeout(() => {
  console.log('8');

  Promise.resolve().then(() => console.log('9'));
}, 0);

console.log('10');

解析

答案:

  • 1
  • 10
  • 2
  • 3
  • 7
  • 4
  • 4.2
  • 4.1
  • 8
  • 9
  • 5
  • 6

原因:

  1. 执行主线程:1、10
  2. 取出宏任务执行,并压入两个微任务:2
  3. 取出两个微任务执行,并压入后生成的两个微任务、一个宏任务:3、7
  4. 取出新压入的微任务执行,并压入一个微任务:4、4.2
  5. 取出最后一个微任务执行:4.1
  6. 没有微任务了,取下一个宏任务执行,并压入一个微任务:8
  7. 执行微任务:9
  8. 取出下一个宏任务执行,并压入一个微任务:5
  9. 取出微任务执行:6
  10. 没有新任务了,结束

打印顺序题4(课后作业):

javascript 复制代码
console.log('Start');

setTimeout(() => {
  console.log('1. First macro task');

  Promise.resolve().then(() => {
    console.log('2. First microtask in the first macro task');

    for (let i = 1; i <= 3; i++) {
      setTimeout(() => {
        console.log(`3. Subtask ${i} in the first macro task's first microtask`);
      }, 0);

      Promise.resolve().then(() => {
        console.log(`4. Microtask in subtask ${i} in the first macro task's first microtask`);
      });
    }

    Promise.resolve().then(() => {
      for (let i = 4; i <= 6; i++) {
        console.log(`5. Subtask ${i} in the first macro task's first microtask`);
      }
    });
  });

  setTimeout(() => {
    console.log('6. Second macro task in the first macro task');

    for (let i = 7; i <= 9; i++) {
      console.log(`7. Subtask ${i} in the second macro task in the first macro task`);
    }

    Promise.resolve().then(() => {
      for (let i = 10; i <= 12; i++) {
        setTimeout(() => {
          console.log(`8. Subtask ${i} in the second macro task's first microtask in the first macro task`);
        }, 0);

        Promise.resolve().then(() => {
          console.log(`9. Microtask in subtask ${i} in the second macro task's first microtask in the first macro task`);
        });
      }
    });
  }, 0);
}, 0);

console.log('End');
相关推荐
supermapsupport36 分钟前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
m0_748254881 小时前
vue+elementui实现下拉表格多选+搜索+分页+回显+全选2.0
前端·vue.js·elementui
ThisIsClark1 小时前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
噢,我明白了2 小时前
同源策略:为什么XMLHttpRequest不能跨域请求资源?
javascript·跨域
sanguine__2 小时前
APIs-day2
javascript·css·css3
苹果醋32 小时前
Golang的文件加密工具
运维·vue.js·spring boot·nginx·课程设计
关你西红柿子3 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
测试19983 小时前
外包干了2年,技术退步明显....
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
济南小草根3 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js
小木_.3 小时前
【python 逆向分析某有道翻译】分析有道翻译公开的密文内容,webpack类型,全程扣代码,最后实现接口调用翻译,仅供学习参考
javascript·python·学习·webpack·分享·逆向分析