learn icon indicating copy to clipboard operation
learn copied to clipboard

ES6 系列之 Promise 及异步处理的几种方式

Open yangtao2o opened this issue 4 years ago • 0 comments

Promise 定义

总结一下回调函数的情况:

  • 回调函数执行多次
  • 回调函数没有执行
  • 回调函数有时同步执行有时异步执行

对于这些情况,可能都要在回调函数中做些处理,并且每次执行回调函数的时候都要做些处理,这就带来了很多重复的代码。

回调地狱的其他问题:

  • 难以复用
  • 堆栈信息被断开
  • 借助外层变量

Promise 使得以上绝大部分的问题都得到了解决。

  1. 嵌套问题
request(url)
  .then(function(result) {
    return writeFileAsynv("1.txt", result);
  })
  .then(function(result) {
    return request(url2);
  })
  .catch(function(e) {
    handleError(e);
  });
  1. 控制反转再反转

使用第三方回调 API 的时候,可能会遇到如下问题:

  1. 回调函数执行多次
  2. 回调函数没有执行
  3. 回调函数有时同步执行有时异步执行

对于第一个问题,Promise 只能 resolve 一次,剩下的调用都会被忽略。

对于第二个问题,我们可以使用 Promise.race 函数来解决。

对于第三个问题,即使 promise 对象立刻进入 resolved 状态,即同步调用 resolve 函数,then 函数中指定的方法依然是异步进行的。

PromiseA+ 规范也有明确的规定:

实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。

Promise 的局限性

  1. 错误被吃掉

其实这并不是 Promise 独有的局限性,try catch 也是这样,同样会捕获一个异常并简单的吃掉错误。

而正是因为错误被吃掉,Promise 链中的错误很容易被忽略掉,这也是为什么会一般推荐在 Promise 链的最后添加一个 catch 函数,因为对于一个没有错误处理函数的 Promise 链,任何错误都会在链中被传播下去,直到你注册了错误处理函数。

  1. 单一值

Promise 只能有一个完成值或一个拒绝原因,当需要传递多个值时,构造成一个对象或数组,然后再传递,then 中获得这个值后,又会进行取值赋值的操作。使用 ES6 的解构赋值:

Promise.all([Promise.resolve(1), Promise.resolve(2)]).then(([x, y]) => {
  console.log(x, y); // 1 2
});
  1. 无法取消

Promise 一旦新建它就会立即执行,无法中途取消。

  1. 无法得知 pending 状态

当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

异步处理的几种方式

题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯两秒亮一次,不断交替循环

先定义下红绿灯:

function red() {
  console.log("red");
}
function green() {
  console.log("green");
}
function yellow() {
  console.log("yellow");
}

异步编程的语法目标,就是怎样让它更像同步编程,有以下几种:

  • 回调函数实现
  • 事件监听 event
  • 发布订阅 Publish/Subscribe
  • Promise 和 Generator
  • Async/await

一、回调函数

这是最常见的一种方式,把函数作为参数送入,然后回调。

第一版:简单明了

function step() {
  console.log("wait for about 3 seconds...");
  setTimeout(() => {
    red();
    setTimeout(() => {
      green();
      setTimeout(() => {
        yellow();
        step();
      }, 2000);
    }, 1000);
  }, 3000);
}

step();

第二版:封装定时器

var light = (timmer, cb) => {
  setTimeout(() => {
    cb();
  }, timmer);
};

function step(cb) {
  light(3000, () => {
    red();
    light(1000, () => {
      green();
      light(2000, () => {
        yellow();
        step();
      });
    });
  });
  typeof cb === "function" && cb();
}

step(() => console.log("wait for about 3 seconds..."));

二、事件监听

采用事件驱动模式。任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

第一版:监听一个事件,然后触发这个事件,并且执行事件里的回调函数

// 引入 Node events 模块
const events = require("events");
const emitter = new events.EventEmitter();

// 监听
emitter.on("lightEvent", str => console.log(str));

// 触发
emitter.emit("lightEvent", "red");
emitter.emit("lightEvent", "green");
emitter.emit("lightEvent", "yellow");

// 输出
// red
// green
// yellow

第二版:加个顺序执行

// 引入 Node events 模块
const events = require("events");
const emitter = new events.EventEmitter();

