异步操作常用的一些方法修饰器

428 阅读3分钟

如何写方法修饰器

首先,修饰器是一个函数(高阶函数),它接收一个函数作为参数,并返回一个函数。

返回的函数所接收的参数同原函数,这样可以保持与原函数一致的使用方式。

function decorateFunc(fn) {
  // 闭包内部,可以做一些公共的操作
  return (...args) => {
    // 做一些前置动作,如处理参数等
    // 调用原函数
    const result = fn(..args);
    // 做一些后置动作,如处理原函数的结果等
    return result;
  }
}

来个例子

给一个异步操作添加超时装饰。

function timeoutDecorator(asyncFn) {
  const _wait = 1000;
  return (...args) => 
    new Promise((resolve, reject) => {
      // 这里需要了解,promise的状态一旦改变就无法再次改变
      asyncFn(...args).then(resolve, reject);
      setTimeout(() => reject('timeout'), _wait);
    });
}

function asyncFn() {
  return new Promise((resolve) => {
    setTimeout(() => resolve('223'), 1500);
  });
}

asyncFn = timeoutDecorator(asyncFn);
asyncFn();

使用Promise.race()

function timeoutDecorator(asyncFn) {
  const _wait = 1000;
  const genTimeoutPromise = 
    () => new Promise((_, reject) => setTimeout(reject, _wait));
  return (...args) => Promise.race([
    asyncFn(...args),
    genTimeoutPromise()
  ]);
}

上面的超时是固定为1000毫秒的,现在将其改为可通过传参设置。

function timeoutDecorator(wait) {
  const _wait = wait;
  return fn => 
    (...args) => 
      new Promise((resolve, reject) => {
        fn(...args).then(resolve, reject);
        setTimeout(() => reject('timeout'), _wait);
      });
}

// 使用
asyncFn = timeoutDecorator(1000)(asyncFn);
asyncFn();

其它常用装饰器方法

1. JSON解析

// 可以防止因为JSON.parse报错没有处理而终止程序
function jsonParseDecorator(fn) {
  return (...args) => 
    new Promise(resolve => {
      fn(...args).then(value => {
        try {
          resolve([null, JSON.parse(value)]);
        } catch (error) {
          resolve([error, null]);
        }
      });
    });
}

2. 挂起重复的调用

// 若某次调用还未有反馈结果(成功/失败),那么后续的重复调用将被挂起
// 待这次调用获得反馈结果,那么被挂起的后续调用立刻获得同样的反馈结果
function suspendDecorator(fn) {
  let _lock = false;
  const _waitingQueue = [];
  return (...args) => 
    new Promise((resolve, reject) => {
      if (_lock) {
        _waitingQueue.push([resolve, reject]);
      } else {
        _lock = true;
        const cb = resolveOrReject => value => {
          resolveOrReject(value);
          const idx = resolveOrReject === resolve ? 0 : 1;
          while (_waitingQueue.length) _waitingQueue.shift()[idx](value);
          _lock = false;
        }
        fn(...args).then(cb(resolve), cb(reject));
      }
      _lock = false;
    });
}

3. 调用结果缓存

// 若之前的调用有了成功的反馈,那么后面的调用将直接得到这个成功的反馈结果。
// time - 缓存时间(毫秒数)
// - 0 默认值,表示一直缓存
function cacheDecorator(time = 0) {
  const _time = time; // 缓存多久
  let _value = undefined; // 上次成功的反馈的结果
  let _cachedTime = 0; // 上次成功的反馈的缓存时间
  return fn =>
    (...args) => 
      new Promise((resolve, reject) => {
        if (
          _cachedTime &&
          (_time === 0 || _cachedTime + _time >= Date.now())
        ) {
          // 直接返回缓存结果
          resolve(_value);
        } else {
          fn(...args).then(value => {
            resolve(value);
            _value = value;
            _cachedTime = Date.now();
          }, reject);
        }
      });
}

4. 调用出错,返回默认值

// 当调用过程中发生错误,为了使程序进行下去,提供默认值
function defaultValueDecorator(defaultValue) {
  const _defaultValue = defaultValue;
  return fn =>
    (...args) => 
      fn(...args).catch(error => {
        // 自定义错误处理
        console.error(error);
        return _defaultValue;
      });
}

5.取消请求(2022-03-23补充)

// cancelRef = { value: false };
function cancelDecorator(cancelRef) {
  return asyncFn => 
    (...args) => 
      asyncFn(...args).then(res => {
        if (!cancelRef.value) return res;
        throw Error(`has canceled`);
      });
}

function createCancelRef() {
  return { value: false };
}

let cancelRef = createCancelRef();

function doCancel() {
  cancelRef.value = true;
  cancelRef = createCancelRef();
}

request = cancelDecorator(cancelRef)(request);

使用compose

现在同时对asyncFn进行多个方式装饰。

asyncFn = defaultValueDecorator('111')(
  cacheDecorator()(
    jsonParseDecorator(
      suspendDecorator(
        timeoutDecorator(1000)(
          asyncFn
        )
      )
    )
  )
);

如上面的这样的修饰方法的方式,很丑陋,也不够灵活(修改的话)。

compose

function componse(...decorators) {
  return fn => 
    decorators.reduceRight((prev, cur) => cur(prev), fn);
}

// 使用compose来修饰
asyncFn = compose(
  defaultValueDecorator('111'),
  cacheDecorator(),
  jsonParseDecorator,
  suspendDecorator,
  timeoutDecorator(1000)
)(asyncFn);

装饰器 Decorator

只能用于类和类的方法,不能用于函数,函数存在函数提升问题

class Demo {
  @defaultValueDecorator('111')
  @cacheDecorator()
  @jsonParseDecorator
  @suspendDecorator
  @timeoutDecorator(1000)
  asyncMethod() {
    // doSomething
  }
}

// 修饰方法需要改写一下
function timeoutDecorator(timeout) {
  const _timeout = timeout;
  return (target, name, descriptor) => {
    const fn = descriptor.value;
    descriptor.value = (...args) =>
      new Promise((resolve, reject) => {
        fn(...args).then(resolve, reject);
        setTimeout(() => reject("timeout"), _timeout);
      });
    return descriptor;
  };
}

Vue2源码中一些装饰方法(非异步)

代码文件: src/shared/util.js

once 函数只执行一次

/**
 * Ensure a function is called only once.
 */
export function once (fn: Function): Function {
  let called = false
  return function () {
    if (!called) {
      called = true
      fn.apply(this, arguments)
    }
  }
}

cached 缓存函数的执行结果

/**
 * Create a cached version of a pure function.
 */
export function cached<F: Function> (fn: F): F {
  const cache = Object.create(null)
  return (function cachedFn (str: string) {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  }: any)
}