JavaUser 发表于 2021-3-19 16:40

【笔记】面试: 来, 手写一个promise

本帖最后由 JavaUser 于 2021-3-19 16:49 编辑

在手写我们喜闻乐见的promise之前, 我们先来系统的梳理一下异步编程的概念, 异步编程是JavaScript中一个极为重要的概念, 但是JavaScript的异步编程,也比较特殊, 我们们一般称之为"单线程异步编程"
JS单线程异步编程
JS是单线程的


也就是说, JavaScript同时只能处理一件事情,"上面的代码未执行完, 则无法执行下面的代码"

JavaScript中也有部分操作是异步编程



但是JavaScript绝非我们传统的编程中的多线程开发, 可以同时做多件事情, 因为是单线程处理, 所以实际上, 如果JavaScript引擎正在处理任务, 那其他事情他还是处理不了的.


浏览器基于EventQueue事件队列, EventLoop事件循环 两大机制, 构建出了"异步编程的效果"


「异步宏任务」

定时器
DOM事件
HTTP请求(ajax、fetch、jsonp...)
...

「异步微任务」

promise「resolve/reject/then...」
async await
requestAnimationFrame
...

进入异步队列(EventQueue)的查找顺序:

首先去异步微任务队列查找:


找到,到了可执行条件的异步微任务, 则把其挪至ESStack中, 交给主线程执行(主线程忙碌)
主线程再次空闲, 则按照上述方式查找
如果一次发现多个任务可执行, 则最先达到可执行阶段, 就谁先执行
如果没有可执行的异步微任务(不代表没有异步微任务), 则去阶段2继续查找


再去异步宏任务队列中查找:


和阶段1的查找方式类似


事件循环机制(EventLoop):


同步任务执行完毕 ⇒ 查找异步队列可执行任务, 放置主线程执行 ⇒ 执行完毕 ⇒ 查找异步队列任务,执行

promise执行过程
一般情况下, 我们都是用promise(res,rej)来管理异步编程
成功:
let p1 = new Promise( (resolve, reject) => {
      resolve('ok')
      []: 'fulfuilled'
      []: 'ok'

})
复制代码
失败:
对于报错的处理机制, 类似于try..catch
let p1 = new Promise( (resolve, reject) => {

    resject('no')
    // 如果遇到报错
    []: 'rejected'
    []: '报错原因'
})
复制代码
实例状态的改变,可以控制,执行then方法时,存放的两个方法中的某一个方法执行


p.then(onfulfilledCallback,onrejectedCallback)


状态成功执行的是:onfulfilledCallback


状态失败执行的是:onrejectedCallback


并且把[]的值传递给方法


p1.then(result => {
    console.log('成功-->', result);
}, reason => {
    console.log('失败-->', reason);
});
复制代码


首先把传递进来的onfulfilledCallback和onrejectedCallback存储起来存储在一个容器中:因为可以基于then给其存放好多个回调函数


其次再去验证当前实例的状态

如果实例状态是pending,则不做任何的处理
如果已经变为fulfilled/rejected,则会通知对应的回调函数执行「但不是立即执行,而是把其放置在EventQueue中的微任务队列中」



“promise本身不是异步的,是用来管理异步的,但是then方法是异步的「微任务」”
then链
执行then方法会返回一个全新的promise实例
let p1 = new Promise((resolve, reject) => {
    resolve('OK');
    // reject('NO');
});
let p2 = p1.then(result => {
    console.log('P1成功-->', result);
    return Promise.reject(10);
}, reason => {
    console.log('P1失败-->', reason);
});
复制代码
执行then方法会返回一些全新的promise实例p2
let p1 = new Promise((resolve, reject) => {
    resolve('OK');
    // reject('NO');
});
let p2 = p1.then(result => {
    console.log('P1成功-->', result);
    return Promise.reject(10);
}, reason => {
    console.log('P1失败-->', reason);
});
复制代码


p2的状态和值是咋改变的?


不论执行的是基于p1.then存放的 onfulfilledCallback / onrejectedCallback 两个方法中的哪一个



只要方法执行不报错:

如果方法中返回一个全新的Promise实例,则“全新的Promise实例”的成功和失败决定p2的成功和失败
如果不是返回promise呢?则 []:fulfiled[]:返回值


如果方法执行报错:

p2的 []: rejected[]: 报错原因



如果onfulfilledCallback/onrejectedCallback不传递,则状态和结果都会“顺延/穿透”到下一个同等状态应该执行的回调函数上「内部其实是自己补充了一些实现效果的默认函数」
Promise.all
Promise.all():
Promise.all也会创建一个新promise实例(AA), 要求数组内所有的promise实例都成功, 此时AA才成功
Promise.race
最先知道状态的promise实例,是成功还是失败,决定了AA是成功还是失败

