一个Node.js的执行问题
发布于 8 年前 作者 jincdream 8638 次浏览 来自 问答
const fs = require('fs')
fs.writeFile('a.txt',(err)=>{
  console.log(2)
})
console.log(1)

这里的1一定会先输出,然后再输出2,为什么是一定? 为什么是一定?为什么是一定

26 回复

你这不是异步的吗? fs.writeFile先执行,但这个操作肯定需要时间,等有callback之后才输出的。好好看看基础知识再说吧

@SoftwareDreamer 这个操作肯定需要时间?你确定是因为这个吗?这个解释似乎不合理吧。

内存操作不需要中断。硬盘操作还需要等总线中断,然后切入IDT,再通过N个调用门返回- -。

推荐你看这个 callback

@ncuzp 好东西 !

@MiguelValentine 但在node.js里面似乎不是因为这个吧?

楼下说的对~,我收回~

上面一票人都没真正明白nodejs的异步啊。

楼主所提的问题跟异步执行时间长短无关,fs.writeFile只是发起异步调用命令,发起之后这个命令就交给其他线程去执行了,javascript代码继续执行,也就是在javascript层面,代码永远是同步的。

很多人会用回调异步,其实并没有真正理解回调异步。要理解回调异步,首先要明确回调和异步的关系,回调是回调,异步是异步,两者没有任何关系。异步实现的方式有多种,中断,多线程,多进程,多服务器,都可以实现异步,异步的本质上让别人帮自己做事情。当我们发起异步调用,我们首先将回调函数注册到事件队列中,发布异步命令,然后继续执行下面的代码。注册的回调函数在异步操作完成之后,将执行的返回结果传给主线程(或着从内核传给应用层),将异步返回值作为函数参数,执行我们原先注册的回调函数,至此我们通过回调函数又回到了javascript。如果我们发布异步调用后,我们自己部继续执行,而是等待异步完成,这就成了原先同步程序,异步调用会出现阻塞,这样程序就无法异步带来的好处。

接下来谈回调,回调是什么?回调是保证同步的一种方式(性能相对最好的方式)。虽然我们需要异步带来的性能好处,但我们大部分业务逻辑其实是同步的(很可能事实上是全部,只要你关心某个回调操作的结果,你就不可避免产生同步需求)。回调通过层层镶套,保证了业务逻辑的顺序执行,也即保证了同步。除了回调之外,还有锁,协程等可以用来保证同步,fs.writeFile本质上就是依靠锁来保证同步的,nodejs不过是将其包装成回调而已。协程是CPS(continuation-passing style)的特化实现,协程各种实现的能力不同,同步的能力和方式也不同。ES6 的 generator/yield 也是CPS特化,但这种类协程实现太弱,利用他来实现同步需要进行复杂的外部包装,要理解这种异步程序,必须先要深刻理解回调模式异步,因为这种异步的本质还是回调模式异步,只不过通过包装,让原来的回调调用由类似 co 这样的自动执行器调用了,promise 的作用是规范回调参数,以有利于回调在执行器里的自动执行,这种模式在语言层面还是无法回避回调异步模式带来的问题,尤其是多个 generator 调用时,执行顺序一定要考虑清楚,协程本质上就是高级 goto,generator 也不会例外,即便到 ES7,async/await 也不能从根本上解决这种弱协程所带来的问题。lua的协程实现则相对强大的许多,lua可以在语言层面用纯粹的同步代码实现异步程序。

我个人认为,javascript将来的标准必然也会实现类似lua的协程,这个结果可能让现在一部分人不能接受,以为自己现在能够异步思维来写程序,比那些写惯同步程序的程序员有优势,javascript如果可以用同步代码了表达异步过程,就会损害这些人的即得利益。但我认为我们其实并不是真正喜欢异步,我们只是喜欢异步带来的性能好处。有的人说这个世界是异步,但我们的大脑却是同步的(少数牛人除外),异步虽然能够带来性能的好处,但我们的业务逻辑大多是同步的。性能,业务逻辑,编程模型三者总是矛盾的。回调异步理论上是性能最好的,但编程模型是最差的,编程体验随着业务逻辑规模的扩大急剧下降; generator/yield 性能要比回调弱一点,编程模型有改进,但随着业务逻辑的扩大,还是需要精通回调异步的程序员来设计准确的调用流程(见后面代码);独立堆栈的协程性能跟纯粹的回调比要弱,优化到位可以跟 generator/yield 相当,编程模型是三者种最好的,最直观最容易理解,跟原先函数返回值同步程序写法没有区别。

下面一段代码引用自 @a1511870876koa技术分享

var app = new SimpleKoa();
app.use(function *(next){
    this.body = '1';
    yield next;
    this.body += '5';
    console.log(this.body);
});
 app.use(function *(next){
     this.body += '2';
     yield next;
	 this.body += '4';
});
 app.use(function *(next){
	 this.body += '3';
 });
app.listen();

