«Внезапное осознание» в процессе рукописного Обещания

JavaScript Promise
«Внезапное осознание» в процессе рукописного Обещания

Дайте мне время для статьи, которая проведет вас через детали рукописных обещаний.

Я полагаю, что мои друзья, возможно, видели много статей о написанных от руки обещаниях, но большинство из них, возможно, видели проблемы, связанные с тем, «как писать обещания от руки» в написании лица, чтобы понять это. Как фронтенд, который повзрослел и снова вошел в бизнес с промисами, автор также готов изучить с вами, как написать промис вручную. ​

Прежде чем мы начнем, давайте вспомним, какие возможности обещают, что сделают их так распространенными. Сначала бросьте наиболее часто используемую сцену, похожую на это 🌰

const p=new MyPromise((resolve,reject)=>{
  setTimeout(()=>{
    resolve(1)
  },2000)  
})
p.then(v=>{
  console.log(v)
  return 2
})
.then(v=>{
  console.log(v)
})
// 我们期望能2s延迟后输出1,2


const p=new MyPromise((resolve,reject)=>{
  setTimeout(()=>{
    reject(1)
  },2000)  
})
p.then(v=>{
  console.log(v)
  return 2
},r=>{
  console.log(v)
  return 3
})
.then(v=>{
  console.log(v)
})
// 我们期望能2s延迟后输出1,3

Некоторые знакомы с соответствующими по сравнению с использованием мелких партнеров, не оправдают вас, и теперь мы должны сочетать подPromises/A+Спецификация, перечислим общие функции, которые нам нужно реализовать.

характеристика

  1. Статус: должен бытьpending fulfilled rejectedодин, только поpendingПеревести вfulfilledилиrejectedи необратимый
  2. необходимо предоставитьthenметод,
    1. принимает два параметра
    2. onFulfilledСоответствующее состояние меняется наfulfilledобратный вызов, когдаonRejectedСоответствующее состояние меняется наrejectedОбратный вызов
    3. С точки зрения того же обещания,thenМожно назвать несколько раз
    4. then
  3. поставкаcatchМетод, результаты уловки (FN), а затем (, fn) согласуются
  4. статический методresolve,reject,all,race

По перечисленным выше функциям начинаем простой разбор примера👇🏻

const p=new MyPromise((resolve,reject)=>{
  setTimeout(()=>{
    resolve(1)
  },1000)
})
p.then((v)=>{
  console.log(v)
},(rej)=>{
  console.error(rej)
})

Объявление общих терминов, используемых в этой статье

  1. fnПредставляет функцию, переданную в конструктор MyPromise.
  2. обратный вызов успехаУказывает, что обратный вызов выполняется, когда MyPromise изменяется с ожидающего на выполненное
  3. обратный вызов с ошибкойУказывает, что обратный вызов выполняется, когда MyPromise изменяется с ожидающего на отклоненный.

В примере мы используемsetTimeoutЧтобы выполнить асинхронную задачу, в реальном бизнес-сценарии это будет заменено запросом интерфейса и т. д. С точки зрения выполнения js эти строки кода, вероятно, представляют собой следующие шаги:

  1. новыйMyPromiseЭтот конструктор возвращает объект p
  2. Конструктор принимает в качестве параметра функцию fn, которая сама принимаетresolveа такжеrejectДва параметра, вызываемые в зависимости от конкретной бизнес-ситуацииresolveилиreject,а такжеresolveа такжеrejectОба поддерживают передачу параметров
  3. Метод then существует для возвращаемого объекта p, который принимает два параметра: обратный вызов при успешном выполнении и обратный вызов при отказе. Все типы — это функции, а входные параметры функции — это входные параметры конструктора.resolveилиrejectЗначение, переданное при окончательном вызове
  4. fnЧерез некоторое время звонитеresolve(1), вам нужно выполнить обратный вызов успеха в это время

Внезапно понял -- 1

На данном этапе анализа нетрудно обнаружить, чтоfnИзмените состояние в середине, а затем выполните функцию обратного вызова, что является типичным режимом публикации-подписки. передачаthenМетод заключается в регистрации обратного вызова в случае успеха или неудачи, а какой из них выполнить, определяется результатом выполнения функции, переданной в конструктор. Кроме того, в спецификации также упоминается экземпляр промиса, который then может вызываться несколько раз, аналогично многократной подписке, поэтому легко представить себе использование массива для управления этими обратными вызовами. Итак, имея в виду эту идею, мы можем начать писать код.下载.jpeg

