Bootstrap

HarmonyOS实战应用开发-JS 事件循环 EventLoop

JS 单线程、事件驱动UI

JS是 单线程、非阻塞 的脚本语言,但JS运行时底层的C++ API是多线程的。对于浏览器是web API,对于nodejs是libuv库

●单线程:解析执行JS 代码的线程只有一个,即所谓的主线程,同一时间只能做一件事。和其他语言中的主线程类似,保证UI刷新不被多线程扰乱
●非阻塞:在JS调用异步API时不会等待异步执行完毕再往下执行,而是直接往下执行

js运行时至少有两个线程:主线程,工作线程。主线程用于解释执行js代码,工作线程用于循环的从消息队列获取消息并执行。

虽然JS可以使用WebWorker创建多个子线程,但创建的子线程完全受主线程控制,不能操作UI,实际上JS依然是单线程,只不过可以创建其他线程处理非UI事件

JS是基于事件驱动的,而不是定时检测数据变化或状态变化再去刷新UI,当触发事件时会通过回调驱动UI刷新

同步任务、异步任务

JS的任务分为 同步 和异步 两种

1.同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
2.异步任务:不进入主线程,而是放在任务队列中,若有多个异步任务则需要在任务队列中排队等待,任务队列类似于缓冲区,任务下一步会被移到执行栈然后主线程执行调用栈的任务

异步执行的运行机制如下。(同步执行也是如此,可以把同步视为没有异步任务的异步执行)

1.所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

2.主线程之外,存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

3.一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务就结束等待状态,进入执行栈开始执行。

4.主线程不断重复上面的第三步

栈是一种先进后出的数据结构,普遍的应用于计算机中。比如应用的栈内存分配、函数调用的栈帧分配、运算符的匹配等

执行栈

JS方法执行的过程中,每调用一个函数会生成一个函数帧放入栈顶,方法执行完毕从栈顶移除。一层层的函数调用就组成了执行栈,执行栈就是不断地把函数帧进行入栈和出栈

对象被分配在一个堆中,堆用于表示一个内存中大的未被组织的区域
队列
一个JS运行时包含了一个待处理的消息队列,消息队列中的每一个消息都与一个回调函数相关联。
当栈为空时,从队列中取出一个消息进行处理。这个处理过程包含了调用与这个消息相关联的函数(以及因而创建了一个初始堆栈帧)。当栈再次为空的时候,也就意味着消息处理结束。

事件队列

1.异步代码的执行,遇到异步事件不会等待它返回结果,而是将这个事件挂起,继续执行执行栈中的其他任务。
2.当异步事件返回结果,将它放到事件队列中,被放入事件队列不会立刻执行起回调,而是等待当前执行栈中所有任务都执行完毕
3.主线程空闲状态,主线程会去查找事件队列中是否有任务,如果有,则取出排在第一位的事件,并把这个事件对应的回调放到执行栈中,然后执行其中的同步代码
在这里插入图片描述

任务队列中的都是已经完成的异步操作,而不是说注册一个异步任务就会被放在这个任务队列中

宏任务、微任务

1.异步任务分为 宏任务(macrotask) 与 微任务 (microtask)

●宏任务特征:有明确的异步任务需要执行和回调;需要其他异步线程支持。

●微任务特征:没有明确的异步任务需要执行,只有回调;不需要其他异步线程支持

2.不同的API注册的任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行

3.针对不同的事件有不同的任务队列,同类型的任务必须在同一个队列。比如io队列,定时器队列等等

宏任务

在这里插入图片描述

微任务

在这里插入图片描述

注意

1.Promise本身是同步的立即执行函数,.then是异步执行函数
2.async/await本质上是基于Promise的语法糖,而Promise是属于微任务的一种。所以在使用await关键字与Promise.then效果类似

setTimeout(_ => console.log(4))

async function main() {
  console.log(1)
  await Promise.resolve()
  console.log(3)
}

main()

console.log(2)
// 输出 1 2 3 4

async函数:在await之前的代码都是同步执行的,可以理解为await之前的代码属于new Promise时传入的代码,await之后的所有代码都是在Promise.then中的回调

事件循环

