jyzwf / blog

在Issues里记录技术得点滴
17 stars 3 forks source link

实现一个类co库 #82

Open jyzwf opened 5 years ago

jyzwf commented 5 years ago

自己实现

先来了解下如何实现如下代码:

const getData = (name) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('My name is ' + name)
    }, 100) // 模拟异步获取数据
  })
}

const run = wrapAsync(function * (lastName) {
  const data1 = yield getData('Jerry ' + lastName)
  const data2 = yield getData('Lucy ' + lastName)
  return [data1, data2]
})

run('Green').then((val) => {
  console.log(val) // => [ 'My name is Jerry Green', 'My name is Lucy Green' ]
})

有没有很熟悉?没错,如果你了解 co 这个库的话,你就知道, wrapAsync 就相当于 co.wrap

一开始拿到这个题,自己也比较懵,因为自己很少使用 Generator,全都直接上手 async/await

大家都知道,Generator 函数如果碰到 yield 会暂停后面代码的执行,要调用 next 方法才能向下执行,每次调用 next 方法的时候,会返回一个对象,其中 value 表示当前 yield 的返回值,done,表示是否遍历完,如果是,则返回 true,否则返回 false。所以总的代码思路就是去循环判断这个 done 是否为 true,然后对后面 yield 的返回值做操作。

这里就直接讲 如何实现一个类似 co 函数,因为 co.wrap 只不过是 co 的一个简单封装,这里将该函数取名为 rco,以示区别。

先来看下 rco 的基本使用:

rco(function* () {
  var result = yield Promise.resolve(true);
  return result;
}).then(function (value) {
  console.log(value);
}, function (err) {
  console.error(err.stack);
});

分析

  1. rco 返回一个 Promise
  2. rco 接收一个函数作为第一个参数,后面参数作为该函数的参数值
  3. yield 后面可以是任何值
  4. 如果 yield 后的值被赋值给一个变量,那该变量就是此次 yield 后返回对象的 value 值
  5. 如果 yield 后面的值报错或者被 reject,且没有被 try...catch 包含,则直接 reject,否则就继续执行后面的遍历

实现

rco

function rco(fn, ...args) {
  return new Promise((resolve, reject) => {
    let step;
    if (isFunction(fn)) {
      // 判断是否是函数
      step = fn.apply(this, args);
    }
    // isGeneratorIterable: 判断下是否是 generator 函数执行后的值
    if (!step || !isGeneratorIterable(step)) return resolve(step);

    let next = step.next(); // 先走到 第一个 yield

    function helper(done) {
      if (!done) {
        // 1.
        Promise.resolve(value2Promise.call(this, next.value))
          .then(data => {
            try {
              // 2.
              next = step.next(data);
            } catch (e) {
              return reject(e);
            }

            helper(next.done); // 继续遍历后面的值
          })
          .catch(e => {
            try {
              //3.
              next = step.throw(e);
            } catch (e) {
              return reject(e);
            }
            // 4.
            helper(next.done);
          });
      } else {
        // 5.
        resolve(next.value);
      }
    }
    helper(next.done);
  });
}

上述代码说明

  1. 由于无法确定 yield 后面的值的类型,所以,这里全部转化为 promise,以方便判断
  2. 由于执行到下一个 yield 的过程中会出现错误,所以用 try...catch 包裹,如果使用者没有处理错误, rco 就会自动来处理该错误,同时 reject 掉。这里我们给 next 方法传入了一个 值,表示上一次 yield 的返回值,以保证用户可能需要改返回值,就好比上面的 基本使用,关于 next 传参,更多信息参见 next 方法的参数 。
  3. 如果 reject 了或者报错了,直接 throw 出来,如果使用者自己捕获了错误,则不会被此 try...catch 捕获,否则,就被该框架捕获,同时 reject 掉,这里利用了 generator 的一个 throw 方法,参见Generator.prototype.throw() 
  4. 如果被捕获了,就继续执行后续的遍历
  5. 此时 done = true,表示已经遍历完

value2Promise

上面说到了,我们会把 yield 后面的值全部转化为  promise,具体代码如下:

function value2Promise(val) {
  if (!val) return val;
  if (isPromise(val)) return val;
  if (isGeneratorFunction(val)) return rco.call(this, val);
  if (isGeneratorIterable(val)) return rco.call(this, curryIdentify(val)); // 1. yield f,f 为 generator 函数之后的返回值
  if (isFunction(val)) return thunkable2Promise.call(this, val);
  if (Array.isArray(val)) return array2Promise.call(this, val);
  if (isObject(val)) return object2Promise.call(this, val);

  return val;
}

