Анализ промисов с точки зрения паттернов проектирования: порвать промисы руками несложно

JavaScript


предисловие

В качестве решения для асинхронного программирования Promise более эффективен, чем традиционные обратные вызовы и события, а также необходимо изучить внешний интерфейс. Как начинающий фронтенд, вы должны не только освоить использование промисов, но и иметь определенное представление о принципах их реализации (грубо говоря, это необходимо для того, чтобы интервью притворялись принудительными). Хотя в Интернете есть много кодов реализации Promise, с сотнями строк, лично я чувствую, что если у меня нет определенного понимания асинхронного программирования и Promise, эти коды - просто доска (вы не можете постучать по ней). доска для собеседования). Во-первых, читатели по умолчанию знакомы с объектами Promise, а затем с точки зрения наиболее часто используемых шаблонов проектирования интерфейса: шаблонов публикации-подписки и наблюдателя, Promise будет реализован шаг за шагом.

Начните с асинхронного программирования

Поскольку Promise — асинхронное решение, как выполняется асинхронная обработка до того, как объект Promise перестанет существовать? Существует два подхода: функции обратного вызова и шаблон проектирования «публикация-подписка» или «наблюдатель». (Дополнительные способы реализации асинхронного программирования см. в моей статье:5 способов реализовать асинхронное программирование в JavaScript)

функция обратного вызова

Я считаю, что callback-функция знакома читателям, ведь самый ранний контакт — это callback-функция, а также очень просто использовать callback-функцию для асинхронной обработки. файл обычно делает это.

fs.readFile("h.js", (err, data) => {
  console.log(data.toString())
});

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

fs.exists("h.js", exists => { // 文件是否存在
  if (exists) {
    fs.readFile("h.js", (err, data) => { // 读文件
      fs.mkdir(__dirname + "/js/", err => { // 创建目录
        fs.writeFile(__dirname + "/js/h.js", data, err => { // 写文件
          console.log("复制成功,再回调下去,代码真的很难看得懂")
        })
      });
    });
  }
});

На самом деле код все еще можно прочитать, спасибо JS-дизайнеру за то, что он не убрал фигурные скобки функции. Написание обратных вызовов наподобие python без фигурных скобок — это (просто шутка. Не сказать, что python — это плохо, в конце концов, JavaScript — лучший язык в мире)

# 这代码属实没法看啊
def callback_1():
      # processing ...
  def callback_2():
      # processing.....
      def callback_3():
          # processing ....
          def callback_4():
              #processing .....
              def callback_5():
                  # processing ......
              async_function1(callback_5)
          async_function2(callback_4)
      async_function3(callback_3)
  async_function4(callback_2)
async_function5(callback_1)

Шаблоны проектирования "публикация-подписка" и "наблюдатель"

Впервые я познакомился с шаблонами проектирования, когда изучал Java и C++, ведь шаблоны проектирования предлагались на основе объектно-ориентированного разделения объектов. Шаблон проектирования «публикация-подписка» похож на шаблон «наблюдатель», но есть небольшие отличия (тестовый сайт интервью находится здесь).

Шаблон наблюдателяВ разработке программного обеспечения это объект, который поддерживает список зависимостей и автоматически уведомляет их при изменении любого состояния.
Опубликовать модель подпискирежим передачи сообщений,диктор(Издатели)Как правило, сообщения публикуются в определенном центре сообщений,подписчик(Подписчик)Вы можете подписаться на информацию из центра сообщений в соответствии с вашими потребностями, что очень похоже на очередь сообщений..

В шаблоне наблюдателя есть только два компонента: получатель и издатель, а в шаблоне публикация-подписка — три компонента: издатель, центр сообщений и получатель.


Разница в реализации кода также более очевидна.

Шаблон проектирования наблюдателя

// 观察者设计模式
class Observer {
  constructor () {
    this.observerList = [];
  }

  subscribe (observer) {
    this.observerList.push(observer)
  }

  notifyAll (value) {
    this.observerList.forEach(observe => observe(value))
  }
}

Шаблон проектирования публикации-подписки (nodejs EventEmitter)

// 发布订阅
class EventEmitter {
  constructor () {
    this.eventChannel = {}; // 消息中心
  }

  // subscribe
  on (event, callback) {
    this.eventChannel[event] ? this.eventChannel[event].push(callback) : this.eventChannel[event] = [callback]
  }

