黑马程序员技术交流社区

标题: 【上海校区】啥?喝着阔落吃着西瓜就把Promise手写出来了... [打印本页]

作者: 不二晨    时间: 2018-7-31 09:40
标题: 【上海校区】啥?喝着阔落吃着西瓜就把Promise手写出来了...
Promise
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。
嗝~~~~~
首先,我们通过字面可以看出来Pormise是一种解决方案,而且还有两种传统的解决方案·回调函数和事件,ok,那么我们就来先聊聊这两种方案。
回调函数 Callback回调函数想必大家都不陌生,就是我们常见的把一个函数当做参数传递给另外一个函数,在满足了一定的条件之后再去执行回调,比如我们想要实现一个在三秒后去计算1到5的和,那么:
    // 求和函数    function sum () {        return eval([...arguments].join('+'))    }    // 三秒后执行函数    function asycnGetSum (callback) {        setTimeout(function(){            var result = callback(1,2,3,4,5);            console.log(result)        },3000)    }    asyncGetSum(sum);复制代码这样的实现就是回调函数,但是如果我要实现在一段动画,动画的执行过程是小球先向右移动100px,然后再向下移动100px,在向左移动100px,每段动画持续时间都是3s.
    dom.animate({left:'100px'},3000,'linear',function(){        dom.animate({top:'100px'},3000,'linear',function(){            dom.animate({left:'0px'},3000,'linear',function(){                console.log('动画 done')            })        })    })复制代码这样就会看到形成了一个回调嵌套,也就是我们常说的回调地狱,导致代码可读性十分差。
事件事件处理就是jQuery中的on绑定事件和trigger触发事件,其实就是我们常见的发布订阅模式,当我订阅了一个事件,那么我就是订阅者,如果发布者发布了数据之后,那么我就要收到相应的通知。
    // 定义一个发布中心    let publishCenter = {        subscribeArrays:{}, // 定义一个订阅者回调函数callback        subscribe:function(key,callback){            // 增加订阅者            if(!this.subscribeArrays[key]){                this.subscribeArrays[key] = [];            }            this.subscribeArrays[key].push(callback)        },        publish:function(){            //发布 第一个参数是key            let params = [...arguments];            let key = params.shift();            let callbacks = this.subscribeArrays[key];            if(!callbacks || callbacks.length === 0){                // 如果没人订阅 那么就返回                return false            }            for( let i = 0 ; i < callbacks.length; i++ ){                callbacks.apply( this, params );            }        }    };        // 订阅 一个wantWatermelon事件    publishCenter.subscribe('wantWatermelon',function(){console.log('恰西瓜咯~~')})        //触发wantWatermelon事件 好咯 可以看到 恰西瓜咯    publishCenter.publish('wantWatermelon')复制代码
恰西瓜中~~~
Promise A+嗝~ok,吃完我们进入正题,看到上面异步编程如此如此如此麻烦,对于我这种头大用户,当然是拒绝的啊,还好我们有Pormise(Pormise大法好),下面我们就来通过实现一个Promise去更深的了解Promise的原理,首先我们了解一下PromiseA+,它是一种规范,用来约束大家写的Promise方法的,为了让大家写的Promise杜绝一些错误,按照我们所期望的流程来走,因此就出现了PromiseA+规范。
Promise特点我们根据PromiseA+文档来一步一步的看Promise有什么特点。
首先我们看文档的2.1节,题目是Promise states,也就是说讲的是Promise的状态,那么都说了些什么呢,我们来看一哈:
ok,那么我们就开始写我们自己的Promise,我们先看看一段正常Promise的写法
    // 成功或者失败是需要提供一个value或者reason    let promise1 = new Promise((resolve,rejected)=>{        // 可以发现 当我们new Promise的时候这句话是同步执行的 也就是说当我们初始化一个promise的时候 内部的回调函数(通常我们叫做执行器executor)会立即执行        console.log('hahahha');        // promise内部支持异步        setTimeout(function(){            resolve(123);        },100)        // throw new Error('error') 我们也可以在执行器内部直接抛出一个错误 这时promise会直接变成rejected态    })    复制代码根据我们上面的代码还有PromiseA+规范中的状态说明,我们可以知道Promise已经有了下面几个特点