关于 thunk

使用

可以说是让用户自己控制何时 resolve 

function get(val, err, error) {
    return function(done) {
        if (error) throw error;
        setTimeout(function() {
            done(err, val); // 通过 done 来决定返回值
        }, 10);
    };
}

rco(function*() {
    var a = yield get(1);
    assert.equal(1, a);
});

实现

export function thunkable2Promise(fn) {
    return new Promise((resolve, reject) => {
        fn.call(this, function(err, ...args) {
            if (err) return reject(err);
            if (args.length > 1) resolve(args);
            resolve(args[0]);
        });
    });
}

关于收集对象中的 promise 值

使用

因为对象里的属性值也可能是 promise,所以我们需要去收集这些值,直到所有的 promise 都 resolve了之后又,才是真正 resolve 了该对象

// 基本使用
function genPromise(val) {
  return new Promise((res, rej) => {
    setTimeout(() => {
      res(val);
    }, 1000);
  });
}

rco(function*() {
  var res = yield {
    a: genPromise(1),
    b: genPromise([213]),
    c: genPromise({ a: 1 })
  };

  assert.equal(3, Object.keys(res).length);
  console.log(res.a);
  console.log(res.b);
  console.log(res.c);
});

// 保证顺序
function timedThunk(time) {
  return function(cb) {
    setTimeout(cb, time);
  };
}

rco(function*() {
  var before = {
    sun: timedThunk(30),
    rain: timedThunk(220),
    moon: timedThunk(10)
  };

  var after = yield before;

  var orderBefore = Object.keys(before).join(",");
  var orderAfter = Object.keys(after).join(",");

  assert.equal(orderBefore, orderAfter);
});

实现

function object2Promise(obj) {
    return new Promise((resolve, reject) => {
        const results = new obj.constructor();
        const promises = []; // 手机所有的 promise
        Object.keys(obj).forEach((key) => {
            const target = obj[key];
            const promise = value2Promise.call(this, target);

            if (promise && isPromise(promise, promises)) {
                awaitPromise(promise, promises, key, results);
            } else {
                results[key] = promise;
            }
        });

        Promise.all(promises).then(() => resolve(results));
    }).catch((e) => {
        reject(e);
    });
}

function awaitPromise(target, promises, key, results) {
  results[key] = undefined; // 这里先保证处理完后的顺序与处理之前的顺序一样,因为可能 resolve 的时间是不一样的,就会导致顺序不一样
  let p = target
    .then(data => {
      results[key] = data;
    })
    .catch(e => {
      results[key] = e;
    });

  promises.push(p);
}

关于 rco.wrap

有了上面的基础,rco.wrap 只是基于上面的简单封装:

rco.wrap = function(fn) {
    return function(...args) {
        return rco.call(this, fn, ...args);
    };
};

对比 co

可以看到,上面我是直接利用了一个 递归 来实现的遍历,然后看下 co 内部是如何实现的,核心代码如下,总的来说,它不能进行处理基础数据,否则会报错,大概的原理和我的也差不多 😅😅

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1);

  // we wrap everything in a promise to avoid promise chaining,
  // which leads to memory leak errors.
  // see https://github.com/tj/co/issues/180
  return new Promise(function(resolve, reject) {
    if (typeof gen === "function") gen = gen.apply(ctx, args);
    if (!gen || typeof gen.next !== "function") return resolve(gen);

    onFulfilled();

    /**
     * @param {Mixed} res
     * @return {Promise}
     * @api private
     */

    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
      return null;
    }

    /**
     * @param {Error} err
     * @return {Promise}
     * @api private
     */

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    /**
     * Get the next value in the generator,
     * return a promise.
     *
     * @param {Object} ret
     * @return {Promise}
     * @api private
     */

    function next(ret) {
      if (ret.done) return resolve(ret.value);
      var value = toPromise.call(ctx, ret.value);
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      return onRejected(
        new TypeError(
          "You may only yield a function, promise, generator, array, or object, " +
            'but the following object was passed: "' +
            String(ret.value) +
            '"'
        )
      );
    }
  });
}

function toPromise(obj) {
  if (!obj) return obj;
  if (isPromise(obj)) return obj;
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}

最后

rco 已上传至 github,参见rco

如有不足,欢迎讨论 😊😊