Nodejs-中的事件循环计时器和processnextTick

前言本篇文章翻译自 Node.js 官网的同名文章也算是经典老物了, 不过官网的文章也随着 Node.js 的演化在修改, 这篇文章最后的编辑时间是 2019年9月10日请注意时效性, 地址在文章的最后有给出. 首次翻译英语水平有限, 错误之处还请多多指教. 什么是事件循环事件循环允许node.js执行非阻塞I/O操作. 虽然 JavaScript 是单线程的, 但是事件循环会尽可能的将操作转移到系统内核中来完成. 现代的操作系统内核都是多线程的, 它们可以在后台处理多种操作. 一旦这些操作完成, 系统内核会通知 Node.js 以便将事件回调放入轮询队列中等待执行. (我们会在随后的内容讨论它们的具体工作细节) 解析事件循环当 Node.js 启动的时候, 他会初始化事件循环, 处理输入的脚本内容 (或者进入 REPL), 脚本可能会调用异步接口, 设置定时器, 或者调用 process.nextTick(), 然后开始处理事件循环(eventloop). 下面的简图中展示了事件循环的操作流程: ┌───────────────────────┐┌─>│ timers ││ └──────────┬────────────┘│ ┌──────────┴────────────┐│ │ I/O callbacks ││ └──────────┬────────────┘│ ┌──────────┴────────────┐│ │ idle, prepare ││ └──────────┬────────────┘ ┌───────────────┐│ ┌──────────┴────────────┐ │ incoming: ││ │ poll │<─────┤ connections, ││ └──────────┬────────────┘ │ data, etc. ││ ┌──────────┴────────────┐ └───────────────┘│ │ check ││ └──────────┬────────────┘│ ┌──────────┴────────────┐└──┤ close callbacks │ └───────────────────────┘每一个方框代表了事件循环中不同的阶段(所有阶段执行完成算是一次事件循环).每一个阶段都有一个由回调组成的 FIFO 队列被用于执行. 虽然不同的队列执行方式不同, 总的来看, 当事件循环进入该阶段后会执行该阶段对应的操作, 然后调用对应的回调直到队列耗尽或者达到了回调执行上限. 在到达上述情况后事件循环进入下一阶段, 然后继续这样的流程. ...

September 11, 2019 · 4 min · jiezi

Java并发16-CompletionService批量执行异步任务

我们思考下这个场景:从三个电商询价,然后保存在自己的数据库里。通过之前所学,我们可能这么实现。 // 创建线程池ExecutorService executor = Executors.newFixedThreadPool(3);// 异步向电商 S1 询价Future<Integer> f1 = executor.submit( ()->getPriceByS1());// 异步向电商 S2 询价Future<Integer> f2 = executor.submit( ()->getPriceByS2());// 异步向电商 S3 询价Future<Integer> f3 = executor.submit( ()->getPriceByS3()); // 获取电商 S1 报价并保存r=f1.get();executor.execute(()->save(r)); // 获取电商 S2 报价并保存r=f2.get();executor.execute(()->save(r)); // 获取电商 S3 报价并保存 r=f3.get();executor.execute(()->save(r));上面的这个方案本身没有太大问题,但是有个地方的处理需要你注意,那就是如果获取电商 S1 报价的耗时很长,那么即便获取电商 S2 报价的耗时很短,也无法让保存 S2 报价的操作先执行,因为这个主线程都阻塞在了 f1.get(),那我们如何解决了? 我们可以增加一个阻塞队列,获取到 S1、S2、S3 的报价都进入阻塞队列,然后在主线程中消费阻塞队列,这样就能保证先获取到的报价先保存到数据库了。下面的示例代码展示了如何利用阻塞队列实现先获取到的报价先保存到数据库。 // 创建阻塞队列BlockingQueue<Integer> bq = new LinkedBlockingQueue<>();// 电商 S1 报价异步进入阻塞队列 executor.execute(()-> bq.put(f1.get()));// 电商 S2 报价异步进入阻塞队列 executor.execute(()-> bq.put(f2.get()));// 电商 S3 报价异步进入阻塞队列 executor.execute(()-> bq.put(f3.get()));// 异步保存所有报价 for (int i=0; i<3; i++) { Integer r = bq.take(); executor.execute(()->save(r));} 利用 CompletionService 实现询价系统不过在实际项目中,并不建议你这样做,因为 Java SDK 并发包里已经提供了设计精良的 CompletionService。利用 CompletionService 能让代码更简练。 ...