我们接下来继续看PromiseA+文档:
好吧,我们总结了这么多规范特点,那么我们就用这些先来练练手
    /**     * 实现一个PromiseA+     * @description 实现一个简要的promise     * @param {Function} executor 执行器     * @author Leslie     */    function Promise(executor){        let self = this;        self.status = 'pending'; // 存储promise状态 pending fulfilled rejected.        self.value = undefined; // 存储成功后的值        self.reason = undefined; // 记录失败的原因        self.onfulfilledCallbacks = []; //  异步时候收集成功回调        self.onrejectedCallbacks = []; //  异步时候收集失败回调        function resolve(value){            if(self.status === 'pending'){                self.status = 'fulfilled';// resolve的时候改变promise的状态                self.value = value;//修改成功的值                // 异步执行后 调用resolve 再把存储的then中的成功回调函数执行一遍                self.onfulfilledCallbacks.forEach(element => {                    element()                });            }        }        function reject(reason){            if(self.status === 'pending'){                self.status = 'rejected';// reject的时候改变promise的状态                self.reason = reason; // 修改失败的原因                // 异步执行后 调用reject 再把存储的then中的失败回调函数执行一遍                self.onrejectedCallbacks.forEach(element => {                    element()                });            }        }        // 如果执行器中抛出异常 那么就把promise的状态用这个异常reject掉        try {            //执行 执行器            executor(resolve,reject);        } catch (error) {            reject(error)        }    }    Promise.prototype.then = function(onfulfilled,onrejected){        // onfulfilled then方法中的成功回调        // onrejected then方法中的失败回调        let self = this;        // 如果onfulfilled不是函数 那么就用默认的函数替代 以便达到值穿透        onfulfilled = typeof onfulfilled === 'function'?onfulfilled:val=>val;        // 如果onrejected不是函数 那么就用默认的函数替代 以便达到值穿透        onrejected = typeof onrejected === 'function'?onrejected: err=>{throw err}        let promise2 = new Promise((resolve,reject)=>{            if(self.status === 'fulfilled'){                // 加入setTimeout 模拟异步                // 如果调用then的时候promise 的状态已经变成了fulfilled 那么就调用成功回调 并且传递参数为 成功的value                setTimeout(function(){                    // 如果执行回调发生了异常 那么就用这个异常作为promise2的失败原因                    try {                        // x 是执行成功回调的结果                        let x = onfulfilled(self.value);                        // 调用resolvePromise函数 根据x的值 来决定promise2的状态                        resolvePromise(promise2,x,resolve,reject);                    } catch (error) {                        reject(error)                    }                },0)                            }                    if(self.status === 'rejected'){                // 加入setTimeout 模拟异步                // 如果调用then的时候promise 的状态已经变成了rejected 那么就调用失败回调 并且传递参数为 失败的reason                setTimeout(function(){                    // 如果执行回调发生了异常 那么就用这个异常作为promise2的失败原因                    try {                        // x 是执行失败回调的结果                        let x = onrejected(self.reason);                         // 调用resolvePromise函数 根据x的值 来决定promise2的状态                        resolvePromise(promise2,x,resolve,reject);                    } catch (error) {                        reject(error)                    }                                    },0)            }                    if(self.status === 'pending'){                //如果调用then的时候promise的状态还是pending,说明promsie执行器内部的resolve或者reject是异步执行的,那么就需要先把then方法中的成功回调和失败回调存储袭来,等待promise的状态改成fulfilled或者rejected时候再按顺序执行相关回调                self.onfulfilledCallbacks.push(()=>{                    //setTimeout模拟异步                    setTimeout(function(){                        // 如果执行回调发生了异常 那么就用这个异常作为promise2的失败原因                        try {                             // x 是执行成功回调的结果                            let x = onfulfilled(self.value)                            // 调用resolvePromise函数 根据x的值 来决定promise2的状态                            resolvePromise(promise2,x,resolve,reject);                        } catch (error) {                            reject(error)                        }                    },0)                })                self.onrejectedCallbacks.push(()=>{                    //setTimeout模拟异步                    setTimeout(function(){                        // 如果执行回调发生了异常 那么就用这个异常作为promise2的失败原因                        try {                             // x 是执行失败回调的结果                            let x = onrejected(self.reason)                             // 调用resolvePromise函数 根据x的值 来决定promise2的状态                            resolvePromise(promise2,x,resolve,reject);                        } catch (error) {                            reject(error)                        }                    },0)                })            }        })        return promise2;    }复制代码一气呵成,是不是觉得之前总结出的特点十分有效,对着特点十分顺畅的就撸完了代码~
那么就让我们接着来看看promiseA+文档里还有些什么内容吧
我们又又又又又又总结了这么多,好吧不说了总结多少就开撸吧。
/** * 用来处理then方法返回结果包装成promise 方便链式调用 * @param {*} promise2 then方法执行产生的promise 方便链式调用 * @param {*} x then方法执行完成功回调或者失败回调后的result * @param {*} resolve 返回的promise的resolve方法 用来更改promise最后的状态 * @param {*} reject 返回的promise的reject方法 用来更改promise最后的状态 */function resolvePromise(promise2,x,resolve,reject){    // 首先判断x和promise2是否是同一引用 如果是 那么就用一个类型错误作为Promise2的失败原因reject    if( promise2 === x) return reject(new TypeError('typeError:大佬,你循环引用了!'));    // called 用来记录promise2的状态改变,一旦发生改变了 就不允许 再改成其他状态    let called;    if( x !== null && ( typeof x === 'object' || typeof x === 'function')){        // 如果x是一个对象或者函数 那么他就有可能是promise 需要注意 null typeof也是 object 所以需要排除掉        //先获得x中的then 如果这一步发生异常了,那么就直接把异常原因reject掉        try {            let then = x.then;//防止别人瞎写报错            if(typeof then === 'function'){                //如果then是个函数 那么就调用then 并且把成功回调和失败回调传进去,如果x是一个promise 并且最终状态时成功,那么就会执行成功的回调,如果失败就会执行失败的回调如果失败了,就把失败的原因reject出去,做为promise2的失败原因,如果成功了那么成功的value时y,这个y有可能仍然是promise,所以需要递归调用resolvePromise这个方法 直达返回值不是一个promise                then.call(x,y => {                    if(called) return;                    called = true;                    resolvePromise(promise2,y,resolve,reject)                }, error=>{                    if(called) return                    called = true;                    reject(error)                })            }else{                resolve(x)            }        } catch (error) {            if(called) return            called = true;            reject(error)        }    }else{        // 如果是一个普通值 那么就直接把x作为promise2的成功value resolve掉        resolve(x)    }}复制代码finnnnnnnnnally,我们终于通过我们的不懈努力实现了一个基于PromiseA+规范的Promise!
最后呢为了完美,我们还要在这个promise上实现Promise.resolve,Promise.reject,以及catch,Promise.all和Promise.race这些方法。
Promise的一些方法Promise.resolve = function(value){    return new Promise((resolve,reject)=>{        resolve(value)    })}Promise.reject = function(reason){    return new Promise((resolve,reject)=>{        reject(reason)    })}Promise.prototype.catch = function(onRejected){    return this.then(null,onRejected)}Promise.all = function(promises){    return new Promise((resolve,reject)=>{        let arr = [];        let i = 0;        function getResult(index,value){            arr[index] = value;            if(++i == promises.length) {                resolve(arr)            }        }        for(let i = 0;i<promises.length;i++){            promises.then(data=>{                getResult(i,data)            },reject)        }    })}Promise.race = function(promises){    return new Promise((resolve,reject)=>{        for(let i = 0 ; i < promises.length ; i++){            promises.then(resolve,reject)        }    })}复制代码Promise 语法糖恰完西瓜来口糖,语法糖是为了让我们书写promise的时候能够更加的快速,所以做了一层改变,我们来看一个例子,比如当我们封装一个异步读取图片的宽高函数
    // 原来的方式    let getImgWidthHeight = function(imgUrl){        return new Promise((resolve,reject)=>{            let img = new Image();            img.onload = function(){                resolve(img.width+'-'+img.height)            }            img.onerror = function(e){                reject(e)            }            img.src = imgUrl;        })    }复制代码是不是觉得怎么写起来有点舒服但又有点不舒服,好像我每次都要去写执行器啊!为什么!好的,没有为什么,既然不舒服 我们就改!
// 实现一个promise的语法糖Promise.defer = Promise.deferred = function (){    let dfd = {};    dfd.promise = new Promise((resolve,reject)=>{        dfd.resolve = resolve;        dfd.reject = reject;    })    return dfd}复制代码有了上面的语法糖我们再看一下那个图片的函数怎么写
    let newGetImgWidthHeight = function(imgUrl){        let dfd = Promise.defer();        let img = new Image();        img.onload = function(){            dfd.resolve(img.width+'-'+img.height)        }        img.onerror = function(e){            dfd.reject(e)        }        img.url = imgUrl;        return dfd.promise    }复制代码是不是发现我们少了一层函数嵌套,呼~~ 得劲~~
最终检测npm install promises-aplus-tests -g复制代码既然我们都说了我们是遵循promiseA+规范的,那至少要拿出点证据来是不是,不然是不是说服不了大家,那么我们就用promises-aplus-tests这个包来检测我们写的promise究竟怎么样呢!安装完成之后来跑一下我们的promise


最终跑出来我们全部通过测试!酷!


作者:LeslieMay
链接:https://juejin.im/post/5b596449f265da0f47352989




作者: wuqiong    时间: 2018-7-31 10:28

作者: 不二晨    时间: 2018-7-31 11:53
奈斯,很赞
作者: 梦缠绕的时候    时间: 2018-7-31 11:56





欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2