объединить сноваPromises/A+Спецификация только обещанияpending, fulfilled, rejectedТри состояния, мы можем нарисовать следующий код Поскольку задействованы новые вызовы, здесь мы используем класс напрямую для обработки MyPromise.

const PENDING='pending'
const FULLFILLED='fullfilled'
const REJECTED='rejected'

class MyPromise{
  
  status=PENDING
  fullfilledStack=[]
  rejectedStack=[]
  
  constructor(fn){
    const resolve=(value)=>{
      for(const cb of this.fullfilledStack){
        cb(value)
      }
    }
    const reject=(reason)=>{
      for(const cb of this.rejectedStack){
        cb(reason)
      }
    }
    fn(resolve,reject)
  }

  then(res,rej){
    this.fullfilledStack.push(res)
    this.rejectedStack.push(rej)
  }
}

Это самый простой пример.Для сбора и вызова обратных вызовов методы, которые мы используем иeventEmitterПохоже, запустили 🌰 выше, нет проблем, мы закончили сцену в нашем 🌰, посмотрим. ​

Еще одной особенностью Promise является его способность объединять вызовы в цепочку, то есть мы можем продолжать дальше, а код, который мы пишем, поддерживает только один раз, так как же решить эту проблему? ​

Прежде всего убедитесь, что then может продолжаться.Это достигается путем возврата нового экземпляра Promise каждый раз.Код, относящийся к методу then release, выглядит следующим образом:

then(res,rej){
  return new MyPromise((resolve,reject)=>{
    // 按不同条件,执行resolve或者reject
  })
}

Поскольку все экземпляры MyPromise возвращаются, проблема непрерывно вызывания тогдашнего метода тогда решена. Следующее, что нам нужно решить, в случае нескольких Thens, как гарантировать, что значение каждым тогда является возвращаемое значение предыдущего метода, затем. в сочетании с нашейВнезапно понял -- 1Основываясь на логике, полученной из , реорганизуйте то, что мы будем делать дальше. С точки зрения разделения труда,fnбудет вызываться конструктором MyPromise, который будетfnПредложите два метода дляfnположить все вthen上注册的回调进行调用。 Следовательно,fnresolveилиreject. когда мыthenКогда экземпляр MyPromise возвращается в методе,fnВам также необходимо получить возвращаемое значение последнего успешного обратного вызова и передать его следующему.resolve

Внезапно понял -- 2

После того, как состояние старого промиса будет обновлено, будет выполнен соответствующий коллбэк.Мы используем характеристики замыкания, чтобы поместить в коллбэк метод изменения нового промиса! И результат выполнения обратного вызова, зарегистрированный на старом промисе, нам нужно только выполнить его, чтобы получить возвращаемое значение и передать его новому промису.resolveметод илиrejectметод.

images (1).jpeg

Объединив эти два пункта, мы можем получить код вида 👇🏻

then(res:Fn,rej?:Fn){
  return new MyPromise((resolve,reject)=>{
    const resCb=(v)=>{
      resolve(res(v))
    }
    this.fullfilledStack.push(resCb)

    const rejCb=(r)=>{
      reject(rej!(r))
    }
    this.rejectedStack.push(rejCb)
  })
}

Измените наш 🌰 на связанные вызовы и попробуйте наш новый метод then

const p=new MyPromise((res,rej)=>{
  setTimeout(()=>{
    res(1)
  },1000)
})
p
.then((v)=>{
  console.log(v)
  return 2
})
.then((v)=>{
  console.log(v)
  return 3
})
.then(v=>{
  console.log(v)
})

// 输出了 1、2、3

С выводом результата проблем нет, но когда я писал это, я понял проблему, то есть массив успешных обратных вызовов и массив неудачных обратных вызовов являются синглтонами или следуют за каждым экземпляром Promise. Здесь автор анализирует указатель this при выполнении кода, хотя внутри then используются стрелочные функции, метод then вызывается каждым объектом Promise.

Внезапно понял -- 3

Каждый раз, когда вызывается then, генерируется новый экземпляр Promise, который выполняется при создании нового экземпляра.fnФункция будет вставлять обратные вызовы в массив обратных вызовов предыдущего экземпляра Promise, поэтому фактически каждый экземпляр Promise будет поддерживать свой собственный массив успешных обратных вызовов и массив обратных вызовов с ошибкой, что также соответствует нашим ожиданиям.

images (3).jpeg

Давайте посмотрим на полную картину текущего кода:


class MyPromise{