  // publish
  emit (event, ...args) {
    this.eventChannel[event] && this.eventChannel[event].forEach(callback => callback(...args))
  }

  // remove event
  remove (event) {
    if (this.eventChannel[event]) {
      delete this.eventChannel[event]
    }
  }

  // once event
  once (event, callback) {
    this.on(event, (...args) => {
      callback(...args);
      this.remove(event)
    })
  }
}

Разницу между ними видно и из кода.Режим наблюдателя не классифицирует события.При возникновении события все наблюдатели будут уведомлены. Шаблон проектирования «публикация-подписка» классифицирует события и инициирует разные события, которые будут уведомлять разных наблюдателей. Следовательно, можно считать, что последний является модернизированной версией первого с более детальным разделением событий уведомления.

Применение публикации-подписки и наблюдателей в асинхронном режиме

// 观察者
const observer = new Observer();
observer.subscribe(value => {
  console.log("第一个观察者,接收到的值为:");
  console.log(value)
});
observer.subscribe(value => {
  console.log("第二个观察者,接收到的值为");
  console.log(value)
});
fs.readFile("h.js", (err, data) => {
  observer.notifyAll(data.toString())
});

// 发布-订阅
const event = new EventEmitter();
event.on("err", console.log);
event.on("data", data => {
  // do something
  console.log(data)
});
fs.readFile("h.js", (err, data) => {
  if (err) event.emit("err", err);
  event.emit("data", data.toString())
});

В асинхронном программировании оба шаблона проектирования реализуют асинхронное программирование путем регистрации глобальных наблюдателей или глобальных событий с последующим уведомлением всех наблюдателей или инициированием определенных событий в асинхронной среде.

Недостатки также очевидны, например, слишком много глобальных наблюдателей/событий, которые трудно поддерживать, конфликты имен событий и т. д. Так родился Promise.

Анализировать и реализовывать промисы с точки зрения шаблона проектирования наблюдателя.

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

const asyncReadFile = filename => new Promise((resolve) => {
  fs.readFile(filename, (err, data) => {
    resolve(data.toString()); // 发布者 相当于观察者模式的notifyAll(value) 或者发布订阅模式的emit
  });
});

asyncReadFile("h.js").then(value => { // 订阅者 相当于观察者模式的subscribe(value => console.log(value)) 或者发布订阅模式的on
  console.log(value);
});

Из приведенного выше кода Promise я думаю, что причина, по которой схема Promise лучше, чем предыдущая схема публикации-подписки/наблюдателя, заключается в следующем: инкапсуляция асинхронных задач, издатель событий находится в функции обратного вызова (разрешение), а приемник событий в объектном методе (then()), используя локальные события, лучше инкапсулирует оба, а не бросает их в глобальные.

Обещание реализации

Основываясь на приведенных выше идеях, мы можем реализовать простое обещание: MyPromise

class MyPromise {
  constructor (run) { // run 函数 (resolve) => any
    this.observerList = [];
    const notifyAll = value => this.observerList.forEach(callback => callback(value));
    run(notifyAll); // !!! 核心
  }

  subscribe (callback) {
    this.observerList.push(callback);
  }
}
// 
const p = new MyPromise(notifyAll => {
  fs.readFile("h.js", (err, data) => {
    notifyAll(data.toString()) // resolve
  })
});

p.subscribe(data => console.log(data)); // then

Несколько строк кода для реализации простого промиса, и приведенный выше код представляет собой изменение шаблона проектирования наблюдателя лишь незначительно.

добавить состояние

Конечно, это еще не конец, MyPromise выше проблематичен. Как упоминалось ранее, промис — это инкапсуляция асинхронных задач, которую можно рассматривать как наименьшую асинхронную единицу (подобную обратному вызову), и должен быть только один асинхронный результат, то есть разрешение в промисе можно использовать только один раз, что эквивалентно одноразовому событию EventEmitter. NotifyAll из MyPromise, реализованный выше, можно использовать несколько раз (почему-то нет), поэтому это может привести к более чем одной ошибке в результате асинхронных задач. Поэтому решение состоит в том, чтобы добавить логическую переменную или добавить состояния, которые являются ожидающими и выполненными состояниями (по сути, то же самое, что и логическая переменная).Когда notifyAll вызывается один раз, немедленно блокируйте notifyAll или снова вызывайте notifyAll, когда ожидающее состояние изменяется на выполненное состояние. функция не будет работать.

