前言
面试过程中经常有围绕 Promise 的 高频问题比如:
- Promise 解决了什么问题?
- 能不能手写一个符合 Promise/A+ 规范的 Promise?
- Promise 在事件循环中的执行过程是怎样的?
- Promise 有什么缺陷,可以如何解决?
那我们今天就围绕第二问题展开 “ 手写一个符合 Promise/A+ 规范的 Promise? “
记住我们之前所学习的概念
- promise表示一个异步操作的最终结果。
- Promise 并不是通过移除回调来解决 “回调地狱” 的问题。
- Promise 所做的只是改变了你传递回调的地方。提供中立 Promise 机制,你就能重新获得了程序的控制权。
什么是Promise/A+ 规范?
简单说它就是一个开放标准,对于开发人员可互操作的 JavaScript promise。
要求
Promise 状态
1.1 Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
1.2 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。
then 方法
2.1 一个 promise 必须提供一个 then 方法,用来获取当前异步操作的 value 或 error。
2.2 一个 promise 的 then 方法接受两个参数:promise.then(onFulfilled, onRejected)。
2.3. then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
getJSON("/posts.json").then(function(json) {return json.post;}).then(function(post) {// ...});
上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。
2.4 onFulfilled, onRejected有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。
getJSON("/post/1.json").then(function(post) {return getJSON(post.commentURL);}).then(function (comments) {console.log("resolved: ", comments);}, function (err){console.log("rejected: ", err);});
上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为resolved,就调用第一个回调函数,如果状态变为rejected,就调用第二个回调函数。
那么满足了以上必要条件的对象我们称为符合 Promise/A+ 规范的 Promise 。
我们自己要写一个Promise,先从什么地方开始呢?
创建一个容器
思维导图
有两个工具函数来操作这个容器。
add 往容器中添加回调函数。
startup 依次去调用容器(回调列表)中的回调函数。
源代码
var cache = {};var container = function(flags){//处理 flags {stopOnFalse: true} flags == undefinedflags = typeof flags == "string" ? (cache[flags] || createflags(flags)):extend({}, flags);var i,len,carryOut,memory,startPoint,stackLen;var stack = [];var fire = function(data){memory = flags.memory && data;carryOut = true;len = stack.length;i = startPoint || 0;startPoint = 0;for(; i<len; i++){//stack[i].apply(data[0], data[1]);//支持的Flags参数if(stack[i].apply(data[0], data[1]) === false && flags.stopOnFalse){break;}}}var self = {add: function(){(function add(args){stackLen = stack.length;//类数组转化成数组对象 遍历获取参数Array.from(args).slice().forEach(function(arg){if(toString.call(arg) === '[object Function]'){//检测是否有重复if(!self.has( arg )){stack.push( arg );}} else if( arg && arg.length && typeof arg !== "string"){ //支持[()=>{...},()=>{...}]add( arg );}});})(arguments);if(memory){startPoint = stackLen;fire(memory);}},has: function(fn){return stack.indexOf(fn) > -1;},//绑定thisstartupWith: function(context, args){args = args || [];args = [ context, args.slice ? args.slice() : args ];//检测回调列表是否只执行一次if(!flags.once || !carryOut){fire(args);}},//使用给定的上下文和参数调用所有回调startup: function(){self.startupWith(this, arguments);}}return self;}function createflags(flags){var res ={};//用于在空白处拆分/\S+/g(flags.match(/\S+/g) || []).forEach(function(flag){res[flag] = true;});return res;}function extend(to, from){for(var key in from){to[key] = from[key];}return to;}
疑惑
startup 为什么不独立调用要写在 self 里面?
原因是整个container 是一个为promise对象服务的基础工具函数。当异步操作的状态绑定, resolve(value); reject(error); 需要给指定的回调传参。如果我们把 startup 设置为独立在 container 作用域中的局部函数那么函数体外的执行环境将无法访问。
resolve 语法糖 本质启动调用 startup
reject 语法糖 本质启动调用 startup

为什么要还要封装 startupWith 方法?
原因是this 绑定, 在回调函数中有可能还出出现需要往容器中添加回调的操作。 或者访问当前异步操作的状态。
这时候可以通过this 访问到当前使用的 self 对象 (上下文对象)。
什么是Flags?
flags 参数是 $.Callbacks() 的一个可选参数, 结构为一个用空格标记分隔的标志可选列表用来改变回调列表中的行为。
例如:
container( 'unique stopOnFalse' )
- once 确保这个回调列表只执行一次
- memory 缓存上一次回调列表的下标值, 当再次添加回调函数时,直接用上一次的下标值立刻调用新加入的回调函数
- unique 一个回调只会被添加一次,不会重复添加
- stopOnFalse 某个回调函数返回false之后中断后面的回调函数
实践应用
```javascript var con = container(); var fn = function(){ console.log(1); };
con.add(fn); con.add([function(){ console.log(2); }]);
con.startup(); // 1 2
con.add([function(){ console.log(3); }]);
con.startup(); // 1 2 3 ```
当配置了 once 回调列表只会调用一次 输出 1 2
当为配置 once 回调列表只会调用二次 输出 1 2 , 1 2 3
问题: 缓存上一次回调列表的下标值, 当再次添加回调函数时,直接用上一次的下标值立刻调用新加入的回调函数,通过配置memory 处理。