June 27, 2019 · 2 min · jiezi

js异步从入门到放弃-三-异步任务队列task-queues

前言本文是对于异步系列第一篇里提到的evenloop模型中,所提到的任务队列(task queues)的展开分析 正文说明:以下代码均使用chrome浏览器运行 关于浏览器表现的差异在最后做补充。 引子-奇怪的执行顺序先看一个典型的例子: console.log('script start')// 第一个异步任务setTimeout(()=>{ console.log('setTimeout')},0)// 第二个异步任务Promise.resolve().then(()=>{ console.log('promise1')}).then(()=>{ console.log('promise2');})console.log('script end')// 实际输出结果: // script start// script end// promise1// promise2// setTimeout根据之前说过的evenloop模型,先输出script start和script end,但是接下来却发现,先执行了Promise指定的callback而不是setTimeout的callback。 两种任务队列(microtask queue&macrotask queue)在之前讨论evenloop模型时,提到了任务队列有2种类型:microtask queue和macrotask queue,他们的区别在于: macrotask的执行:是在evenloop的每次循环过程,取出macrotask queue中可执行的第一个(注意不一定是第一个,因为我们说过例如setTimeout可以指定任务被执行的最少延迟时间,当前macrotask queue的首位保存的任务可能还没有到执行时间,所以queue只是代表callback插入的顺序,不代表执行时也要按照这个顺序)。microtask的执行:在evenloop的每次循环过程之后,如果当前的执行栈(call stack)为空,那么执行microtask queue中所有可执行的任务(某些文献内容中 直接把macrotask称为task,或者某些中文文章中把它们翻译成"微任务"和"宏任务",含义都是相似的:macrotask或者task代表相对单独占据evenloop过程一次循环的任务,而microtask有可能在一次循环中执行多个) 现在回头来解析前面的例子: 第一次执行主函数,输出script start遇到setTimeout,将对应的callback插入macrotask queue遇到promise,将对应的callback插入microtask queue输出script end,主函数运行结束,执行栈清空,此时开始检查microtask queue,发现里面有可运行的任务,因此按顺序输出promise1和promise2microtask queue执行完,开始新一轮循环,从macrotask queue取出setTimeout任务并执行,输出setTimeout结束,呈现上面的输出结果。常见异步操作对应的回调函数任务类型如下: macrotask: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI renderingmicrotask: process.nextTick, Promises, Object.observe, MutationObserver大概可以这样区分:和html交互密切相关的异步操作,一般是macrotasks;由emcascript的相关接口返回的异步操作,一般是microtasks 如何判断执行顺序接下来看一个更复杂的例子,帮助理解不同异步任务的执行顺序 <style> .outer { padding: 30px; background-color: aqua; } .inner { height: 100px; background-color: brown; }</style><body> <div class="outer">outer <div class="inner">inner</div> </div></body><script> var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // Let's listen for attribute changes on the // outer element new MutationObserver(function () { console.log('mutate'); }).observe(outer, { attributes: true }); // Here's a click listener… function onClick() { console.log('click'); setTimeout(function () { console.log('timeout'); }, 0); Promise.resolve().then(function () { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } // …which we'll attach to both elements inner.addEventListener('click', onClick); outer.addEventListener('click', onClick);运行以上代码,可以在浏览器看到两个嵌套的div(如图): ...

May 9, 2019 · 1 min · jiezi

JavaScript事件循环(Event Loop)