Для согласования с объектом Promise здесь используется метод добавления состояния (кстати, название метода изменено, notifyAll => resolve, subscribe => then).

const pending = "pending";
const fulfilled = "fulfilled";

class MyPromise {
  constructor (run) { // run 函数 (resolve) => any
    this.observerList = [];
    this.status = pending;
    const resolve = value => {
      if (this.status === pending) {
        this.status = fulfilled;
        this.observerList.forEach(callback => callback(value));
      }
    };
    run(resolve); // !!! 核心
  }

  then (callback) {
    this.observerList.push(callback);
  }
}

const p = new MyPromise(resolve => {
  setTimeout(() => {
    resolve("hello world");
    resolve("hello world2"); // 不好使了
  }, 1000);
});

p.then(value => console.log(value));

Реализовать связанные вызовы

Кажется, что это начинает немного набрасываться, но then в MyPromise не имеет цепочек вызовов.Далее давайте реализуем then-цепочку.Следует отметить, что then-метод в Promise возвращает новый экземпляр Promise, а не предыдущий Обещать. Поскольку метод then продолжает возвращать новые объекты MyPromise, необходимо свойство для хранения уникального асинхронного результата. С другой стороны, реализация метода then по-прежнему должна регистрировать обратный вызов, но реализация должна учитывать текущее состояние.Если он находится в состоянии ожидания, нам нужно зарегистрировать обратный вызов в очереди при возврате нового MyPromise. , Если он находится в состоянии выполнения, то верните новый объект MyPromise напрямую и передайте результат предыдущего объекта MyPromise новому объекту MyPromise.

const pending = "pending";
const fulfilled = "fulfilled";

class MyPromise {
  constructor (run) { // run 函数 (resolve) => any
    this.resolvedCallback = [];
    this.status = pending;
    this.data = void 666; // 保存异步结果
    const resolve = value => {
      if (this.status === pending) {
        this.status = fulfilled;
        this.data = value; // 存一下结果
        this.resolvedCallback.forEach(callback => callback(this.data));
      }
    };
    run(resolve); // !!! 核心
  }

  then (onResolved) {
    // 这里需要对onResolved做一下处理,当onResolved不是函数时将它变成函数
    onResolved = typeof onResolved === "function" ? onResolved : value => value;
    switch (this.status) {
      case pending: {
        return new MyPromise(resolve => {
          this.resolvedCallback.push(value => { // 再包装
            const result = onResolved(value); // 需要判断一下then接的回调返回的是不是一个MyPromise对象
            if (result instanceof MyPromise) {
              result.then(resolve) // 如果是,直接使用result.then后的结果,毕竟Promise里面就需要这么做
            } else {
              resolve(result); // 感受一下闭包的伟大
            }
          })
        })
      }
      case fulfilled: {
        return new MyPromise(resolve => {
          const result = onResolved(this.data); // fulfilled态,this.data一定存在,其实这里就像map过程
          if (result instanceof MyPromise) {
            result.then(resolve)
          } else {
            resolve(result); // 闭包真伟大
          }
        })
      }
    }
  }
}

const p = new MyPromise(resolve => {
  setTimeout(() => {
    resolve("hello world");
    resolve("hello world2"); // 不好使了
  }, 1000);
});

p.then(value => value + "dpf")
 .then(value => value.toUpperCase())
 .then(console.log);

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

обработка ошибок

Выполнены только объекты разрешения, а затем объекты MyPromise. Библиотеки без тестов — хулиганы, а код без обработки ошибок — хулиганы, поэтому обработка ошибок по-прежнему очень важна. Так как асинхронная задача может не завершиться или в середине может возникнуть ошибка, эту ситуацию необходимо обработать. Поэтому нам нужно добавить состояние reject, чтобы указать на ошибку в асинхронной задаче, и использовать очередь rejectCallback для хранения события ошибки, отправленного reject. (Предупреждение о высокой энергии впереди, программирование, ориентированное на попытку/улов, началось)

const pending = "pending";
const fulfilled = "fulfilled";
const rejected = "rejected"; // 添加状态 rejected