熟悉了Promise整个的执行过程, 我们就能剖析出它基本的结构了
Promise的基本结构
Promise骨架
基于上述, 我们可以看出来一个Promise构造最重要的四个组成部分是

PromiseResult 存放实参函数的执行结果
PromiseState 存放当前当前用于影响then函数执行结果的状态
onFulfilledCallbacks/onRejectedCallbacks 成功状态与失败状态的待执行方法的事件池
在实参函数内部对状态, 结果进行更改的change函数


我们现在先来看promise构造函数, 不考虑then的执行, 当然如果您对构造函数,面向对象不是特别熟悉, 建议您先看一下原型与原型链: 如何自己实现 call, bind, new? 中关于new执行的过程


// 一个基本的Promise应该长这样

function Promise (executor) {

    // 当前实例
    var self = this,
    // 改变状态与结果的函数
      change

    // 只允许基于new去构造promise实例
    if (!(self instanceof Promise)) throw new TypeError('undefined is not a promise!')
    // 要求传进来的executor必须是一个函数
    if (typeof executor !== 'function') throw new TypeError('Promise resolver ' + executor + ' is not a function!')

    // 存放状态与结果
    self.PromiseResult = undefined
    self.PromiseState = 'pending'
    // 成功与失败 事件池
    self.onFulfilledCallbacks = []
    self.onRejectedCallbacks = []
   
    change = function change (state, result) {
      // 如果状态已经发生改变了, 则不做任何处理
      if (state !== 'pending') return
      
      self.PromiseState = state
      self.PromiseResult = result
      
      // 根据当前的状态拿到对应状态的事件池
      let callbacks = self.PromiseState === 'fulfilled' ? self.onFulfilledCallbacks : self.onRejectedCallbacks
      
      var len = callback.length,
            i = 0
      if ( len > 0 ) {
            // 这里我没办法创建微任务, 所以用一个宏任务做了包裹,
            // 类似于Promise的异步执行, 但是then方法内的函数执行时一个异步微任务,
            // 这是自实现没法做到的, 一定要注意这里!!
            setTimeout(function(){
                for (i; i < len; i++) {
                   let callback = callbacks
                   // 执行该事件池中的函数, 并把结果传出
                   if(typeof callback === 'function') callback(self.PromiseResult)
                }
            }, 0)
      }
    }
    // 传入Promise内的实参函数是同步执行的,
    // 但是他的执行成败会影响状态与结果, 所以用try..catch做一下包裹
    try {
      executor(
            function resolve (result) {
                change('fulfilled', result)
            },
            function reject (reason) {
                change('rejected', reason)
            }
      )
    } catch (err) {
      // 函数执行如果报错, 直接捕获错误状态与结果
      change('rejected', err)
    }
   
}
复制代码

在不考虑then的情况下, 一个完整的promise就算是写好了, 内部对异步也做了管控, 接下来我们来处理then/then链跟catch, 由于catch的情况比较简单, 我们就直接看then链的执行流程就好了

then与then链


由于这部分比较杂乱, 所以我把所有的代码单独抽离出来, 我们一个一个方法看
// then/catch方法添加到promise的实例上
Promise.prototype = {
    constructor: Promise,
    // 标识符, 表示这个promise为我们自己重构的
    selfWrite: true,
    then: function then (onFulfilled, onRejected) {
      // 实现then链的本质就是返回一个promise实例, 在新的promise内部对传进来的方法做处理
      newPromise = function newPromise(){}
      return newPormise
    },
    catch: function catch (onRejected){}
}
复制代码
then

then方法为了实现then链机制, 我们返回一个新的Promise实例

