阅读 371

promise 总结篇 并实现一个 promise(then, all, allSettled, race, catch)附详细解释

这里是对我理解的 Promose 的一个总结,如有不恰当的地方,希望指出。
说到 promise 就要知道为啥会出现 promise, promise 到底解决了一个什么问题 (学习一门新技术,最好知道它是怎么诞生的,以及解决了什么问题 :)
简单描述一下:

Promise 解决了什么问题

Promise 解决的是异步编码风格的问题。页面上的任务都是执行在主线程之上的,对于页面来说,主线程就是它的整个世界。在执行一项耗时任务的时候(网络文件下载,获取设备摄像头等设备信息任务),这些任务会被放到页面主线程之外的进程或者线程中去执行。 -- 页面编程的一大特点:异步回调
Web 页面的单线程架构决定了异步回调,而异步回调影响到了我们的编码方式。

Promise 没出现前

在没有 Promise 之前,假设有个下载请求,需要封装异步代码,让处理流程变得线性, 参考下边的伪代码


XFetch(Request('https://abc.com'),
      function resolve(res) {
          console.log(res)
          XFetch(Request('https://abc.com/aPage'),
              function resolve(res) {
                  console.log(res)
                  XFetch(Request('https://abc.com/bPage')
                      function resolve(res) {
                          console.log(res)
                      }, function reject(e) {
                          console.log(e)
                      })
              }, function reject(e) {
                  console.log(e)
              })
      }, function reject(e) {
          console.log(e)
      })
复制代码

可以从上面代码看到嵌套了好几个回调,每个回调都有不确定性。一旦需求变得复杂起来,就会产生问题

  • 任务的不确定性,需要对每个任务的执行结果做两次判断
  • 嵌套多个回调,陷入了回调地狱,代码的可读性也变差了,不方便维护。
  • 多次错误处理

Promise 出现后

先了解下 Promise 的 不可变性:在《你不知道的 JavaScript》一书中说到:“Promise 决议后就是外部不可变的值,我们可以安全的把这个值传递给第三方,并确信它不会被有意无意地修改。” 那么这个不可变性是 Promise 中最重要和最基础的因素。 Promise 出现后到底解决了什么问题呢?

  1. 信任问题。使用回调函数时可能会出现以下问题(不确定性)
  • 调用回调过早
  • 调用回调过晚 (或不被调用)
  • 调用回调次数过少或过多

那么使用了 Promise 之后我们可以明确的知道对 then(...) 的第一个参数来说,毫无疑义,总是处理完成的情况。

  1. 消灭了嵌套调用,实现了线性调用 (使用过 promise 的小伙伴应该都知道这让人愉快的使用方式)
  • 每次你对 Promise 调用 then(...), 它都会创建并返回一个新的 Promise, 我们可以将其链接起来。
  • 不管从 then(...) 调用完成的回调(第一个参数)返回的值是什么,它都会被自动设置为被链接 Promise 的完成
  1. 合并了多个任务的错误处理。
    结合下边的代码理解

function test(resolve, reject) {
    let rand = Math.random();
    if (rand > 0.5) resolve()
    else reject()
}
let p1 = new Promise(test);
let p2 = p1.then((value) => {
    return new Promise(test)
})
let p3 = p2.then((value) => {
    return new Promise(test)
})
p3.catch((error) => {
    console.log("error")
 })
复制代码

上面代码有 3 个 promise 对象, p1 - p3 无论哪个任务抛出了异常,都可以通过最后一个对象 p3.catch 来捕获异常, 这样就解决了每个任务都需要单独处理异常的问题。

以上是对 Promise 一个总结。tip:如果想很详细的了解 Promise,推荐看看《你不知道的 JaveScript》中卷。

实现 promise

先来看看平时我们是怎么使用 promise 的

let p = new Promise((resolve, reject) => {
  let rand = Math.random();
    if (rand > 0.5) {
      resolve('喵喵喵')
    }
    else reject('error')
})
p.then((res) => {
    console.log(res) // 喵喵喵
})
p.catch((error)) => {
    console.log(error) // error
})
复制代码

分析一下结构:

  • myPromise 为一个 构造函数,this 指向 实例
  • 三个 Status: Pending, Resolved, Rejected
  • 参数:一个回调函数 (resolve, reject) => {...}
  • 两个内部函数 resolve(), reject(), 用来决议 status,透穿 value
const PENDING = "pending"
const RESOLVED = "resolved"
const REJECTED = "rejected"
function myPromise(fn) {
  let that = this
  this.status = PENDING
  this.value = null
  this.resolvedCb = [] // 异步状态下 收集 resove cb
  this.rejectedCb = [] // 异步状态下 收集 reject cb 
  function resolve(val) {
    // 避免隐式转换,使用 that 
    if (that.status === PENDING) {
      that.status = RESOLVED
      that.value = val
      // 执行一遍 cb 
      that.resolvedCb.forEach(cb => cb(that.value))
    } // 确定 status 值
  }
  function reject(error) {
    if (that.status === PENDING) {
      that.status = REJECTED
      that.value = error
      that.rejectedCb.forEach(cb => cb(that.value))
    }
  }
  try {
    fn(resolve, reject)
  } catch(e) {
    reject(e)
  }
}
复制代码