class MyPromise {
  constructor (run) { // run 函数 (resolve, reject) => any
    this.resolvedCallback = [];
    this.rejectedCallback = []; // 添加一个处理错误的队列
    this.status = pending;
    this.data = void 666; // 保存异步结果
    const resolve = value => {
      if (this.status === pending) {
        this.status = fulfilled;
        this.data = value;
        this.resolvedCallback.forEach(callback => callback(this.data));
      }
    };
    const reject = err => {
      if (this.status === pending) {
        this.status = rejected;
        this.data = err;
        this.rejectedCallback.forEach(callback => callback(this.data));
      }
    };
    try { // 对构造器里传入的函数进行try / catch
      run(resolve, reject); // !!! 核心
    } catch (e) {
      reject(e)
    }
  }

  then (onResolved, onRejected) { // 添加两个监听函数
    // 这里需要对onResolved做一下处理,当onResolved不是函数时将它变成函数
    onResolved = typeof onResolved === "function" ? onResolved : value => value;
    onRejected = typeof onRejected === "function" ? onRejected : err => { throw err };

    switch (this.status) {
      case pending: {
        return new MyPromise((resolve, reject) => {
          this.resolvedCallback.push(value => {
            try { // 对整个onResolved进行try / catch
              const result = onResolved(value);
              if (result instanceof MyPromise) { 
                result.then(resolve, reject)
              } else {
                resolve(result); 
              }
            } catch (e) {
              reject(e) // 捕获异常,将异常发布
            }
          });
          this.rejectedCallback.push(err => {
            try { // 对整个onRejected进行try / catch
              const result = onRejected(err);
              if (result instanceof MyPromise) {
                result.then(resolve, reject)
              } else {
                reject(err)
              }
            } catch (e) {
              reject(err) // 捕获异常,将异常发布
            }
          })
        })
      }
      case fulfilled: {
        return new MyPromise((resolve, reject) => {
          try { // 对整个过程进行try / catch
            const result = onResolved(this.data);
            if (result instanceof MyPromise) {
              result.then(resolve, reject)
            } else {
              resolve(result);  
            }
          } catch (e) {
            reject(e) // 捕获异常,将异常发布
          }
        })
      }
      case rejected: {
        return new MyPromise((resolve, reject) => {
          try { // 对整个过程进行try / catch
            const result = onRejected(this.data);
            if (result instanceof MyPromise) {
              result.then(resolve, reject)
            } else {
              reject(result) 
            }
          } catch (e) {
            reject(e) // 捕获异常,将异常发布
          }
        })
      }
    }
  }
}

const p = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    reject(new Error("error"));
    resolve("hello world");  // 不好使了
    resolve("hello world2"); // 不好使了
  }, 1000);
});

p.then(value => value + "dpf")
 .then(console.log)
 .then(() => {}, err => console.log(err));

Видно, что реализация метода then более сложная, но это основной метод, после него легко реализовать остальные методы Реализация каждого метода MyPromise приведена ниже.

поймать реализацию

Эта реализация очень проста

catch (onRejected) {
    return this.then(void 666, onRejected)
}

Статический метод MyPromise.resolve

static resolve(p) {
     if (p instanceof MyPromise) {
       return p.then()
     } 
     return new MyPromise((resolve, reject) => {
       resolve(p)
     })
 }

Статический метод MyPromise.reject

static reject(p) {
    if (p instanceof MyPromise) {
      return p.catch()
    } 
    return new MyPromise((resolve, reject) => {
      reject(p)
    })
}
 

Статический метод MyPromise.all

static all (promises) {
    return new MyPromise((resolve, reject) => {
      try {
        let count = 0,
            len   = promises.length,
            value = [];
        for (let promise of promises) {
          MyPromise.resolve(promise).then(v => {
            count ++;
            value.push(v);
            if (count === len) {
              resolve(value)
            }
          })
        }
      } catch (e) {
        reject(e)
      }
    });
  }

Статический метод MyPromise.race

static race(promises) {
    return new MyPromise((resolve, reject) => {
      try {
        for (let promise of promises) {
          MyPromise.resolve(promise).then(resolve)
        }
      } catch (e) {
        reject(e)
      }
    }) 
  }

Полная реализация кода MyPromise

const pending = "pending";
const fulfilled = "fulfilled";
const rejected = "rejected"; // 添加状态 rejected