JS 引擎本身不实现事件循环机制,这是由它的宿主实现的,浏览器中的事件循环主要是由浏览器来实现,而在 NodeJS 中也有自己的事件循环实现

事件循环负责 执行代码、收集和处理事件以及执行队列中的子任务, 最简单的事件循环模型如下
在这里插入图片描述

queue.waitForMessage() 会同步地等待消息到达 (如果当前没有任何消息等待被处理)

执行至完成

1.每一个消息完整地执行后,其他消息才会被执行
2.当一个函数执行时,它不会被抢占,只有在它运行完毕之后才会去运行任何其他的代码,才能修改这个函数操作的数据

settimeout 添加消息

1.函数 setTimeout 接受两个参数:待加入队列的消息和一个时间值(可选,默认为 0)。这个时间值代表了消息被实际加入到队列的最小延迟时间。如果队列中没有其他消息并且栈为空,在这段延迟时间过去之后,消息会被马上处理。但是,如果有其他消息,setTimeout 消息必须等待其他消息处理完。因此第二个参数仅仅表示最少延迟时间,而非确切的等待时间

2.零延迟并不意味着回调会立即执行。以 0 为第二参数调用 setTimeout 并不表示在 0 毫秒后就立即调用回调函数。其等待的时间取决于队列里待处理的消息数量

永不阻塞

JavaScript 的事件循环模型与许多其他语言不同的一个非常有趣的特性是,它永不阻塞。处理 I/O 通常通过事件和回调来执行

运行机制

总方针:先同步再异步,异步中先微任务再宏任务

事件循环的过程中,执行栈在同步代码执行完成后,优先检查微任务队列是否有任务需要执行,如果没有,再去宏任务队列检查是否有任务执行,如此往复。

微任务一般在当前循环就会优先执行,而宏任务会等到下一次循环,因此,微任务一般比宏任务先执行,并且微任务队列只有一个,宏任务队列可能有多个

根据WHATWG规范,在事件循环的一个周期中,应该从宏任务队列中处理正好一个(宏)任务。 在这个宏任务完成之后,所有可用的微任务将在相同的周期内被处理。当这些微任务正在被处理时,它们可以入队更多的微任务,这些微任务将一个接一个地运行,直到微任务队列耗尽
在这里插入图片描述

浏览器下的事件循环

浏览器中的堆、栈、消息队列
在这里插入图片描述

执行过程

1.JS 在解析执行一段代码时,会将同步代码按顺序排在执行栈中
2.然后依次执行里面的函数,每次执行一个方法会为这个方法生成独有的执行环境(上下文 context),待这个方法执行完成后,销毁当前的执行环境,并从栈中弹出此方法(即消费完成),然后继续下一个方法。
3.当遇到异步任务时就交给其他线程处理
4.等待当前执行栈所有同步代码执行完成后,会从队列中取出 已完成的异步任务的回调 加入执行栈继续执行,遇到异步任务时又交给其他线程,…,如此循环往复。
5.其他异步任务完成后,将回调放入任务队列中待执行栈来取出执行。
在这里插入图片描述
在这里插入图片描述

在事件驱动的模式下,至少包含了一个执行循环来检测任务队列是否有新的任务。通过不断循环去取出异步回调来执行,这个过程就是事件循环,而每一次循环就是一个事件周期或称为一次 tick

执行栈可视化

function f(b) {
	const a = 12;
	return a+b+35;
}

function g(x){
	const m = 4;
	return f(m*x);
}

g(21);

使用 Loupe 查看上面代码的执行过程如下,也可以使用 pythontutor.com/render.html… 手动查看每一步的执行
在这里插入图片描述

调用g的时候,创建了第一个函数帧,包含了g的参数和局部变量。当g调用f的时候,第二个函数帧就被创建、并置于第一个函数帧之上,包含了f的参数和局部变量。当f返回时,最上层的函数帧就出栈了(剩下g函数调用的函数帧)。当g返回的时候,栈就空了

单个事件循环过程

在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

●执行一个宏任务(栈中没有就从事件队列中获取)
●执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
●宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
●当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
●渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

执行过程总结

执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。
在这里插入图片描述

更形象化的表示

在这里插入图片描述