实现 myPromise.then(...)

myPromsie.then(onFulfilled, onRejected) 接收两个函数,一个代表完成状态,一个代表 reject 状态。

 myPromise.prototype.then = function(onFulfilled, onRejected) {
  const that = this;
  // 透传值,待会我们测一下透传效果
  onFulfilled = typeof onFulfilled === "function" ? onFulfilled : v => v;
  onRejected = typeof onRejected === "function"  ? onRejected: r => { throw r; };
  // 异步状态下收集 cb
  if (that.status === PENDING) {
    that.resolvedCb.push(onFulfilled);
    that.rejectedCb.push(onRejected);
  }
  if (that.status === RESOLVED) {
    onFulfilled(that.value);
  }
  if (that.status === REJECTED) {
    onRejected(that.value);
  }
  return this;
};
// 测试
let p = new myPromise((resolve, reject) => {
  let rand = Math.random();
    if (rand > 0.5) {
      resolve('success')
    }
    else reject('error')
})
p.then((res) => {
  console.log(res)
},
(error) => {
  console.log(error)
})

// 异步状态下的测试
let p = new myPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 3000)
}).then((res) => {
  console.log(res) // 3 秒后打印 success
})

// 测试透传值
let m = new myPromise((resolve, reject) => {
  setTimeout(() => {
    resolve("可爱的我出现了~");
  }, 1000);
})
  .then()
  .then()
  .then(res => console.log(res)); // 可爱的我出现了
复制代码

实现 myPromise.all(...)

关于promise.all, MDN 上描述是:“Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败 promise 的结果。”
综上得出:

  • promise.all 接收一个 promises 数组
  • promise.all 返回一个 promise 实例
  • promise.all(promises) 是返回当所有 promises 都 resolve 后的 list
  • 其中有一个 promise 失败了,返回 reject 代码实现
myPromise.prototyep.all = function(promises) {
  return new myPromise((resolve, reject) => {
    let res = [];
    let count = 0;
    // 注意 for 里边用 let 定义 i,闭包问题
    for (let i = 0; i < promises.length; i++) {
      promises[i].then(value => {
        count++;
        res[i] = value;
        if (count === promises.length) {
          resolve(res);
        }
      }, error => {
        reject('error')
      });
    }
  });
};
// 测试
let p1 = new myPromise(resolve => {
  setTimeout(() => {
    resolve(1);
  }, 1000);
});
let p2 = new myPromise(resolve => {
  setTimeout(() => {
    resolve(2);
  }, 500);
});
let p3 = new myPromise(resolve => {
  setTimeout(() => {
    resolve(3);
  }, 300);
});
myPromise.all([p1, p2, p3]).then(res => console.log(res)); // [1,2,3]

复制代码

实现 allSettled

MDN 中描述: “该Promise.allSettled()方法返回一个在所有给定的promise已被决议或被拒绝后决议的promise,并带有一个对象数组,每个对象表示对应的promise结果。”

与 promise.all() 对比

promise.all(promises) 是 promises 中有一个promise reject,那么整个promise.all都返回 reject,当我们做数据处理的时候,有时候想更多的展示页面内容,promise.all 可能不是我们的首选。
在 ES2020 中,有了 Promise.allSettled,它很好的解决了 promise.all 的痛点,如定义是返回了所有的 promises list,无论它是 reslove 还是 reject (tip: 使用时注意浏览器的兼容性)
结合代码看一下

 Promise.allSettled([
 Promise.reject({ code: 500, msg: '服务异常' }),
 Promise.resolve({ code: 200, list: [] }),
 Promise.resolve({ code: 200, list: [] })
 ]).then(res => {
   console.log(res)
 /*
       0: {status: "rejected", reason: {…}}
       1: {status: "fulfilled", value: {…}}
       2: {status: "fulfilled", value: {…}}
   */
 // 过滤掉 rejected 状态,尽可能多的保证页面区域数据渲染
 RenderContent(
   res.filter(el => {
     return el.status !== 'rejected'
   })
 )
})
复制代码

接下来结合一个图片 list 加载实现一下,其实也可以当作不兼容 promise.allSettled 浏览器的 polyfill

var urls = ['https://www.kkkk1000.com/images/getImgData/getImgDatadata.jpg', 'https://www.kkkk1000.com/images/getImgData/gray.gif', 'https://www.kkkk1000.com/images/getImgData/Particle.gif', 'https://www.kkkk1000.com/images/getImgData/arithmetic.png', 'https://www.kkkk1000.com/images/getImgData/arithmetic2.gif', 'https://www.kkkk1000.com/images/getImgData/getImgDataError.jpg', 'https://www.kkkk1000.com/images/getImgData/arithmetic.gif', 'https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2018/10/29/166be40ccc434be0~tplv-t2oaga2asx-image.image'];