class MyPromise {
  constructor (run) { // run 函数 (resolve, reject) => any
    this.resolvedCallback = [];
    this.rejectedCallback = []; // 添加一个处理错误的队列
    this.status = pending;
    this.data = void 666; // 保存异步结果
    const resolve = value => {
      if (this.status === pending) {
        this.status = fulfilled;
        this.data = value;
        this.resolvedCallback.forEach(callback => callback(this.data));
      }
    };
    const reject = err => {
      if (this.status === pending) {
        this.status = rejected;
        this.data = err;
        this.rejectedCallback.forEach(callback => callback(this.data));
      }
    };
    try { // 对构造器里传入的函数进行try / catch
      run(resolve, reject); // !!! 核心
    } catch (e) {
      reject(e)
    }
  }

  static resolve (p) {
    if (p instanceof MyPromise) {
      return p.then()
    }
    return new MyPromise((resolve, reject) => {
      resolve(p)
    })
  }

  static reject (p) {
    if (p instanceof MyPromise) {
      return p.catch()
    }
    return new MyPromise((resolve, reject) => {
      reject(p)
    })
  }

  static all (promises) {
    return new MyPromise((resolve, reject) => {
      try {
        let count = 0,
            len   = promises.length,
            value = [];
        for (let promise of promises) {
          MyPromise.resolve(promise).then(v => {
            count ++;
            value.push(v);
            if (count === len) {
              resolve(value)
            }
          })
        }
      } catch (e) {
        reject(e)
      }
    });
  }

  static race(promises) {
    return new MyPromise((resolve, reject) => {
      try {
        for (let promise of promises) {
          MyPromise.resolve(promise).then(resolve)
        }
      } catch (e) {
        reject(e)
      }
    })
  }

  catch (onRejected) {
    return this.then(void 666, onRejected)
  }

  then (onResolved, onRejected) { // 添加两个监听函数
    // 这里需要对onResolved做一下处理,当onResolved不是函数时将它变成函数
    onResolved = typeof onResolved === "function" ? onResolved : value => value;
    onRejected = typeof onRejected === "function" ? onRejected : err => { throw err };

    switch (this.status) {
      case pending: {
        return new MyPromise((resolve, reject) => {
          this.resolvedCallback.push(value => {
            try { // 对整个onResolved进行try / catch
              const result = onResolved(value);
              if (result instanceof MyPromise) { 
                result.then(resolve, reject)
              } else {
                resolve(result); 
              }
            } catch (e) {
              reject(e)
            }
          });
          this.rejectedCallback.push(err => {
            try { // 对整个onRejected进行try / catch
              const result = onRejected(err);
              if (result instanceof MyPromise) {
                result.then(resolve, reject)
              } else {
                reject(err)
              }
            } catch (e) {
              reject(err)
            }
          })
        })
      }
      case fulfilled: {
        return new MyPromise((resolve, reject) => {
          try { // 对整个过程进行try / catch
            const result = onResolved(this.data);
            if (result instanceof MyPromise) {
              result.then(resolve, reject)
            } else {
              resolve(result);  // emit
            }
          } catch (e) {
            reject(e)
          }
        })
      }
      case rejected: {
        return new MyPromise((resolve, reject) => {
          try { // 对整个过程进行try / catch
            const result = onRejected(this.data);
            if (result instanceof MyPromise) {
              result.then(resolve, reject)
            } else {
              reject(result)
            }
          } catch (e) {
            reject(e)
          }
        })
      }
    }
  }
}

Суммировать

В этой статье мы хотим проанализировать реализацию Promise на основе шаблонов публикации-подписки и наблюдателя, начиная с эволюции асинхронного программирования, функций обратного вызова для публикации-подписки и шаблонов проектирования наблюдателя, а затем обнаружить, что шаблоны проектирования Promises и наблюдателя схожи, поэтому Сначала с этой точки зрения анализируется реализация Promise.Конечно, функция Promise далека от этого, поэтому в данной статье анализируется принцип реализации распространенных методов Promise. Появление промисов изменило традиционный метод асинхронного программирования, сделав JavaScript более гибким в асинхронном программировании, а код более удобным для сопровождения и читабельным. Поэтому, как перспективный фронтенд, вы должны иметь определенное представление о реализации Promise.