then: function then (onFulfilled, onRejected) {

var self = this,
    // then参数函数的结果
    x,
    newPromise
// 判断then传入的是否为一个函数
if (typeof onFulfilled !== 'function') {
    onFulfilled = function onFulfilled(result) {
      return result
    }
}
if (typeof onRejected !== 'function') {
    onRejected = function onFulfilled(reason) {
      throw reason
    }
}
newPromise = new Promise(function (resolve, reject) {
      // 1. 已知当前promise实例的状态
      // 2. 未知状态即pending, 则把then事件添加到事件池内
      switch(self.PromiseState){
          case 'fulfilled':
            // 包装成一个异步函数
            setTimeout(function(){
                  try{
                      // 把上一个promise实例的结果传入
                      x = onFulfilled(self.PromiseResult)
                      // 对x处理, 如传入的x是一个promise实例
                      hanlde(newPromise, x, resolve, reject)
                  } catch(err) {
                      reject(err)
                  }
            }, 0)
            break;
          case 'rejected':
             setTimeout(function () {
                  try {
                  x = onRejected(self.PromiseResult)
                  handle(newPromise, x, resolve, reject)
                  } catch (err) {
                  reject(err)
                  }
            }, 0)
            break;
         default:
         // 未知状态, 则直接添加到事件池待执行
         self.onFulfilledCallbacks.push(function (result) {
               try {
                  x = onFulfilled(result)
                  handle(newPromise, x, resolve, reject)
               } catch (err) {
                   reject(err)
               }
         })
         self.onRejectedCallbacks.push(function (reason) {
            try {
                x = onRejected(reason)
                handle(newPromise, x, resolve, reject)
            } catch (err) {
                reject(err)
            }
            })
      }
      // 返回promise 后续then执行
   return newPromise
})
}
复制代码
handle函数

上段代码中的handle函数主要的作用是处理当前实例的状态

// onFulfilled, onRejected方法的执行返回结果处理
function handle(newPromise, x, resolve, reject){
// 新实例与内部函数的执行结果如果是同一个实例, 会变成死循环
    if(newPromise === x ) throw new TypeError('Chaining cycle detected for promise')

    if( isPromise(x) ) {
      // 如果函数的执行结果一个promise实例将当前两个待执行函数传入
      x.then(resolve, reject)
    }
    // 如果x是普通值/函数 则直接将结果传入下一个实例
    resolve(x)
}
复制代码
isPromise函数

判断当前执行结果是不是一个promise实例

function isPromise (x) {
    if( x == null) return false
    if( /^(function|object)$/i.test(typeof x) ) {
      if( typeof x.then === 'function' ) {
            return true
      }
    }
    return false
}
复制代码
catch

catch其实很简单,就是只给then传入失败的函数

catch: function (onRejected) {
      // 只给then传入失败状态的函数
      var self = this
      return self.then(null, onRejected)
    }
复制代码
resolve(result) / reject(reason)

Promise实例也可以通过Promise.resovle的方式来执行, 是因为实例上同样具有这样的两个API, 实现也非常简单.

Promise.resolve = function resolve (value){
    // 返回一个新promise实例, 并将状态改成成功
    return new Promise(function (resolve) ) {
      resolve(value)
    }
}
复制代码
Promise.reject = function reject(value) {
    // 返回一个新promise实例, 并将状态改成成功
    return new Promise(function (reject) {
      reject(value)
    })
}
复制代码
all(promises)

all方法的执行方式, 其实就是传进来的所有promise都为成功状态时, 该promise才为成功状态

Promise.all = function all (promises) {
    let results = [],
      n = 0,
      newPromises
    // 如果参数不是是一个可迭代类型
    if ( Array.isArray(promises) ) throw new TypeError(promises + 'is not iterable')
    // 让数组内部所有的参数都变成一个promise实例
    promises = promises.map( function (promise) {
      if ( !isPromise(promise) ) return new Promise.resolve(promise)
      return promise
    }
    newPromise = new Promise(function (resolve, reject) {
      promises.forEach(function (promise, index) {
            promise.then(function (result) {
                // 要求每一个实例都成功才返回成功状态
                results = result
                // 在每一个promise实例中,执行的函数我们没有办法确定是否是同步函数
                //使用push的话, 可能会导致数组前排的异步函数执行结果与后排的同步函数执行结果位置混乱
                n++
                if(n >= promises.length) {
                  resolve(results)
                }
            }).catch(function (reason) {
                // 只要有一个实例走到了catch直接返回失败
                reject(reason)
            })
      })
    })
    return newPromise
}
复制代码
finally(callback)

finally执行需要返回一个promise实例, 并且无论状态如何finally传进去的函数都必须执行

Promise._finally = function _finally(callback) {
    let self = this,
      p = new Promise
    return self.then(
      function Fulfill(value) {
      // 管控callback函数
      p.resolve( callback() ).then( function () { return value } )
       },
      function reject(reason) {
      p.resolve( callback() ).then( function () { throw reason } )
       }
    )
}

红尘痴子 发表于 2021-3-19 17:08

:rggrg你这复制粘贴也不排下样式   https://juejin.cn/post/6940531182706622500

syx594 发表于 2021-3-19 16:52

虽然看不到,但是绝对的偶像

yuenc 发表于 2021-3-19 23:39

谢谢分享
页: [1]
查看完整版本: 【笔记】面试: 来, 手写一个promise