  fulfilledStack=[]
  rejectedStack=[]

  constructor(fn){
    const resolve=(value)=>{
      for(const cb of this.fulfilledStack){
        cb(value)
      }
    }
    const reject=(reason)=>{
      for(const cb of this.rejectedStack){
        cb(reason)
      }
    }
    fn(resolve,reject)
  }

  then(onfulfilled,onrejected){
    return new MyPromise((resolve,reject)=>{
      const resCb=(v)=>{
        resolve(onfulfilled(v))
      }
      this.fulfilledStack.push(resCb)
      
      const rejCb=(r)=>{
        reject(onrejected(r))
      }
      this.rejectedStack.push(rejCb)
    })
  }
}

const p0=new MyPromise((resolve,reject)=>{
  setTimeout(()=>{
    resolve(1)
  },1000)
})

const p1 = p0.then((v)=>{ // cb1
  console.log(v)
  return 2
})
const p2 = p1.then((v)=>{ // cb2
  console.log(v)
  return 3
})
const p3 = p2.then((v)=>{ // cb3
  console.log(v)
})

До сих пор мы поддерживали связанные вызовы, давайте взглянем на реализацию contrustor, а затем на методы в классе MyPromise.конструктор:

  1. принимает fn функцию в качестве параметра
  2. Объявите две функции, соответственно ответственные за выполнение функций обратного вызова успеха и отказа в текущем экземпляре MyPromise по очереди.
  3. Good объявит две функции, переданные в качестве параметра fn, вызов fn для

тогда:

  1. Принять onfulfilled, onrejected две функции в качестве параметров
  2. возвращает новый экземпляр MyPromise
  3. Объявите две функции, которые выполняют передачу по значению, и используйте замыкания, чтобы сохранить возможность изменять состояние нового MyPromise. И передайте возвращаемое значение onfulfilled() или onrejected() вновь сгенерированному экземпляру MyPromise.
  4. Поместите две функции в массив текущего экземпляра MyPromise соответственно. Конечный эффект таков: обратный вызов старого экземпляра MyPromise может управлять состоянием нового экземпляра MyPromise.

Давайте проследим за нашим 🌰, чтобы увидеть, что происходит, когда код выполняется.

  1. Строка 36 При выполнении возвращает объект P0, мы зарегистрировали функцию таймера, которая после 1S, выполнение разрешения (1)
  2. В строке 42 мы вызываем метод then объекта p0 и передаем функцию cb1. Его можно получить реализацией метода then, в этом метод then вернет новый объект p1, а мы перейдем к объекту p0fulfilledStackВ этом методе есть метод push, который выполнит cb1, который мы передали ранее, и использует его возвращаемое значение в качестве параметра для передачи его экземпляру p1.fnфункциональныйresolveметод.
  3. В строке 46 мы вызываем метод then для p1, так же, как и в строке 42, мы вернем объект p2, а в p1fulfilledStackВ методе push он выполнит cb2 и передаст возвращаемое значение cb2 функции fn, которая создает экземпляр объекта p2.resolveметод.
  4. Последующие вызовы then не будут повторяться.После выполнения метода синхронизации время 1 с истекает, и выполняется resolve(1) в методе fn, используемом для создания экземпляра p0.fulfilledStackФункции in будут выполняться последовательно с 1 в качестве аргумента. В этот момент cb1 будет официально выполнен, и управление перейдет к выходу 1, а возвращаемое значение 2 cb1 будет передано на p1.resolve, п1fulfilledStackФункции in будут выполняться последовательно с 2 аргументами. И так далее, конечный вывод консоли 1, 2, 3

До сих пор мы рассмотрели сценарии, когда функция CB напрямую возвращает значение. Нам также необходимо рассмотреть сценарий, в котором функция CB возвращает новый экземпляр MyPromise, такой как следующий демо

const p=new MyPromise((resolve,reject)=>{
  setTimeout(()=>{
    resolve(1)
  },1000)
})
p
.then((v)=>{
  console.log(v)
  return new MyPromise((res)=>{
    setTimeout(() => {
      res(2)
    }, 1000);
  })
})
.then((v)=>{
  console.log(v)
  return 3
})
.then(v=>{
  console.log(v)
})

cb函数返回了MyPromise的实例,我们的then方法最终也会返回新的MyPromise实例,我们又该如何处理这种情况呢? ​

Внезапно понял -- 4

