最近看到一个实现异步方法转换为同步方法的库 deasync。正好看到有些场景可以用到,所以分析一下库的源代码。 这个库的源码在这里 deasync 。最好是看了我上一篇的介绍 NodeJS debug C++ 的那篇文章,同样的内容我就不注释了。
源代码 这其实是一个 NodeJS 的 C++ 的插件。核心代码只有下面的一些:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <uv.h> #include <v8.h> #include <napi.h> #include <uv.h> using namespace Napi;Napi::Value Run (const Napi::CallbackInfo& info) { Napi::Env env = info.Env (); Napi::HandleScope scope (env) ; uv_run (uv_default_loop (), UV_RUN_ONCE); return env.Undefined (); } static Napi::Object init (Napi::Env env, Napi::Object exports) { exports.Set (Napi::String::New (env, "run" ), Napi::Function::New (env, Run)); return exports; } NODE_API_MODULE (deasync, init)
这里面用的是 N-API 的接口,但这个库里面的代码不能正常的编译出 Debug 模块,所以把这核心代码拿出来又写了一个新的库,然后重新编译即可。
源码分析 直接看代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <uv.h> #include <v8.h> #include <napi.h> #include <node.h> using namespace Napi;Napi::Value Run (const Napi::CallbackInfo &info) { Napi::Env env = info.Env (); Napi::HandleScope scope (env) ; uv_run (uv_default_loop (), UV_RUN_ONCE); return env.Undefined (); } static Napi::Object init (Napi::Env env, Napi::Object exports) { exports.Set (Napi::String::New (env, "run" ), Napi::Function::New (env, Run)); return exports; } NODE_API_MODULE (NODE_GYP_MODULE_NAME, init)
uv_run
显式的运行一次 EventLoop, 并且暴露 run 方法,然后用在下面的 js 代码中:
1 2 3 4 5 6 7 const deasyncAddon = require ('./build/Debug/hello.node' )const loopWhile = function (pred ) { while (pred ()) { process._tickCallback () if (pred ()) deasyncAddon.run () } }
先不管 process._tickCallback()
,看起来就像是写了一个阻塞的循环代码,只有 pred() 或者 isEnd() 结束后才会中断:
1 2 3 4 5 const wait = function (time ) { const start = Date .now () const isEnd = ( ) => (Date .now () - start) < time while (isEnd ()) {} }
但几乎完全不一样。pred 这个回调方法完成后,就会终止,而不像 wait 方法让整个线程都阻塞,干不了其它事情。
在 loopWhite 方法中, process._tickCallback()
的使用也很巧妙,在 NodeJS 源码中有说:
1 2 3 4 5 process.nextTick = nextTick; process._tickCallback = runNextTicks;
在正常的代码中出现 _tickCallback(),则马上执行之前所有的 nextTicks 任务,接着执行同步任务。但我看 runNextTicks 源码的时候,发现其实是执行 runMicrotasks
😂。所以会把 Process.nextTick、Promise.then catch finally、MutationObserver
这些全部执行完毕后,才会进入下一步。
所以如果代码是这样的:
1 2 3 4 5 6 7 8 9 10 11 setImmediate (() => { console .log ('setImmediate 1' )})process.nextTick (() => { console .log ('nextTick 1' ) }) Promise .resolve ().then (() => { console .log ('promise then 1' )})process._tickCallback () console .log ('sync 1' )
为什么会这样呢?因为 nextTicks 的名字有误,nodejs 网站上面有说, setImmediate 意思应该互换 nextTick。nextTick 才是在同一个阶段立即执行, 而 setImmediate 是在 EventLoop 接下来执行,或者在 tick 上触发。(文末参考链接)
使用例子 首先 deasync 库把 loopWhile 封装成一个等待方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function deasync (fn ) { return function ( ) { let done = false let args = Array .prototype .slice .apply (arguments ).concat (cb) let err let res fn.apply (this , args) loopWhile (() => !done) if (err) throw err return res function cb (e, r ) { err = e res = r done = true } } }
然后可以写一个简单的阻塞 sleep 方法:
1 2 3 4 5 const sleep = deasync (function (timeout, done ) { setTimeout (done, timeout) }) sleep (1000 ) console .log ('run here!' )
更多可以去看看 deasync 中的例子。
un promisify deasync 默认只处理带 callback 的方法。如果是想要处理 promise 的方法,需要包裹一层转化一下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const unPromisify = fn => { return (...args ) => { if (args.length <1 ) throw new Error ('unPromisify arguments length must at least 2.' ) const cb = args.pop () fn (...args).then (data => cb (null , data)).catch (err => cb (err, null )) } } async function say (e,e2 ) { console .log ('good' , e, e2) return 'r' } const saySync = deasync (unPromisify (say))try { const c = saySync ('allen' , 'alex' ) console .log ('return:' , c) } catch (e) { console .error (e) } console .log ('done' )
同样类型的库 还有一个类似功能的库:node-fibers ,不过它的实现原理不同,并且复杂得多。
参考