1、为什么要有事件循环?因为js是单线程的,事件循环是js的执行机制,也是js实现异步的一种方法。既然js是单线程,那就像只有一个窗口的银行,客户需要排队一个一个办理业务,同理js任务也要一个一个顺序执行。如果一个任务耗时过长,那么后一个任务也必须等着。那么问题来了,假如我们想浏览新闻,但是新闻包含的超清图片加载很慢,难道我们的网页要一直卡着直到图片完全显示出来?因此聪明的程序员将任务分为两类:同步任务异步任务当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。2、宏任务与微任务JavaScript中除了广泛的同步任务和异步任务,我们对任务有更精细的定义:macro-task(宏任务): 包括整体代码script,setTimeout,setIntervalmicro-task(微任务): Promise,process.nextTick不同的类型的任务会进入不同的Event Queue(事件队列),比如setTimeout、setInterval会进入一个事件队列,而Promise会进入另一个事件队列。一次事件循环中有宏任务队列和微任务队列。事件循环的顺序,决定js代码执行的顺序。进入整体代码(宏任务-<script>包裹的代码可以理解为第一个宏任务),开始第一次循环,接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列的任务执行完毕,再执行所有的微任务。如:<script> setTimeout(function() { console.log(‘setTimeout’); }) new Promise(function(resolve) { console.log(‘promise’); }).then(function() { console.log(’then’); }) console.log(‘console’); /* —————————-分析 start——————————— / 1、&lt;script&gt;中的整段代码作为第一个宏任务,进入主线程。即开启第一次事件循环 2、遇到setTimeout,将其回调函数放入Event table中注册,然后分发到宏任务Event Queue中 3、接下来遇到new Promise、Promise,立即执行;将then函数分发到微任务Event Queue中。输出: promise 4、遇到console.log,立即执行。输出: console 5、整体代码作为第一个宏任务执行结束,此时去微任务队列中查看有哪些微任务,结果发现了then函数,然后将它推入主线程并执行。输出: then 6、第一轮事件循环结束 开启第二轮事件循环。先从宏任务开始,去宏任务事件队列中查看有哪些宏任务,在宏任务事件队列中找到了setTimeout对应的回调函数,立即执行之。此时宏任务事件队列中已经没有事件了,然后去微任务事件队列中查看是否有事件,结果没有。此时第二轮事件循环结束;输出:setTimeout / —————————-分析 end——————————— */</script>3、分析更复杂的代码<script> console.log(‘1’); setTimeout(function() { console.log(‘2’); process.nextTick(function() { console.log(‘3’); }) new Promise(function(resolve) { console.log(‘4’); resolve(); }).then(function() { console.log(‘5’) }) }) process.nextTick(function() { console.log(‘6’); }) new Promise(function(resolve) { console.log(‘7’); resolve(); }).then(function() { console.log(‘8’) }) setTimeout(function() { console.log(‘9’); process.nextTick(function() { console.log(‘10’); }) new Promise(function(resolve) { console.log(‘11’); resolve(); }).then(function() { console.log(‘12’) }) })</script>一、第一轮事件循环a)、整段<script>代码作为第一个宏任务进入主线程,即开启第一轮事件循环b)、遇到console.log,立即执行。输出:1c)、遇到setTimeout,将其回调函数放入Event table中注册,然后分发到宏任务事件队列中。我们将其标记为setTimeout1d)、遇到process.nextTick,其回调函数放入Event table中注册,然后被分发到微任务事件队列中。记为process1e)、遇到new Promise、Promise,立即执行;then回调函数放入Event table中注册,然后被分发到微任务事件队列中。记为then1。输出: 7f)、遇到setTimeout,将其回调函数放入Event table中注册,然后分发到宏任务事件队列中。我们将其标记为setTimeout2此时第一轮事件循环宏任务结束,下表是第一轮事件循环宏任务结束时各Event Queue的情况-宏任务事件队列微任务事件队列第一轮事件循环(宏任务已结束)process1、then1第二轮事件循环(未开始)setTimeout1 第三轮事件循环(未开始)setTimeout2 可以看到第一轮事件循环宏任务结束后微任务事件队列中还有两个事件待执行,因此这两个事件会被推入主线程,然后执行g)、执行process1。输出:6h)、执行then1。输出:8第一轮事件循环正式结束!二、第二轮事件循环a)、第二轮事件循环从宏任务setTimeout1开始。遇到console.log,立即执行。输出: 2b)、遇到process.nextTick,其回调函数放入Event table中注册,然后被分发到微任务事件队列中。记为process2c)、遇到new Promise,立即执行;then回调函数放入Event table中注册,然后被分发到微任务事件队列中。记为then2。输出: 5此时第二轮事件循环宏任务结束,下表是第二轮事件循环宏任务结束时各Event Queue的情况-宏任务事件队列微任务事件队列第一轮事件循环(已结束) 第二轮事件循环(宏任务已结束)process2、then2第三轮事件循环(未开始)setTimeout2 可以看到第二轮事件循环宏任务结束后微任务事件队列中还有两个事件待执行,因此这两个事件会被推入主线程,然后执行d)、执行process2。输出:3e)、执行then2。输出:5第二轮事件循环正式结束!三、第三轮事件循环a)、第三轮事件循环从宏任务setTimeout2开始。遇到console.log,立即执行。输出: 9d)、遇到process.nextTick,其回调函数放入Event table中注册,然后被分发到微任务事件队列中。记为process3c)、遇到new Promise,立即执行;then回调函数放入Event table中注册,然后被分发到微任务事件队列中。记为then3。输出: 11此时第三轮事件循环宏任务结束,下表是第三轮事件循环宏任务结束时各Event Queue的情况-宏任务事件队列微任务事件队列第一轮事件循环(已结束) 第二轮事件循环(已结束) 第三轮事件循环(未开始)(宏任务已结束)process3、then3可以看到第二轮事件循环宏任务结束后微任务事件队列中还有两个事件待执行,因此这两个事件会被推入主线程,然后执行d)、执行process3。输出:10e)、执行then3。输出:124、参考文章https://juejin.im/post/59e85e… ...