const lightHandler = (timmer, cb) => {
  setTimeout(() => {
    cb();
  }, timmer);
};

// 监听
emitter.on("lightEvent", str => console.log(str));

// 触发
function step() {
  lightHandler(3000, () => {
    emitter.emit("lightEvent", "red");
    lightHandler(1000, () => {
      emitter.emit("lightEvent", "green");
      lightHandler(2000, () => {
        emitter.emit("lightEvent", "yellow");
        step();
      });
    });
  });
}

step();

依旧是回调执行,我们继续远征吧。

三、发布/订阅

"事件",完全可以理解成"信号"。

我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern),又称"观察者模式"(observer pattern)。 - 阮一峰

订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

第一版:

const publisher = {
  // 缓存列表
  lists: {},
  // 订阅
  subscribe: function(event, handler) {
    (this.lists[event] || (this.lists[event] = [])).push(handler);
  },
  // 发布
  publish: function() {
    const event = [].shift.call(arguments);
    const events = this.lists[event];

    if (!events || events.length === 0) {
      return false;
    }

    events.forEach(item => {
      item.apply(this, arguments);
    });
  }
};

// 订阅
publisher.subscribe("lightEvent", red);
publisher.subscribe("lightEvent", green);
publisher.subscribe("lightEvent", yellow);

// 发布
publisher.publish("lightEvent");

第二版:

const publisher = {
  // 缓存列表
  lists: {},
  // 订阅
  subscribe: function(event, handler) {
    (this.lists[event] || (this.lists[event] = [])).push(handler);
  },
  // 取消订阅
  unsubscribe: function(event, handler) {
    const events = this.lists[event];
    if (!events) {
      return false;
    }
    if (!handler) {
      events && (events.length = 0);
    } else {
      events.forEach((item, i) => {
        if (item === handler) {
          events.splice(i, 1);
        }
      });
    }
  },
  // 发布
  publish: function() {
    const event = [].shift.call(arguments);
    const events = this.lists[event];

    if (!events || events.length === 0) {
      return false;
    }

    events.forEach(item => {
      item.apply(this, arguments);
    });
  }
};

const lightHandler = (timmer, cb) => {
  setTimeout(() => {
    cb();
  }, timmer);
};

const colorHandler = color => console.log(color);

// 订阅
publisher.subscribe("redEvent", colorHandler);
publisher.subscribe("greenEvent", colorHandler);
publisher.subscribe("yellowEvent", colorHandler);

function step() {
  lightHandler(3000, () => {
    publisher.publish("redEvent", "red");
    lightHandler(1000, () => {
      publisher.publish("greenEvent", "green");
      lightHandler(2000, () => {
        publisher.publish("yellowEvent", "yellow");
        step();
      });
    });
  });
}

step();

三、Promise

直接上代码:

var light = (timmer, cb) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      cb();
      resolve();
    }, timmer);
  });
};

var step = () => {
  Promise.resolve()
    .then(() => {
      return light(3000, red);
    })
    .then(() => {
      return light(1000, green);
    })
    .then(() => {
      return light(2000, yellow);
    })
    .then(() => {
      step();
    })
    .catch(err => console.log(err));
};

step();

四、Generator

Promise 的写法减少了好多回调,但是仍有回调的存在,这次尝试使用 Generator,看是否能够避免回调。

const light = (timmer, cb) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      cb();
      resolve();
    }, timmer);
  });
};

function* gen() {
  yield light(3000, red);
  yield light(1000, green);
  yield light(3000, yellow);
}

const iterator = gen();

const step = (gen, iterator) => {
  const s = iterator.next();
  // 返回 { value: Promise { <pending> }, done: false }
  if (s.done) {
    step(gen, gen());
  } else {
    // value 返回 Promise 对象
    s.value.then(() => {
      step(gen, iterator);
    });
  }
};

step(gen, iterator);

五、Async/await

有了 Generator 做铺垫,async/await 就比较容易理解了:

const light = (timmer, cb) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      cb();
      resolve();
    }, timmer);
  });
};

async function step() {
  await light(3000, red);
  await light(1000, green);
  await light(2000, yellow);
  step();
}

step();

同步写法,容易理解,和我们的线性思考方式一致,async/awaitES2017 的方案。

学习资料

yangtao2o avatar Apr 06 '20 05:04 yangtao2o