1.检查macrotask队列是否为空,非空则到2,为空则到3
2.执行macrotask中的一个任务
3.继续检查microtask队列是否为空,若有则到4,否则到5
4.取出microtask中的任务执行,执行完成返回到步骤3
5.执行视图更新

node环境下的事件循环

Node.js使用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv

libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现的
在这里插入图片描述

Node.js的运行机制

(1)V8引擎解析JavaScript脚本。
(2)解析后的代码,调用Node API。
(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
(4)V8引擎再将结果返回给用户。
Node.js 中事件循环的实现依赖 libuv 引擎,Libuv 库是事件驱动的,封装和统一了不同平台的 API 实现。核心源码 参考

int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int can_sleep;

  r = uv__loop_alive(loop);
  // 检查循环是否还活跃,然后更新时间
  if (!r)
    uv__update_time(loop);

  /* Maintain backwards compatibility by processing timers before entering the
   * while loop for UV_RUN_DEFAULT. Otherwise timers only need to be executed
   * once, which should be done after polling in order to maintain proper
   * execution order of the conceptual event loop. */
  // 如果运行模式是 UV_RUN_DEFAULT 并且循环仍然活跃且未被停止,会执行 timers 阶段
  if (mode == UV_RUN_DEFAULT && r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop);
	// timers阶段
    uv__run_timers(loop);
  }
  // 进入 while 循环,不断处理事件和回调,直到循环不再活跃或被停止
  // 依次执行 I/O callbacks、idle callbacks、prepare callbacks、poll 阶段(阻塞等待 I/O 事件)、处理 pending 队列中的事件、检查是否需要更新 provider_idle_time、执行 check 阶段、执行 close callbacks 阶段
  while (r != 0 && loop->stop_flag == 0) {
    can_sleep =
        uv__queue_empty(&loop->pending_queue) &&
        uv__queue_empty(&loop->idle_handles);
	// I/O callbacks 阶段
    uv__run_pending(loop);
    // idle阶段
    uv__run_idle(loop);
    // prepare阶段
    uv__run_prepare(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && can_sleep) || mode == UV_RUN_DEFAULT)
      timeout = uv__backend_timeout(loop);

    uv__metrics_inc_loop_count(loop);
    // poll阶段
    uv__io_poll(loop, timeout);

    /* Process immediate callbacks (e.g. write_cb) a small fixed number of
     * times to avoid loop starvation.*/
    for (r = 0; r < 8 && !uv__queue_empty(&loop->pending_queue); r++)
      uv__run_pending(loop);

    /* Run one final update on the provider_idle_time in case uv__io_poll
     * returned because the timeout expired, but no events were received. This
     * call will be ignored if the provider_entry_time was either never set (if
     * the timeout == 0) or was already updated b/c an event was received.
     */
    uv__metrics_update_idle_time(loop);
    // check阶段
    uv__run_check(loop);
    // close callbacks阶段
    uv__run_closing_handles(loop);
    // 更新时间并执行 timers 阶段
    uv__update_time(loop);
    uv__run_timers(loop);
    // 根据运行模式判断是否继续循环
    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  /* The if statement lets gcc compile it to a conditional store. Avoids
   * dirtying a cache line.
   */
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

Node的事件循环存在几个阶段,不同的版本执行顺序略有差异

node版本更新到11之后,Event Loop运行原理发生了变化,一旦执行一个阶段里的一个宏任务(setTimeout,setInterval和setImmediate)就立刻执行微任务队列

鸿蒙中的JS引擎基于libuv

具体的实现可以参考 gitee.com/openharmony… 这个仓库代码
在这里插入图片描述

事件循环模型

libuv引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段

每个阶段都有一个要执行的回调 FIFO 队列。
尽管每个阶段都有其自己的特殊方式,但是通常当事件循环进入给定阶段时,它将执行该阶段特定的任何操作,然后在该阶段的队列中执行回调,直到队列耗尽或执行回调的最大数量为止。当队列已为空或达到回调限制时,事件循环将移至下一个阶段,依此类推。
在这里插入图片描述

事件循环各阶段详解

node中事件循环的顺序

外部输入数据 --> 轮询阶段(poll) --> 检查阶段(check) --> 关闭事件回调阶段(close callback) --> 定时器检查阶段(timer) --> I/O 事件回调阶段(I/O callbacks) --> 闲置阶段(idle, prepare) --> 轮询阶段…