这样的代码作为一个例子说明即便是局部的同步可以相对清晰的实现,但跨generator的执行顺序我们还是需要仔细考虑清楚的,毕竟 generator/yield 的组合本质上就是高级 goto,我们学习编程的时候老师反复强调不要用 goto,其实 goto并没有错,错的是不明所以的随意使用,goto 在某些场合可以明显简化代码,这个时候就不能教条的听老师的,但用 goto 的时候必须清楚自己在干什么,尤其是对资源的清理一定要考虑全面。同样的道理,generator/yield 也没有错,用的时候我们必须规划好各个generator的调用顺序,这个顺序某些时候可能被设计的并不是那么直观,类似这个例子作为学习理解koa执行顺序的例子很好,但在实现中我们是要尽量避免的。

我告诉你为什么一定。。。 因为,nodejs把writeFile的callback放到下一个事件循环,而下面的console.log是当前的函数里的语句。是机制是这样,跟快慢无关。目前是这样。

10楼简单直白,一语中的!writeFile再快也没用,它是在下一个循环中,而console.log(1)是在当前循环中。一定是先输出的。

process.nextTick(() => console.log(2)); //相当于 fs.writeFile  nextTick是最快的异步

var start = new Date(); // 模拟同步耗时操作
function fn(n){   
	if(n<=2){    
		return 1;    
	}else{    
		return fn(n-1)+fn(n-2);    
	}    
}    
fn(40); // 同步调用
console.log('elapsed: %d', new Date() - start); 
console.log(1);

输出参考 :

elapsed: 1852
1
2

基础js问题。标准不同。js回调函数都会在下一个周期执行。

mozilla的文档是比较权威的。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop

拿楼主的例子来讲,不管存不存在下一个事件,javascript代码都会一直执行下去,直到javascript栈为空,然后才会到事件队列里去看有没有事件。换句话就是说不是因为事件发生在下个循环中,console.log(1)才一定会先执行,而是因为javascript的事件必定是在javascript栈为空(也即函数全部返回)后才会执行。

如果想更深入的了解,请参考 @LanceHBZhang

Node.js挖掘之六:两条腿走路,一个例子浅析libuv架构

这篇文章是到目前为止是最准确的,其他所谓大神的,看看就算了,要么本身就是错的,要么就是过时的,误导的成分很多。

最好,也最实际的,收获也最多的,其实是自己去读源代码。

类似 setTimeout(fn, 0); …

@coordcn 我说的不就是那个意思嘛?第一个操作需要时间,第二个操作当然会提前Log出来。(因为Node.js是异步的,虽然第一个操作是先执行,但其callback之后;第二个操作已经早执行了)

@jincdream 看一看node.js的异步就知道了。Node.js是异步的,你第一个操作需要时间,但第二个操作不会因为第一个操作需要时间而等待(Node.js异步处理)。所以第二个log先执行,而第一个写文件callback之后就会Log出来。

@SoftwareDreamer

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop

这是mozilla的文档,看了你就自然明白了。

javascript只有在栈被清空后才会到事件队列里面去看有没有事件可执行,这才是问题的关键,这个问题也是保证javascript可以依靠层层回调来保证逻辑同步的关键。这里跟异步操作需要执行多少时间没有关系,哪怕执行时间再少,也得等到javascript函数全部执行完成后才会到事件队列里去执行回调函数。这个问题同样也解释了nodejs为什么不适合做大量计算。

@klesh 非常好。并不是像前面几个人说的,什么执行时间,而是于回调函数的执行时间无关,因为回调函数会在写一个循环中才执行。你这个例子说的很好。测试没问题亿,棒!

我是看了这个东西,才提了这个问题出来,看来能说出所以然的人不多。我一开始也不知道的~

执行js代码的时候,通过调用node提供的C++接口最终把io观察者都保存到default_loop_struct里面,js代码执行完之后,node继续运行才进入epoll_wait()等待。也就是说node在epoll_wait()的时候,js代码执行完毕了。 node源码详解(二 )

@jincdream

这也是我认为一些nodejs鼓吹者浮躁的原因,这个问题是最基础,也是最本质的问题,但很遗憾,并没有多少人愿意去弄清楚,包括一些所谓的大神的文章,要么本身就是错的,要么是过时的。

源代码面前无秘密,代码就在那里,一些人宁愿相信大神的忽悠,也不愿意去多看一眼源代码。

@coordcn

回调是什么?回调是保证同步的一种方式(性能相对最好的方式)。虽然我们需要异步带来的性能好处,但我们大部分业务逻辑其实是同步的(很可能事实上是全部,只要你关心某个回调操作的结果,你就不可避免产生同步需求)。回调通过层层镶套,保证了业务逻辑的顺序执行,也即保证了同步。除了回调之外,还有锁,协程等可以用来保证同步

这段让我毛瑟顿开,感谢~~

因为fs比后面那个console.log后返回,为什么后返回呢?因为IO操作啊

你的代码是同步的

When the stack is empty, a message is taken out of the queue and processed.

楼主你要好好看@coordcn 给的链接,那里给出了一个清晰的说明。最好看英文原版:https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop

回到顶部