January 21, 2019 · 1 min · jiezi

你与弄懂promise之间可能只差这篇文章(一)

promise诞生之前:因为JS引擎在执行js代码时只分配了一个线程去执行,所以Javascript是单线程的。由于有这个前置设定,前端er在书写代码时绕不开的一件事是就是—-如何处理异步,即处理“现在和稍后”关系的问题,事实上我们每一天都在与异步逻辑打交道。在promise出现之前,前端er基本上都是通过callback的方式来解决“稍后”的问题,例如有经典的“发布-订阅”模式,观察者模式,他们都运用了传入回调函数的高阶函数。vue2.x源码在实现数据双向绑定时就是运用的发布-订阅模式。我们先来看看三个例子。(例子均在node环境中运行, 其中name.txt中的内容是"kk", age.txt中的内容是10。)1 . 回调函数(callback)。fs读取文件的先后顺序是不固定的,我们无法判断哪个文件先读取完成。此例实现的是,在完全读取两个文件的内容之后进行某个操作(例如console个啥的)。let fs = require(‘fs’);let arr = [];let after = (times, cb) => { return (data) => { arr.push(data); if (–times === 0) { cb(arr) } }}let on = after(2, (arr) => { console.log(‘我是在全部读取了2个文件内容之后打印出来的, ‘, arr)})fs.readFile(’name.txt’, ‘utf8’, (err, data) => { on(data)})fs.readFile(‘age.txt’, ‘utf8’, (err, data) => { on(data)})结果: 我是在全部读取了2个文件内容之后打印出来的, [ ‘kk’, ‘10’ ]。说明: 这种写法的问题在于,需要依靠计数来执行回调函数里面的内容。我们先得这计算出有几个异步操作,然后统计出来在全部的异步操作完成后再执行回调。2 .发布-订阅模式。订阅的时候添加订阅者,发布的时候执行相应的订阅函数。此例实现的是,在特定的时候emit了某事件,订阅了该事件的回调函数继而执行。class EventEmitter { constructor () { this.subs = {} } on (eventName, cb) { if (!this.subs[eventName]) { this.subs[eventName] = [] } this.subs[eventName].push((…args) => cb(…args)) } emit (eventName, …args) { if (this.subs[eventName]) { this.subs[eventName].forEach(cb => cb(…args)) } else { throw Error(没有订阅${eventName}这个事件) } }}const event = new EventEmitter();let fs = require(‘fs’);event.on(‘kk-event’, (…args) => { fs.readFile(’name.txt’, ‘utf8’, (err, data) => { console.log(‘data1’, data, …args) })})event.on(‘kk-event’, (…args) => { fs.readFile(‘age.txt’, ‘utf8’, (err, data) => { console.log(‘data2’, data, …args) })})event.emit(‘kk-event’, 123, 456)结果:data1 kk 123 456data2 10 123 4563 . 观察者模式。它与发布-订阅两者本质是一样的,只不过观察者模式在写法上强调观察者和被观察者之间的关系,而发布-订阅模式则没有这样的关系。此例实现的是,在被观察者的状态发生变化后,观察者执行自己的update方法进行更新。 class Subject { constructor() { this.observers = []; this.state = ‘’; // 假设观察者观察的是被观察者的state } setState (status) { // 当state变化时出发观察者的update方法 this.state = status; this.notify(); } attach (observer) { this.observers.push(observer) // 与发布-订阅不同的是,这里添加的是一个个观察者实例,这就将被观察者和观察者之间关联了起来 } notify () { this.observers.forEach(observe => observe.update()) // 在被观察者状态变化时,调用更新的是观察者的update方法 }}class Observer { constructor (name, target) { this.name = name; this.target = target; } update () { console.log(通知${this.name},被观察者状态变化,所以观察者${this.name}跟着变化) }}let fs = require(‘fs’);let subject = new Subject();let observer1 = new Observer(‘kk1’, subject);let observer2 = new Observer(‘kk2’, subject);subject.attach(observer1);subject.attach(observer2);subject.setState(‘B’);结果:通知kk1,被观察者状态变化,所以观察者kk1跟着变化通知kk2,被观察者状态变化,所以观察者kk2跟着变化 ...

January 8, 2019 · 2 min · jiezi

类script标签,异步加载,顺序执行

主要是想实现把压缩加密后的js文件存储在本地,网上找了下没找到理想的,所以自己动手写了一个,主要是仿照script标签的功能,实现异步加载,顺序执行。如果本地已经有该文件,则不重新加载,直接调用本地数据。jsFile是存储文件信息的数组,其中,path是文件路径,name是存储在本地的名字,active表示当前文件是否已经执行,load表示当前文件手已经加载完成,还要一个隐藏的content属性,存储文件内容。!function loadJS(){ var jsFile = [ {name:‘file1’,path:‘js/file1.js’,active:false,load:false}, {name:‘file2’,path:‘js/file2.js’,active:false,load:false}, {name:‘file3’,path:‘js/file3.js’,active:false,load:false}, {name:‘file4’,path:‘js/file4.js’,active:false,load:false}, {name:‘file5’,path:‘js/file5.js’,active:false,load:false} ] jsFile.forEach(function(item, index){ if(localStorage[‘file_’ + item.name]){ item.load = true implementJS(item, index) }else{ $.ajax({ type:“get”, url:item.path, dataType:’text’, success:function(data){ item.content = data item.load = true implementJS(item, index) } }); } }) function implementJS(item, index){ //如果上一个文件已经执行了,则执行这个js文件 if(index == 0 || jsFile[index - 1].active){ storageJS(item, index) //尝试执行下一个js文件 jsFile[index + 1] && jsFile[index + 1].load && implementJS(jsFile[index + 1], index + 1) } } function storageJS(item, index){ //存储并执行js文件 var name = ‘file_’ + item.name localStorage[name] = item.content || localStorage[name] || ’’ //这里要使用window.eval或者eval.call(window),否则eval里面的变量就不是全局变量 window.eval(localStorage[name]) item.active = true }}() ...

December 28, 2018 · 1 min · jiezi