timers 阶段

执行所有到期的timer 加入timer队列里的callback,一个timer callback指得是一个通过setTimeout或者setInterval函数设置的回调函数

一个定时器指定一个时间阀值,过了这个值就尽早执行提供的回调函数,而不是一个人们希望它执行的确切时间。当指定的时间已过,定时器的回调函数会尽早执行,如果操作系统有其他定时任务或者正在执行其他回调,定时器的回调函数将会延迟执行。

> 注:技术上,poll轮询阶段控制定时器何时执行

举个例子,指定一个定时器100ms后执行,然后代码异步读取一个文件,耗费95ms:

const fs = require('fs');
function someAsyncOperation (callback) {
	// 假定这里读取完文件需要95ms
	fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(function () {
	const delay = Date.now() - timeoutScheduled;
	console.log(delay + "ms have passed since I was scheduled"); 
}, 100);

// someAsyncOperation的callback在95ms之后执行
someAsyncOperation(function () {
	const startCallback = Date.now();
	// 这里独占CPU10ms
	while (Date.now() - startCallback < 10) {
		// do nothing
	}
});

这里例子中,定时器打印时,已经是105ms了

1.读取文件耗费95ms,当文件读取完后,执行callback
2.callback中又耗费10ms啥也没干,干耗着不放CPU资源
3.当定时器到达100ms时,会被扔到定时器队列中
4.然而由于这时文件读取事件还未完成,因此定时器任务只能等待。等到105ms后,文件读取任务完成,取出定时器队列中的回调执行。

I/O callbacks 阶段

执行 除了close事件、timers(定时器,setTimeout、setInterval等)事件、setImmediate()事件之外的 几乎所有的回调,其中主要为系统级别的回调函数,比如 TCP 连接失败的回调;

idle, prepare 阶段

仅在node内部使用,可以不用管

poll 阶段

v8引擎将js代码解析并传入libuv引擎后,循环首先进入poll阶段。在这个阶段里会等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。
在node.js里,任何异步方法(除timer,close,setImmediate之外)完成时,都会将其callback加到poll queue里,并立即执行
poll阶段执行逻辑如下:

1.查看poll queue中是否有事件,有任务就按先进先出的顺序依次执行回调,直至queue为空或执行的callback到达系统上限
2.当queue为空时,会检查是否有setImmediate()的callback,如果有 event loop将结束poll阶段进入check阶段,并执行check阶段的queue中的callback。
3.同时也会检查是否有到期的timer,如果有就把这些到期的timer的callback按照调用顺序放到timer queue中,结束poll阶段进入timer阶段执行queue中的 callback。 这两者的顺序是不固定的,受代码运行环境的影响。
4.如果两者的queue都是空的,loop会在poll阶段停留,直到有一个i/o事件返回,之后会进入i/o callback阶段并立即执行这个事件的callback。node在一些特殊情况下会阻塞在这里
在这里插入图片描述

check 阶段

check阶段专门用来执行setImmediate()方法的回调,当poll阶段进入空闲状态,并且setImmediate queue中有callback时,事件循环进入这个阶段

close callbacks 阶段

当一个socket连接或者一个handle被突然关闭时(例如调用了socket.destroy()方法),close事件会被发送到这个阶段执行回调。否则事件会用process.nextTick()方法发送出去

小结

1.每一个阶段都有一个装有callbacks的fifo queue(队列)
2.当event loop运行到一个指定阶段时,node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时,event loop会转入下一个阶段
3.当所有阶段被顺序执行一次后,称 event loop 完成了一个 tick

注意上面六个阶段都不包括 process.nextTick()

Promise.nextTick, setTimeout, setImmediate的使用场景和区别

Promise.nextTick

node 中一个独立于 eventLoop 的特殊任务队列 nextTick queue,里面的事件会在每一个阶段执行完毕准备进入下一个阶段时优先执行
在每一个 eventLoop 阶段完成,进入下一个阶段之前会检查 nextTick 队列,如果里面有任务会让这部分任务优先于微任务执行,直至任务队列清空,如果使用错误会导致node死循环。是所有异步任务中最快执行的。

setTimeout

定义一个回调,并且希望这个回调在我们所指定的时间间隔后第一时间去执行。该回调并不会在我们预期的时间间隔后精准的执行。执行的时间存在一定的延迟和误差,这是不可避免的。node会在可以执行timer回调的第一时间去执行设定的任务

在poll阶段为空闲时,且设定时间到达后执行,但它在timer阶段执行

在浏览器中 setTimeout 的延时设置为 0 的话,会默认为 4ms,NodeJS 为 1ms。具体值可能不固定,但不是为 0。

setImmediate

从意义上是立刻执行的意思,但是实际上是在一个固定的阶段才会执行回调,即poll阶段之后 check阶段

Node与浏览器Event Loop差异

Node.js与浏览器的 Event Loop 差异如下:

●Node.js:microtask 在事件循环的各个阶段之间执行;
●浏览器:microtask 在事件循环的 macrotask 执行完之后执行
在这里插入图片描述

Nodejs和浏览器的事件循环流程对比如下:

1.执行全局的 Script 代码(与浏览器无差);

2.把微任务队列清空:注意,Node 清空微任务队列的手法比较特别

●在浏览器中,我们只有一个微任务队列需要接受处理;
●但在 Node 中,有两类微任务队列:next-tick 队列和其它队列。其中这个 next-tick 队列,专门用来收敛 process.nextTick 派发的异步任务。在清空队列时,优先清空 next-tick 队列中的任务,随后才会清空其它微任务;

3.开始执行 macro-task(宏任务)。注意,Node 执行宏任务的方式与浏览器不同:在浏览器中,我们每次出队并执行一个宏任务;而在 Node 中,我们每次会尝试清空当前阶段对应宏任务队列里的所有任务(除非达到系统限制);

4.步骤3开始,会进入 3 -> 2 -> 3 -> 2…的循环

写在最后

有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)资料用来跟着学习是非常有必要的。

