├── .gitattributes ├── .gitignore ├── .travis.yml ├── howdo.js ├── package.json ├── readme.md ├── test ├── _utils.js ├── follow.js ├── readme.md ├── together.js └── until.js └── todo.md /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Thumbs.db 2 | ehthumbs.db 3 | Desktop.ini 4 | $RECYCLE.BIN/ 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | Icon 9 | ._* 10 | .Spotlight-V100 11 | .Trashes 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | *.iml 17 | .idea/ 18 | .ipr 19 | .iws 20 | *~ 21 | ~* 22 | *.diff 23 | *.patch 24 | *.bak 25 | .DS_Store 26 | .project 27 | .*proj 28 | .svn/ 29 | *.swp 30 | *.swo 31 | *.pyc 32 | *.pyo 33 | .build 34 | node_modules 35 | sea-modules 36 | _site 37 | .cache 38 | dist 39 | examples 40 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | before_script: 5 | - export DISPLAY=:99.0 6 | - sh -e /etc/init.d/xvfb start 7 | script: 8 | - npm test -------------------------------------------------------------------------------- /howdo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Howdo 3 | * 适应了 global 与 window 4 | * @author ydr.me 5 | * @create 2014年7月26日19:28:27 6 | * @update 2014年8月26日13:09:31 7 | * @update 2014年10月24日00:24:32 8 | * @update 2015年02月04日11:42:57 9 | * @update 2015年07月01日17:35:20 10 | * @update 2015年11月27日10:57:12@3.0.0 11 | */ 12 | 13 | 14 | 'use strict'; 15 | 16 | var _global = typeof window !== 'undefined' ? window : global; 17 | var slice = Array.prototype.slice; 18 | var noop = function () { 19 | // ignore 20 | }; 21 | /** 22 | * 判断是否为函数 23 | * @param obj 24 | * @returns {boolean} 25 | */ 26 | var isFunction = function (obj) { 27 | return typeof obj === 'function'; 28 | }; 29 | 30 | 31 | /** 32 | * 遍历 33 | * @param object 34 | * @param callback 35 | */ 36 | var each = function (object, callback) { 37 | var i; 38 | var j; 39 | 40 | if (object && object.constructor === Array) { 41 | for (i = 0, j = object.length; i < j; i++) { 42 | if (callback(i, object[i]) === false) { 43 | break; 44 | } 45 | } 46 | } else if (typeof object === "object") { 47 | for (i in object) { 48 | if (object.hasOwnProperty && object.hasOwnProperty(i)) { 49 | if (callback(i, object[i]) === false) { 50 | break; 51 | } 52 | } else { 53 | if (callback(i, object[i]) === false) { 54 | break; 55 | } 56 | } 57 | } 58 | } 59 | }; 60 | 61 | /** 62 | * 下一次 63 | * @param callback 64 | */ 65 | var nextTick = function (callback) { 66 | setTimeout(callback, 0); 67 | }; 68 | 69 | module.exports = { 70 | task: function () { 71 | if (this.constructor === Howdo) { 72 | return this; 73 | } 74 | 75 | var args = slice.call(arguments); 76 | var howdo = new Howdo(); 77 | 78 | return howdo.task.apply(howdo, args); 79 | }, 80 | each: function () { 81 | if (this.constructor === Howdo) { 82 | return this; 83 | } 84 | 85 | var args = slice.call(arguments); 86 | var howdo = new Howdo(); 87 | 88 | return howdo.each.apply(howdo, args); 89 | } 90 | }; 91 | 92 | 93 | ////////////////////////////////////////////////////////////////////// 94 | /////////////////////////[ constructor ]////////////////////////////// 95 | ////////////////////////////////////////////////////////////////////// 96 | 97 | // 构造函数 98 | function Howdo() { 99 | var the = this; 100 | 101 | the._executed = false; 102 | the._ignoreErr = false; 103 | the._taskIndex = -1; 104 | the._taskList = []; 105 | the._tryCallbackList = []; 106 | the._catchCallbackList = []; 107 | the._contextList = []; 108 | the._allCallback = null; 109 | } 110 | 111 | Howdo.prototype = { 112 | constructor: Howdo, 113 | 114 | /** 115 | * 单次分配任务 116 | * @param {Function} fn 任务函数 117 | * @return Howdo 118 | * @chainable 119 | */ 120 | task: function (fn) { 121 | var the = this; 122 | 123 | if (!isFunction(fn)) { 124 | throw new TypeError('howdo `task` must be a function'); 125 | } 126 | 127 | the._taskIndex++; 128 | the._taskList.push(fn); 129 | the._contextList.push({ 130 | index: the._taskIndex, 131 | task: fn 132 | }); 133 | 134 | return the; 135 | }, 136 | 137 | 138 | /** 139 | * 添加任务回退方法 140 | * @param rollback {Function} 回退、中止方法 141 | * @returns {Howdo} 142 | */ 143 | rollback: function (rollback) { 144 | var the = this; 145 | 146 | if (the._taskIndex > -1 && isFunction(rollback)) { 147 | the._contextList[the._taskIndex].rollback = rollback; 148 | } 149 | 150 | return the; 151 | }, 152 | 153 | 154 | /** 155 | * 添加任务回退方法 156 | * @param abort {Function} 回退、中止方法 157 | * @returns {Howdo} 158 | */ 159 | abort: function (abort) { 160 | var the = this; 161 | 162 | if (the._taskIndex > -1 && isFunction(abort)) { 163 | the._contextList[the._taskIndex].abort = abort; 164 | } 165 | 166 | return the; 167 | }, 168 | 169 | 170 | /** 171 | * 直到 结束 172 | * @param fn {Function} 验证函数 173 | * @param [ignoreError] {Boolean} 是否忽略错误 174 | * @returns {Howdo} 175 | */ 176 | until: function (fn, ignoreError) { 177 | var the = this; 178 | 179 | if (!isFunction(fn)) { 180 | throw new TypeError('until `condition` must be a function'); 181 | } 182 | 183 | the._untilCondition = fn; 184 | the._ignoreErr = ignoreError !== false; 185 | 186 | return the; 187 | }, 188 | 189 | 190 | /** 191 | * 循环分配任务 192 | * @param {Object} object 对象或者数组 193 | * @param {Function} callback 回调 194 | * @return Howdo 195 | */ 196 | each: function (object, callback) { 197 | var howdo = this; 198 | 199 | each(object, task); 200 | 201 | function task(key, val) { 202 | howdo = howdo.task(function () { 203 | var args = [key, val]; 204 | args = args.concat(slice.call(arguments)); 205 | callback.apply(val, args); 206 | }); 207 | } 208 | 209 | return howdo; 210 | }, 211 | 212 | 213 | /** 214 | * 跟着做,任务串行执行 215 | * 链式结束 216 | * @param [callback] {Function} 回调 217 | */ 218 | follow: function (callback) { 219 | var the = this; 220 | 221 | if (the._executed) { 222 | return the; 223 | } 224 | 225 | if (!isFunction(callback)) { 226 | callback = noop; 227 | } 228 | 229 | the._allCallback = callback; 230 | the._executed = true; 231 | 232 | var current = 0; 233 | var count = the._taskList.length; 234 | var args = []; 235 | // 串行回退已成功的任务 236 | var rollbackTask = function () { 237 | each(the._contextList, function (index, context) { 238 | if (!context) { 239 | return false; 240 | } 241 | 242 | if (!context.error && isFunction(context.rollback)) { 243 | context.rollback.call(context); 244 | context.error = null; 245 | } 246 | }); 247 | }; 248 | 249 | nextTick(function () { 250 | if (!count) { 251 | the._fixCallback(); 252 | return the; 253 | } 254 | 255 | (function _follow() { 256 | var fn = function () { 257 | args = slice.call(arguments); 258 | var error = args[0]; 259 | 260 | // has error 261 | if (error && !the._ignoreErr) { 262 | context.error = error; 263 | rollbackTask(); 264 | return the._fixCallback(error); 265 | } 266 | 267 | current++; 268 | 269 | var canStop = false; 270 | 271 | if (the._untilCondition) { 272 | // 一轮任务完成 273 | if (current === count) { 274 | canStop = the._untilCondition.apply(_global, args.slice(1)); 275 | current += canStop ? 0 : -count; 276 | } 277 | } else { 278 | canStop = current === count; 279 | } 280 | 281 | if (canStop) { 282 | rollbackTask(); 283 | the._fixCallback.apply(the, args); 284 | } else if (current < count) { 285 | args.shift(); 286 | _follow(); 287 | } 288 | }; 289 | 290 | args.unshift(fn); 291 | var task = the._taskList[current]; 292 | var context = the._contextList[current]; 293 | task.apply(context, args); 294 | })(); 295 | }); 296 | 297 | return the; 298 | }, 299 | 300 | /** 301 | * 一起做,任务并行执行 302 | * 链式结束 303 | * @param [callback] {Function} 回调 304 | */ 305 | together: function (callback) { 306 | var the = this; 307 | 308 | if (the._executed) { 309 | return; 310 | } 311 | 312 | if (!isFunction(callback)) { 313 | callback = noop; 314 | } 315 | 316 | the._allCallback = callback; 317 | the._executed = true; 318 | 319 | var doneLength = 0; 320 | var count = the._taskList.length; 321 | var taskData = []; 322 | var hasCallback = false; 323 | var i = 0; 324 | 325 | // 中止未完成的 task 326 | var abortTask = function () { 327 | hasCallback = true; 328 | each(the._contextList, function (index, context) { 329 | if (!context.done && isFunction(context.abort)) { 330 | context.abort.call(context); 331 | } 332 | }); 333 | }; 334 | 335 | // 回退已经完成的 task 336 | var rollbackTask = function () { 337 | each(the._contextList, function (index, context) { 338 | if (context.done && isFunction(context.rollback)) { 339 | context.rollback.call(context); 340 | } 341 | }); 342 | }; 343 | 344 | nextTick(function () { 345 | if (!count) { 346 | hasCallback = true; 347 | the._fixCallback(); 348 | return the; 349 | } 350 | 351 | for (; i < count; i++) { 352 | _doTask(i, the._taskList[i]); 353 | } 354 | 355 | function _doTask(index, task) { 356 | var context = the._contextList[index]; 357 | var fn = function () { 358 | if (hasCallback) { 359 | return; 360 | } 361 | 362 | var args = slice.call(arguments); 363 | var ret = []; 364 | var j = 0; 365 | 366 | context.done = true; 367 | 368 | // has Error 369 | if (args[0] && !the._ignoreErr) { 370 | abortTask(); 371 | rollbackTask(); 372 | return the._fixCallback(args[0]); 373 | } 374 | 375 | var canStop = false; 376 | var value = args.slice(1); 377 | 378 | if (the._untilCondition) { 379 | canStop = the._untilCondition.apply(_global, value); 380 | 381 | if (canStop) { 382 | doneLength = count; 383 | } else { 384 | doneLength++; 385 | } 386 | 387 | if (doneLength === count) { 388 | if (canStop) { 389 | ret = ret.concat(value); 390 | } 391 | 392 | ret.unshift(null); 393 | abortTask(); 394 | the._fixCallback.apply(the, ret); 395 | } 396 | } else { 397 | doneLength++; 398 | taskData[index] = value; 399 | 400 | if (doneLength === count) { 401 | for (; j < taskData.length; j++) { 402 | ret = ret.concat(taskData[j]); 403 | } 404 | 405 | ret.unshift(null); 406 | abortTask(); 407 | the._fixCallback.apply(the, ret); 408 | } 409 | } 410 | }; 411 | 412 | task.call(context, fn); 413 | } 414 | }); 415 | 416 | return the; 417 | }, 418 | 419 | /** 420 | * 正常回调 421 | * @param callback 422 | */ 423 | try: function (callback) { 424 | var the = this; 425 | 426 | if (isFunction(callback)) { 427 | the._tryCallbackList.push(callback); 428 | } 429 | 430 | return the; 431 | }, 432 | 433 | /** 434 | * 异常回调 435 | * @param callback 436 | */ 437 | catch: function (callback) { 438 | var the = this; 439 | 440 | if (isFunction(callback)) { 441 | the._catchCallbackList.push(callback); 442 | } 443 | 444 | return the; 445 | }, 446 | 447 | /** 448 | * 修正回调 449 | * @param err 450 | * @private 451 | */ 452 | _fixCallback: function (err/*arguments*/) { 453 | var the = this; 454 | var args = slice.call(arguments, 1); 455 | 456 | the._allCallback.apply(_global, arguments); 457 | 458 | if (err) { 459 | return each(the._catchCallbackList, function (i, callback) { 460 | callback.call(_global, err); 461 | }); 462 | } 463 | 464 | each(the._tryCallbackList, function (i, callback) { 465 | callback.apply(_global, args); 466 | }); 467 | } 468 | }; 469 | 470 | //Howdo.prototype.serial = Howdo.prototype.follow; 471 | //Howdo.prototype.parallel = Howdo.prototype.together; 472 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "howdo", 3 | "version": "3.3.0", 4 | "private": false, 5 | "description": "任务流程控制", 6 | "scripts": { 7 | "test": "node node_modules/mocha/bin/mocha -c --recursive --ui bdd test -t 0" 8 | }, 9 | "preferGlobal": false, 10 | "bin": {}, 11 | "engines": { 12 | "node": ">= 0.10.0" 13 | }, 14 | "main": "howdo.js", 15 | "keywords": [ 16 | "async", 17 | "howdo", 18 | "promise", 19 | "cloudcome", 20 | "ydr.me" 21 | ], 22 | "author": { 23 | "name": "cloudcome", 24 | "email": "cloudcome@163.com", 25 | "url": "http://ydr.me" 26 | }, 27 | "homepage": "https://github.com/cloudcome/nodejs-howdo", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/cloudcome/nodejs-howdo" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/cloudcome/nodejs-howdo/issues" 34 | }, 35 | "dependencies": {}, 36 | "peerDependencies": {}, 37 | "devDependencies": { 38 | "mocha": "^2.3.4" 39 | }, 40 | "license": "MIT" 41 | } 42 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 本项目已停止维护,请使用 plan 代替 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/_utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 文件描述 3 | * @author ydr.me 4 | * @create 2015-11-27 10:21 5 | */ 6 | 7 | 8 | 'use strict'; 9 | 10 | 11 | exports.random = function (min, max) { 12 | return Math.floor(Math.random() * (max - min + 1) + min); 13 | }; 14 | 15 | exports.async = function (value) { 16 | return function (callback) { 17 | var timeout = exports.random(300, 500); 18 | setTimeout(function () { 19 | console.log('[' + timeout + ']', value); 20 | callback(null, value); 21 | }, timeout); 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /test/follow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 文件描述 3 | * @author ydr.me 4 | * @create 2015-11-27 10:23 5 | */ 6 | 7 | 8 | 'use strict'; 9 | 10 | var assert = require('assert'); 11 | var howdo = require('../howdo.js'); 12 | 13 | var utils = require('./_utils.js'); 14 | 15 | 16 | describe('follow', function () { 17 | it('no each', function (done) { 18 | howdo 19 | .task(utils.async(1)) 20 | .task(utils.async(2)) 21 | .task(utils.async(3)) 22 | .task(utils.async(4)) 23 | .follow() 24 | .try(function (value) { 25 | assert.equal(value, 4); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('each', function (done) { 31 | howdo 32 | .each(new Array(4), function (index, value, next) { 33 | utils.async(index + 1)(next); 34 | }) 35 | .follow() 36 | .try(function (value) { 37 | assert.equal(value, 4); 38 | done(); 39 | }); 40 | }); 41 | 42 | it('rollback', function (done) { 43 | var a = 1; 44 | 45 | howdo 46 | .task(function (next) { 47 | a++; 48 | next(); 49 | }) 50 | .rollback(function () { 51 | console.log('rollback', 1); 52 | a--; 53 | }) 54 | .task(function (next) { 55 | setTimeout(function () { 56 | next(new Error('')); 57 | }, 1000); 58 | }) 59 | .follow(function () { 60 | setTimeout(function () { 61 | assert.equal(a, 1); 62 | done(); 63 | }); 64 | }); 65 | }); 66 | }); 67 | 68 | 69 | -------------------------------------------------------------------------------- /test/readme.md: -------------------------------------------------------------------------------- 1 | 单元测试 2 | -------------------------------------------------------------------------------- /test/together.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 文件描述 3 | * @author ydr.me 4 | * @create 2015-11-27 10:23 5 | */ 6 | 7 | 8 | 'use strict'; 9 | 10 | var assert = require('assert'); 11 | var howdo = require('../howdo.js'); 12 | 13 | var utils = require('./_utils.js'); 14 | 15 | 16 | describe('together', function () { 17 | it('no each', function (done) { 18 | howdo 19 | .task(utils.async(1)) 20 | .task(utils.async(2)) 21 | .task(utils.async(3)) 22 | .task(utils.async(4)) 23 | .together() 24 | .try(function (val1, val2, val3, val4) { 25 | assert.equal(val1, 1); 26 | assert.equal(val2, 2); 27 | assert.equal(val3, 3); 28 | assert.equal(val4, 4); 29 | done(); 30 | }); 31 | }); 32 | 33 | it('each', function (done) { 34 | howdo 35 | .each(new Array(4), function (index, value, next) { 36 | utils.async(index + 1)(next); 37 | }) 38 | .together() 39 | .try(function (val1, val2, val3, val4) { 40 | assert.equal(val1, 1); 41 | assert.equal(val2, 2); 42 | assert.equal(val3, 3); 43 | assert.equal(val4, 4); 44 | done(); 45 | }); 46 | }); 47 | 48 | it('rollback', function (done) { 49 | var a = 1; 50 | howdo 51 | .task(function (done) { 52 | this.timeid = setTimeout(function () { 53 | a += 1; 54 | console.log('任务1失败'); 55 | done(new Error(''), 1); 56 | }, 100); 57 | }) 58 | .rollback(function () { 59 | console.log('回退任务1'); 60 | a -= 1; 61 | }) 62 | .abort(function () { 63 | console.log('中止任务1'); 64 | clearTimeout(this.timeid); 65 | }) 66 | .task(function (done) { 67 | this.timeid = setTimeout(function () { 68 | a += 2; 69 | done(null, 2); 70 | }, 200); 71 | }) 72 | .rollback(function () { 73 | console.log('回退任务2'); 74 | a -= 2; 75 | }) 76 | .abort(function () { 77 | console.log('中止任务2'); 78 | clearTimeout(this.timeid); 79 | }) 80 | .task(function (done) { 81 | this.timeid = setTimeout(function () { 82 | a += 3; 83 | done(null, 3); 84 | }, 300); 85 | }) 86 | .rollback(function () { 87 | console.log('回退任务3'); 88 | a -= 3; 89 | }) 90 | .abort(function () { 91 | console.log('中止任务3'); 92 | clearTimeout(this.timeid); 93 | }) 94 | .together(function () { 95 | setTimeout(function () { 96 | assert.equal(a, 1); 97 | done(); 98 | }, 1000); 99 | }); 100 | }); 101 | }); 102 | 103 | 104 | 105 | var a = 1; 106 | 107 | howdo 108 | .task(function(next){ 109 | this.timeid = setTimeout(function(){ 110 | a++; 111 | next(new Error('任务1出错')); 112 | }, 100); 113 | }) 114 | .rollback(function(){ 115 | a--; 116 | }) 117 | .task(function(next){ 118 | this.timeid = setTimeout(function(){ 119 | a++; 120 | next(new Error('任务2出错')); 121 | }, 200); 122 | }) 123 | .abort(function(){ 124 | clearTimeout(this.timeid); 125 | }) 126 | .together(function(){ 127 | console.log(a === 1); 128 | }); -------------------------------------------------------------------------------- /test/until.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 文件描述 3 | * @author ydr.me 4 | * @create 2015-11-27 10:23 5 | */ 6 | 7 | 8 | 'use strict'; 9 | 10 | var assert = require('assert'); 11 | var howdo = require('../howdo.js'); 12 | 13 | var utils = require('./_utils.js'); 14 | 15 | var async1 = function (callback) { 16 | var index = this.index; 17 | var timeout = utils.random(300, 900); 18 | 19 | this.timeId = setTimeout(function () { 20 | var value = Math.random(); 21 | console.log('async1', '[' + index + ']', value); 22 | callback(null, value); 23 | }, timeout); 24 | }; 25 | 26 | var async2 = function (callback) { 27 | var index = this.index; 28 | var timeout = utils.random(300, 900); 29 | 30 | this.timeId = setTimeout(function () { 31 | var value = Math.random(); 32 | console.log('async2', '[' + index + ']', value); 33 | callback(null, value); 34 | }, timeout); 35 | }; 36 | 37 | var abort = function () { 38 | console.log('中止任务', this.index); 39 | clearTimeout(this.timeId); 40 | }; 41 | 42 | describe('until', function () { 43 | it('follow', function (done) { 44 | howdo 45 | .task(async1) 46 | .task(async2) 47 | .until(function (value) { 48 | return value > 0.8; 49 | }) 50 | .follow() 51 | .try(function (value) { 52 | console.log('follow', '=', value); 53 | assert.equal(value > 0.8, true); 54 | done(); 55 | }); 56 | }); 57 | 58 | it('together', function (done) { 59 | howdo 60 | .task(async1) 61 | .abort(abort) 62 | .task(async1) 63 | .abort(abort) 64 | .task(async1) 65 | .abort(abort) 66 | .task(async1) 67 | .abort(abort) 68 | .task(async1) 69 | .abort(abort) 70 | .task(async1) 71 | .abort(abort) 72 | .task(async1) 73 | .abort(abort) 74 | .task(async1) 75 | .abort(abort) 76 | .task(async1) 77 | .abort(abort) 78 | .task(async1) 79 | .abort(abort) 80 | .task(async1) 81 | .until(function (value) { 82 | return value > 0.8; 83 | }) 84 | .together() 85 | .try(function (value) { 86 | if (value) { 87 | console.log('together', '=', value); 88 | assert.equal(value > 0.8, true); 89 | } else { 90 | console.log('together fail'); 91 | } 92 | 93 | setTimeout(function () { 94 | done(); 95 | }, 1000); 96 | }); 97 | }); 98 | }); 99 | 100 | 101 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------