Мы сделали так, чтобы каждый раз генерировать новый экземпляр Promise и синхронизировать состояние нового экземпляра со старым. Теперь, поскольку cb возвращает экземпляр промиса, нам нужно только передать метод разрешения и метод отклонения вновь сгенерированного промиса then в then экземпляра промиса, возвращенного cb.下载 (1).jpegКроме того, мы, кажется, упустили проблему ошибок выполнения метода, которая требует, чтобы Promise был установлен вrejectedгосударство, мы составляем эту ссылку

then(res,rej){
  return new MyPromise((resolve,reject)=>{
    const resCb=(v)=>{
      try{
        const result = res(v)
        if(result instanceof MyPromise){
          // res方法的return中,返回的是MyPromise实例 ,例如 return new MyPromise()
          // 将新构造出来的MyPromise实例的状态,交由res返回的MyPromise实例所控制
          result.then(resolve,reject)
        }else{
          resolve(result)
        }
      } catch (e){
        reject(e)
      }
    }
    this.fullfilledStack.push(resCb)

    const rejCb=(r)=>{
      try {
        const result = rej(r)
        if(result instanceof MyPromise){
          result.then(resolve,reject)
        }else{
          resolve(result)
        }
      } catch (e){
        reject(r)
      }
    }
    this.rejectedStack.push(rejCb)
  })
}

Я запустил демо, вывод нормальный, временной интервал правильный. На данный момент проблема связанных вызовов нами решена. Верните наши глазаPromises/A+Спецификация, передается по значению ** Далее нам нужно иметь дело с. ** Мы посмотрим на интерпретацию значений спецификации

 promise2 = promise1.then(onFulfilled, onRejected);
  1. If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1.
  2. If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.

Мы имеем дело с этими двумя сценариями, и нетрудно придумать следующий код

  then(res,rej){
    return new MyPromise((resolve,reject)=>{
      const resCb=(v)=>{
        if(typeof res !=='function'){
          return resolve(v)
        }
        try{
          const result = res(v)
          if(result instanceof MyPromise){
            // res方法的return中,返回的是MyPromise实例 ,例如 return new MyPromise()
            // 将新构造出来的MyPromise实例的状态,交由res返回的MyPromise实例所控制
            result.then(resolve,reject)
          }else{
            resolve(result)
          }
        } catch (e){
          reject(e)
        }
      }
      this.fullfilledStack.push(resCb)
      
      const rejCb=(r)=>{
        if(typeof rej !=='function'){
          return reject(r)
        }
        try {
          const result = rej!(r)
          if(result instanceof MyPromise){
            result.then(resolve,reject)
          }else{
            resolve(result)
          }
        } catch (e){
          reject(r)
        }
      }
      this.rejectedStack.push(rejCb)
    })
  }

До сих пор мы обсуждали, как промисы справляются с асинхронностью. Порядок выполнения кода всегда был таким: новый промис, привязать обратный вызов через then, изменить состояние промиса и выполнить соответствующий обратный вызов, но если в нашем демо нет setTimeout, порядок новый промис, изменить состояние Обещание, а затем зарегистрируйте обратный вызов, очевидно, что обратный вызов не будет выполняться в это время, поэтому нам также необходимо настроить наш метод then, чтобы убедиться, что состояние промиса изменилось до выполнения then, и обратный вызов может быть выполнен правильно. ​


  then(res,rej){
    return new MyPromise((resolve,reject)=>{
      // ...
      this.fullfilledStack.push(resCb)
      this.status===FULLFILLED && resCb(this.value)
      
      // ...
      this.rejectedStack.push(rejCb)
      this.status===REJECTED && rejCb(this.value)
    })
  }
}


Передача значения и совместимая синхронизация завершены. В этом изменении мы добавили поле значения в MyPromise для передачи значения текущего экземпляра в соответствующую функцию обратного вызова. ​

На данный момент реализация метода then подошла к концу, и мы будем улучшать метод-член catch и статические методы, такие как resolve, reject, all и race. При внедрении Promise.all я столкнулся с такой же ситуацией, то есть как узнать, все ли промисы выполнены? ​

Внезапно понял - 5

Мы можем использовать простой счетчик, чтобы определить, было ли выполнено каждое обещание.