这份鸿蒙(HarmonyOS NEXT)资料包含了鸿蒙开发必掌握的核心知识要点,内容包含了(ArkTS、ArkUI开发组件、Stage模型、多端部署、分布式应用开发、音频、视频、WebGL、OpenHarmony多媒体技术、Napi组件、OpenHarmony内核、鸿蒙南向开发、鸿蒙项目实战等等)鸿蒙(HarmonyOS NEXT)技术知识点。

希望这一份鸿蒙学习资料能够给大家带来帮助,有需要的小伙伴自行领取,限时开源,先到先得~无套路领取!!

获取这份完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习资料

鸿蒙(HarmonyOS NEXT)最新学习路线

在这里插入图片描述

有了路线图,怎么能没有学习资料呢,小编也准备了一份联合鸿蒙官方发布笔记整理收纳的一套系统性的鸿蒙(OpenHarmony )学习手册(共计1236页)与鸿蒙(OpenHarmony )开发入门教学视频,内容包含:ArkTS、ArkUI、Web开发、应用模型、资源分类…等知识点。

获取以上完整版高清学习路线,请点击→纯血版全套鸿蒙HarmonyOS学习资料

《鸿蒙 (OpenHarmony)开发入门教学视频》

在这里插入图片描述

《鸿蒙生态应用开发V2.0白皮书》

在这里插入图片描述

《鸿蒙 (OpenHarmony)开发基础到实战手册》

OpenHarmony北向、南向开发环境搭建

在这里插入图片描述

《鸿蒙开发基础》

●ArkTS语言
●安装DevEco Studio
●运用你的第一个ArkTS应用
●ArkUI声明式UI开发
.……
在这里插入图片描述

《鸿蒙开发进阶》

●Stage模型入门
●网络管理
●数据管理
●电话服务
●分布式应用开发
●通知与窗口管理
●多媒体技术
●安全技能
●任务管理
●WebGL
●国际化开发
●应用测试
●DFX面向未来设计
●鸿蒙系统移植和裁剪定制
……
在这里插入图片描述

《鸿蒙进阶实战》

●ArkTS实践
●UIAbility应用
●网络案例
……
在这里插入图片描述

获取以上完整鸿蒙HarmonyOS学习资料,请点击→纯血版全套鸿蒙HarmonyOS学习资料

;