跟co类似,但有一点点golang的感觉
发布于 8 年前 作者 yyrdl 4058 次浏览 来自 分享

要说什么啊: 一个和 co类似的模块,带点golang的感觉(defer和接收返回值的形式)。

哦,原来是造轮子啊? 哎呀,客官,别急,简单看看呗,绝对大有不同 ^_^

发这儿干嘛: 分享,然后求意见。正是由于身边人的建议,更新到了现在的版本。

talk is cheap,show me the code

好,代码来了

defer

defer 这个功能借鉴至golang的defer 关键字,执行逻辑也一样,在将要退出时执行绑定的操作.

const co = require("zco");
const request = require("request");

co(function*(next,defer) {  
	defer(function*(inner_next,error) {
	    if(error) {
		   console.log(error.message);//"模拟一下未知的错误" ,这个错误是后面手动throw模拟的
		}
		//做些清理工作,案例:个人在一个有热更新需求的项目中需要确保释放了模块的引用,并且无论之后的代码是否报错.
	});
	let [err,response,body]=yield request("www.baidu.com",next);//异步请求百度首页,与request模块无缝对接
	//..... do something else
	throw new Error("模拟一下未知的错误");
})();

inner_next功能和外部的next 一致,error是捕获的defer之后的代码抛出的异常。

超时终止协程

某些场景会设置最大等待时间,个人是做爬虫工作的,一些破网站经常超时,希望在超时时终止协程,不再继续往后执行。

到截止时间,还没有执行的操作就没有任何执行的意义了,应该尽早终止,这样避免了没有必要的资源消耗(内存,CPU,带宽…)。 需要注意的是 defer定义的操作在挂起协程时会执行.

来个栗子:


const co=require("zco");

let variable=1;
//先伪造一个耗时操作
const I_need_more_time=function(){
    return co(function*(next){
         variable+=100;//加100
		 yield setTimeout(next,2*100);//等待200毫秒,模拟耗时操作
		 variable+=1000;//加1000
	})
}
//期望在100毫秒内返回结果,否则终止,并抛出超时错误。这里铁定超时
co.timeLimit(1*100,co(function*(){
      variable+=10;
	  yield I_need_more_time();//执行准备好的耗时操作
	  variable+=10000;
	  return variable;
}))((err,result)=>{
   if(err){
      console.log(err.message);//打出 "timeout"
   }
})
//设一个更长的等待,确保上面的耗时操作都能执行完
setTimeout(function(){
    console.log(variable);//打印一下variable这个变量的值。
	// 值为 "111" ,说明加1000和加10000的操作都没有执行,因为被终止执行了。
},400);

终止协程的方法是zco模块内部使用的,如果开发者因为其他原因想要终止协程,也可以使用,给个例子:

const co=require("zco");

var zco_future=co(function*(next){
   //............
});
zco_future();//执行协程
zco_future.__zco_suspend__();//终止协程

只要持有zco的future就可以在任意时刻终止, co(), co.all(),co.timeLimit().co.wrapPromise() 都会返回一个zco的future。但是co.wrapPromise返回的future的终止方法并没有任何用,象征性的调一下,原因在于无法干预Promise内部的逻辑(有可能可以hack进去,还没研究过)。所以不推荐使用Promise。

结束语

上面列出的俩个特性是zco不同于tj 的co的主要区别。 写zco最初的目的是想抛弃Promise,因为在项目中Promise增加了额外的代码量,并且非常不喜欢Promise处理异常的方式,然而现存的coroutine模块又依赖于Promise,只好自己搞一个了。

在大家的建议和项目的需求中慢慢完善了这个模块,除了上述的示例,更多的例子可以在项目首页看到,地址: https://github.com/yyrdl/zco

欢迎提意见和建议 :)

6 回复

看一下这个吧 ,这个做的比较像golang https://github.com/dayuoba/allco

@dayuoba 接收返回数据这里比较像golang ,多返回值这里比较纠结

有一点疑问:

  • defer里面定义的错误,和co(generator).catch(errorHandle) 的做法相比有什么好处么,因为我感觉这样做错误处理逻辑看起来没有catch统一处理来的干净
  • timeLimit 后续操作不执行节省资源看示例应该没问题,但是如果当前进行一个很大的包网络传输,这也是很大的资源开销,那么 timeLimit 能保证帮助开发者把正在进行中的网络操作或者别的异步操作资源释放掉么

@hyj1991 第一个问题: defer不是设计来处理错误的,是用来做清理工作的(可联想C++析构函数或是golang defer的作用),同时传入了捕获到的错误是出于做清理工作时可能需要知道是否出错的考量。defer定义的操作和defer后面的代码处于同级作用域,而Promise的catch不是。然后Promise的catch是不分青红皂白的在最后统一处理,确实省事,但不同地方出的错误应具有不同的含义,在项目中需要严格区分,一是方便debug,二是方 便区分统计。所以zco在这里参考golang以值的形式返回,由使用者决定是抛出还是怎么处理,见例子中请求百度首页那儿。 co框架是把request包装成Promise,出错直接reject了。另外Promise不catch的话貌似会吞bug(co返回的是Promise)

第二个问题:不能,因为zco无法知道这个耗时的操作是什么操作,以及停止他的方法。只能保证他后面的操作不会执行。想要做到你的要求的话需要hook很多东西

@yyrdl 类比析构函数大概明白了,至于第二点我觉得还是很有现实意义的,能否考虑在defer中允许传入自定义的钩子来让开发者可以把当前超时的操作本身也释放掉

@hyj1991 想了想第二个问题,结合defer还是可以搞定的,但是需要自己code。给个伪代码:

const co=require("zco");
//上传大文件
var uploadLargeFile=function(){
   return co(function*(){});
}
//取消上传的方法
var abortUpload=function(){
}

co.timeLimit(1*1000,co(function*(next,defer){
    defer(function*(_,error){
	    if(error){//被终止时会传入一个相应的错误
		   abortUpload();//出错,取消上传,实际的时候需要做的安全一点,加一些标志位什么的
		}
	})
	//.....
	yiled uploadLargeFile();//开始上传大文件
	//......
	return "ok";
}))((err,result)=>{
   if(err){
     //错误正确的处理地方
   }else{
    
   }
})
回到顶部