images (2).jpeg


  catch(cb:Fn){
    this.then(undefined,cb)
  }

  static resolve(val){
    if(val instanceof MyPromise) return val
    return new MyPromise(resolve=>{
      resolve(val)
    })
  }

  static reject(val){
    return new MyPromise((resolve,reject)=>{
      reject(val)
    })
  }

  static all(arr){
    return new MyPromise((resolve,reject)=>{
      var count=0
      var result=[]
      arr.forEach((p,index)=>{
        MyPromise.resolve(p).then((val)=>{
          count++
          result[index]=val
          if(count === arr.length){
            resolve(result)
          }
        },reason=>{
          reject(reason)
        })
      })
    })
  }
  
  static race(arr){
    return new MyPromise((resolve,reject)=>{
      for(const p of arr){
        MyPromise.resolve(p)
        .then(
          v=>resolve(v),
          r=>reject(r)
        )
      }
    })
  }

Используя демонстрацию для проверки нашей логики, мы обнаружили, что при выполнении гонки несколько MyPromise по-прежнему выдают несколько результатов, что не соответствует нашим ожиданиям.Давайте вернемся и посмотрим, в чем проблема. ​

Внезапно понял -- 6

Мы добавляем концепцию состояния в Promise, но не ограничиваем изменения между состояниями, чтобы состояние Promise можно было изменять несколько раз, вызывая несколько обратных вызовов.

images.jpeg

Теперь мы добавляем это ограничение, соответствующий код выглядит следующим образом

constructor(fn){
  const resolve=(value:PValue)=>{
    if(this.status===PENDING){
      this.status=FULLFILLED
      this.value=value
      for(const cb of this.fullfilledStack){
        cb(value)
      }
    }
  }
  const reject=(reason)=>{
    if(this.status===PENDING){
      this.status=REJECTED
      this.value=reason
      for(const cb of this.rejectedStack){
        cb(reason)
      }
    }
  }
  fn(resolve,reject)
}

至此,我们已经完成了一个Promise的核心方法,经过一次次的“恍然大悟”,也完整的理解了手写一个Promise需要掌握的知识点 ​

полный код


const PENDING='pending'
const FULLFILLED='fullfilled'
const REJECTED='rejected'

class MyPromise{

  status=PENDING
  value=undefined
  fullfilledStack=[]
  rejectedStack=[]

  constructor(fn){
    const resolve=(value)=>{
      if(this.status===PENDING){
        this.status=FULLFILLED
        this.value=value
        for(const cb of this.fullfilledStack){
          cb(value)
        }
      }
    }
    const reject=(reason)=>{
      if(this.status===PENDING){
        this.status=REJECTED
        this.value=reason
        for(const cb of this.rejectedStack){
          cb(reason)
        }
      }
    }
    fn(resolve,reject)
  }

  then(res,rej){
    return new MyPromise((resolve,reject)=>{
      const resCb=(v)=>{
        if(typeof res !=='function'){
          return resolve(v)
        }
        try{
          const result = res(v)
          if(result instanceof MyPromise){
            // res方法的return中,返回的是MyPromise实例 ,例如 return new MyPromise()
            // 将新构造出来的MyPromise实例的状态,交由res返回的MyPromise实例所控制
            result.then(resolve,reject)
          }else{
            resolve(result)
          }
        } catch (e){
          reject(e)
        }
      }
      this.fullfilledStack.push(resCb)
      this.status===FULLFILLED && resCb(this.value)
      
      const rejCb=(r)=>{
        if(typeof rej !=='function'){
          return reject(r)
        }
        try {
          const result = rej(r)
          if(result instanceof MyPromise){
            result.then(resolve,reject)
          }else{
            resolve(result)
          }
        } catch (e){
          reject(r)
        }
      }
      this.rejectedStack.push(rejCb)
      this.status===REJECTED && rejCb(this.value)
    })
  }

  catch(cb){
    this.then(undefined,cb)
  }

  static resolve(val){
    if(val instanceof MyPromise) return val
    return new MyPromise(resolve=>{
      resolve(val)
    })
  }

  static reject(val){
    return new MyPromise((resolve,reject)=>{
      reject(val)
    })
  }

  static all(arr){
    return new MyPromise((resolve,reject)=>{
      var count=0
      var result=[]
      arr.forEach((p,index)=>{
        MyPromise.resolve(p).then((val)=>{
          count++
          result[index]=val
          if(count === arr.length){
            resolve(result)
          }
        },reason=>{
          reject(reason)
        })
      })
    })
  }

  static race(arr){
    return new MyPromise((resolve,reject)=>{
      for(const p of arr){
        MyPromise.resolve(p)
        .then(
          v=>resolve(v),
          r=>reject(r)
        )
      }
    })
  }
}

Справочная статья

Интервьюер: «Можете ли вы написать Обещание от руки?»

9k слов | Анализ принципа реализации Promise/async/Generator