function allSettled(iterable) {
 return new Promise((resolve, reject) => {
   
   function addElementToResult(i, elem) {
     result[i] = elem;
     elementCount++;
     if (elementCount === result.length) {
       resolve(result);
     }
   }
   let index = 0;
   重写包装一下返回结果,加入返回状态
   for (const promise of iterable) {
     const currentIndex = index;
     promise.then(
       (value) => addElementToResult(
         currentIndex, {
           status: 'fulfilled',
           value
         }),
       (reason) => addElementToResult(
         currentIndex, {
           status: 'rejected',
           reason
         }));
     index++;
   }
   if (index === 0) {
     resolve([]);
     return;
   }
   let elementCount = 0;
   const result = new Array(index);
 });
}

function loadImg(url) {
   return new Promise((resolve, reject) => {
       const img = new Image()
       img.onload = function () {
           console.log('一张图片加载完成');
           resolve(url);
       }
       img.onerror = reject
       img.src = url
   })
};
let arr = urls.map(url => loadImg(url))
allSettled(arr).then(res => {
console.log("resolve", res)
})
复制代码

实现 race

MDN 中描述 :“Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。”

const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two');
});

Promise.race([promise1, promise2]).then((value) => {
  console.log(value);
  // Both resolve, but promise2 is faster
});
// expected output: "two"
复制代码

综上:一旦 promises list 中有值 resolve 就会输出(谁快谁输出)

  const myRace = function (promises) {
  if (!Array.isArray(arr)) {
    throw new Error('promises not a Array')
  }
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promises.length; i++) {
      Promise.resolve(promises[i]).then((value) => {
        resolve(value);
        return;
      },  //哪个 then 先捕捉到,那个先返回。
      (err) => {
        reject(err)
        return;
      }
      )
    }
  })
}
// 测试
let p1 = new myPromise(resolve => {
  setTimeout(() => {
    resolve(1);
  }, 1000);
});
let p2 = new myPromise(resolve => {
  setTimeout(() => {
    resolve(2);
  }, 500);
});
let p3 = new myPromise(resolve => {
  setTimeout(() => {
    resolve(3);
  }, 300);
});
myRace([p1, p2, p3]).then(res => console.log(res)); // 3
复制代码

差点忘记的 catch

一口气写了这么多,差点忘记实现 catch 了,道理都懂,那就直接上代码好了,顺便结合一下之前写的 promise

const PENDING = "pending"
const RESOLVED = "resolved"
const REJECTED = "rejected"
function myPromise(fn) {
  let that = this
  this.status = PENDING
  this.value = null
  this.resolvedCb = [] // 异步状态下 收集 resove cb
  this.rejectedCb = [] // 异步状态下 收集 reject cb 
  function resolve(val) {
    // 避免隐式转换,使用 that 
    if (that.status === PENDING) {
      that.status = RESOLVED
      that.value = val
      // 执行一遍 cb 
      that.resolvedCb.forEach(cb => cb(that.value))
    } // 确定 status 值
  }
  function reject(error) {
    if (that.status === PENDING) {
      that.status = REJECTED
      that.value = error
      that.rejectedCb.forEach(cb => cb(that.value))
    }
  }
  try {
    fn(resolve, reject)
  } catch(e) {
    reject(e)
  }
}
// .then
 myPromise.prototype.then = function(onFulfilled, onRejected) {
  const that = this;
  // 透传值,待会我们测一下透传效果
  onFulfilled = typeof onFulfilled === "function" ? onFulfilled : v => v;
  onRejected = typeof onRejected === "function"  ? onRejected: r => { throw r; };
  // 异步状态下收集 cb
  if (that.status === PENDING) {
    that.resolvedCb.push(onFulfilled);
    that.rejectedCb.push(onRejected);
  }
  if (that.status === RESOLVED) {
    onFulfilled(that.value);
  }
  if (that.status === REJECTED) {
    onRejected(that.value);
  }
  return this;
};

// catch
myPromise.prototype.catch = function(errFn) {
  errFn =
    typeof errFn === "function"
      ? errFn
      : r => {
          throw r;
        };
  const that = this;
  if (that.status === PENDING) {
    this.rejectedCb.push(errFn);
  }
  if (that.status === REJECTED) {
    errFn(that.value);
  }
  return this;
};
 let p = new myPromise((resolve, reject) => {
   reject('错了吧')
 })
 p.catch((err => {
   console.log('知道', err)
 }))
复制代码

总结

这里就不写怎么实现 promise A+ 了,累 0.0
好了,终于写完了,不管怎样,学一个东西都要了解一下它的原理。希望大家看完之后能够帮到你们,然后实现一个自己的 promise 怎么样 :)

文章分类
前端
文章标签