一步一步实现基于Task的Promise库(一)Promise的基本实现
如果我们现在有一个需求,大概是先读取一个文件的内容,再把得到的内容传给后台去解析,最后把解析后的结果再保存到那个文件,按照最原始的做法代码就是下面这个样子的:
1 //读取文件的原始内容 2 var readFile = function(fileName, callback){ 3 window.setTimeout(function(){ 4 console.log("read '" + fileName + "' complete."); 5 var rawContent = "..... content ......"; 6 if(callback){ 7 callback(rawContent); 8 } 9 }, 2000); 10 }; 11 //请求服务器来解析原始内容,得到真正的内容 12 var resolveFile = function(serverUrl, rawContent, callback){ 13 window.setTimeout(function(){ 14 console.log("resolve complete."); 15 var realContent = "..... 内容 ....."; 16 if(callback){ 17 callback(realContent); 18 } 19 }, 1000); 20 }; 21 //把真正的内容写入一开始的文件 22 var writeBack = function(fileName, realContent, callback){ 23 window.setTimeout(function(){ 24 console.log("writeBack complete."); 25 if(callback){ 26 callback(); 27 } 28 }, 2000); 29 }; 30 readFile("aa.txt", function(rawContent){ 31 resolveFile("/service/fileResolve.ashx", rawContent, function(realContent){ 32 writeBack("aa.txt", realContent, function(){ 33 //给个完工的通知 34 alert("everything is ok."); 35 }); 36 }); 37 });
这里我全部采用window.setTimeout来模拟一个异步操作,然而这种嵌套回调方法的做法看起来非常丑陋,如果能改掉嵌套的形式,采用链式调用会美观很多。因此我们期望的调用形式是下面这个样子:
1 //期望的调用形式(一) Promise的基本实现 2 var taskExp1_1 = new Task(readFile, ["aa.txt"]) 3 .then(resolveFile, ["/service/fileResolve.ashx"]) 4 .then(writeBack, ["aa.txt"]) 5 .then(function(){ 6 alert("everything is ok."); 7 this.end(); 8 }) 9 //do方法才是正真的执行这一组异步调用,不调用do方法相当于只是配置一组异步调用 10 .do();
这种异步方法的链式调用实际上就是一个Promise的实现,只不过这里是通过一个Task类去完成的,我们的目标就是实现这个Task类,它包含了一组有先后逻辑依赖的异步操作,then方法里面传递的function并不会因为then的执行而执行,实际上then方法可以看做是对一组有先后逻辑依赖的异步操作的一个配置,真正导致执行的是do方法,调用do方法会从这个队列的头部开始调用,而标致一个异步操作的结束是在异步操作方法里面,看下面的代码:
1 //读取文件的原始内容 2 var readFile = function(fileName){ 3 var _this = this; 4 window.setTimeout(function(){ 5 var rawContent = "xxxxxxxx (" + fileName + ")"; 6 console.log("read '" + fileName + "' complete. rawContent is " + rawContent); 7 //告知异步调用已经完成 8 _this.end(rawContent); 9 }, 2000); 10 };
我们稍微对readFile方法做了修改,当文件读取完成的时候调用this.end方法通知异步操作的完成,这样Task就知道该进行下一个异步操作了,就会执行resolveFile方法,那么这里有一个问题就是readFile方法需要传递一个参数rawContent给resolveFile方法,可以看到this.end(rawContent);这句代码已经有传递,resolveFile方法如何接收呢?
1 //请求服务器来解析原始内容,得到真正的内容 2 var resolveFile = function(serverUrl){ 3 var _this = this; 4 //可以从params属性中获取上一个异步调用传递过来的参数 5 var rawContent = _this.params; 6 window.setTimeout(function(){ 7 var realContent = "Greeting (" + serverUrl + ")"; 8 console.log("resolve file complete. realContent is " + realContent); 9 _this.end(realContent); 10 }, 1000); 11 };
可以看到resolveFile方法通过this.params接收readFile的输出参数。
到目前为止,我们看到的都是如何使用Task类,那么我们最希望有一个什么样的库来完成这种逻辑配置关系呢? 除了上面说的传参问题,还有一个就是我希望每一个异步操作都可以接收一些形参,这样我们使用Task类的时候就不用自己拐弯抹角的塞参数了,否则我们可能要这样写:
1 var taskExp1_1 = new Task(function (){ 2 readFile.call(this, "aa.txt"); 3 }).then(function (){ 4 resolveFile.call(this, "/service/fileResolve.ashx"); 5 }).then(function (){ 6 writeBack.call(this, "aa.txt"); 7 }).then(function () { 8 alert("everything is ok."); 9 this.end(); 10 }) 11 .do();
下面是整个demo和Task类的实现细节:
1 <script type="text/javascript"> 2 //读取文件的原始内容 3 var readFile = function(fileName){ 4 var _this = this; 5 window.setTimeout(function(){ 6 var rawContent = "xxxxxxxx (" + fileName + ")"; 7 console.log("read '" + fileName + "' complete. rawContent is " + rawContent); 8 _this.end(rawContent); 9 }, 2000); 10 }; 11 //请求服务器来解析原始内容,得到真正的内容 12 var resolveFile = function(serverUrl){ 13 var _this = this; 14 var rawContent = _this.params; 15 window.setTimeout(function(){ 16 var realContent = "Greeting (" + serverUrl + ")"; 17 console.log("resolve file complete. realContent is " + realContent); 18 _this.end(realContent); 19 }, 1000); 20 }; 21 //把真正的内容写入一开始的文件 22 var writeBack = function(fileName){ 23 var _this = this; 24 var realContent = _this.params; 25 window.setTimeout(function(){ 26 console.log("writeBack complete."); 27 _this.end(); 28 }, 2000); 29 }; 30 var WorkItem = function(func, args){ 31 return { 32 //表示执行此异步操作的先决条件 33 condition: "", 34 //表示当前异步操作是否执行完了 35 isDone: false, 36 //正真的执行 37 'do': function(context){ 38 func.call(context, args); 39 } 40 }; 41 }; 42 var Task = function(func, args){ 43 //Task内部会维护一个异步方法的队列,此队列严格按照先后顺序执行 44 var wItemQueue = []; 45 //当前异步方法 46 var currentItem; 47 //执行异步方法前要判断的先决条件集合(目前只有then) 48 var condition = { 49 //直接执行 50 then: function(workItem){ 51 return true; 52 } 53 }; 54 //初始化一个异步操作,这个方法主要处理接受参数的多样性 55 var _initWorkItem = function(func, args, condition){ 56 if(func instanceof Task){ 57 return null; 58 } 59 else{ 60 return _enqueueItem(new WorkItem(func, args), condition); 61 } 62 }; 63 //记录异步操作的先决条件,并添加到队列中去 64 var _enqueueItem = function(item, condition){ 65 if(condition){ 66 item.condition = condition; 67 } 68 wItemQueue.push(item); 69 return item; 70 }; 71 //试着执行下一个异步操作,如果这个操作满足他的先决条件,那就执行 72 var _tryDoNextItem = function(context){ 73 var next = _getCurNextItem(); 74 if(next){ 75 if(condition[next.condition](next)){ 76 currentItem = next; 77 currentItem.do(context); 78 } 79 } 80 }; 81 //获取下一个异步操作,如果已经是最后一个了返回undefined 82 var _getCurNextItem = function(){ 83 var i=0; 84 for(; i<wItemQueue.length; i++){ 85 if(currentItem == wItemQueue[i]){ 86 break; 87 } 88 } 89 return wItemQueue[i + 1]; 90 }; 91 //定义异步操作的上下文环境 92 var Context = function(){}; 93 Context.prototype = { 94 //上一个异步调用传递过来的参数 95 'params': null, 96 //执行此方法就表示当前异步操作已经完成,那么会尝试执行下一个异步操作 97 end: function(output){ 98 currentItem.isDone = true; 99 this.params = output; 100 _tryDoNextItem(this); 101 return this; 102 } 103 }; 104 currentItem = _initWorkItem(func, args); 105 106 //Task的公共方法,这些方法都应该支持链式调用(都返回this) 107 return { 108 //开始执行 109 'do': function(){ 110 if(currentItem && currentItem.condition == ""){ 111 currentItem.do(new Context()); 112 } 113 return this; 114 }, 115 //配置下一个异步操作 116 then: function(func, args){ 117 _initWorkItem(func, args, 'then'); 118 return this; 119 } 120 }; 121 }; 122 123 var taskExp_1 = new Task(readFile, ["aa.txt"]) 124 .then(resolveFile, ["/service/fileResolve.ashx"]) 125 .then(writeBack, ["aa.txt"]) 126 .then(function(){ 127 alert("everything is ok."); 128 this.end(); 129 }) 130 .do(); 131 </script>