Decorator @与 async 同时使用

目的

防止重复提交表单,所以设置了一个loading值来控制。 代码修改如下。

function setLoading(loading) {
  return function(target, name, descriptor) {
    const old = descriptor.value;
    descriptor.value = function() {
      // 获取vue/react 组件的isLoading属性
      const isLoading = this.data[loading];
      if(isLoading) return;
      this.data[loading] = true;

      old.apply(this, arguments);

      // 重置loading
      this.data[loading] = false;
      // 更新data到dom
      this.$update();
    };
    return descriptor;
  }
}

  @setLoading('isLoading')
  async getUserList() {
    // 请求后台接口
    const res = await backend.getUserList();
    if(res) {
      // 请求成功后的业务逻辑
    }
  }

有两个问题:

  1. 不知道我这个setLoading装饰器写的是不是对的。我看别人都是最后得return old.apply(this, arguments);但是我得将原来函数中间的逻辑在中间执行呃。。
  2. 由于getUserList是异步的函数,所以装饰器中的this.$update()这一步会提前backend.getUserList执行了。。最终导致数据请求回来了,但是dom无法更新了。

求大佬解答一下。。我在想可不可以在装饰器中加一个settimeout。。强行延迟this.$update(),不过感觉这样太low了

第三个问题: 我想基于上面的setLoading代码,实现如果请求时间大于300ms,才会显示loading的效果。

function setLoading(loading) {
  let timer = null;
  return function(target, name, descriptor) {
    const old = descriptor.value;
    descriptor.value = function() {
      // 获取vue/react 组件的isLoading属性
      const isLoading = this.data[loading];
      if(isLoading) return;
      
      // 这里设置一个300ms的定时器
      timer = setTimeout(() => this.data[loading] = true, 300);
      
      old.apply(this, arguments);
      
      // 重置loading与定时器timer
      clearTimeout(timer);
      this.data[loading] = false;
      // 更新data到dom
      this.$update();
    };
    return descriptor;
  }
}

然后我试了一下,手动把chrome网速调为slow 3g(网速很慢,请求时间 > 300ms). 效果符合预期,dom在loading结束后更新了data。但是正常网速下(请求时间<<<300ms),loading不会出现,数据也请求到了,但是dom的data并没有更新。

阅读 2.6k
1 个回答

方法装饰器简单介绍

方法的装饰器是通过修改原有的描述对象,返回新的描述对象来替换原有的方法.

function log(target, name, descriptor) {
  // target 是需要装饰方法的类的原型对象
  // name   是需要装饰方法的方法名
  // descriptor:{                 需要装饰方法的描述对象
  //   value: specifiedFunction,  原来的方法
  //   enumerable: false,         是否可遍历
  //   configurable: true,        描述符是否可被改变
  //   writable: true,            value 是否可悲改变
  // }
  const oldValue = descriptor.value
  const value = function value(...args) {
    console.log('log')            // 在前后添加需要的功能
    oldValue.apply(this, args)    // 执行原先的方法
  }
  return { ...descriptor, value }
}
class MyClass{
  @log
  function myFun(){}
}

解答

  1. 如果你原有的方法有返回值的话,你希望加了装饰器之后还继续让这个值返回,那就需要返回old.apply(this, arguments),如果不需要有返回值,那就可以不用返回
  2. 因为原来的方法是异步的,所以如果想要让update在后面执行的话,第一种方法用async await关键词,让原来的方法变为同步的.第二种方法就是把update放在原先方法的then里面执行

    function setLoading(loading) {
      return function(target, name, descriptor) {
        const old = descriptor.value
        descriptor.value = async function() {
          // 添加 async
          // 添加 async
          const isLoading = this.data[loading]
          if (isLoading) return
          this.data[loading] = true
    
          await old.apply(this, arguments) // 添加 await
    
          this.data[loading] = false
          this.$update()
    
          // 或者可以这样,把后面的处理逻辑放在 then 里面
          // 这样就可以等原来而方法执行完成, 之后在执行后面的 update
          // old.apply(this, arguments).then(() => {
          //   this.data[loading] = false
          //   this.$update()
          // })
        }
        return descriptor
      }
    }

补充

不会更新 dom 的话,你可以试试看分别在getUserListupdate输出看看是谁先执行的.
你评论里提到了react,我猜测你是用react的,getUserList没有返回值,那可能是通过state来同步你的数据的,而setState是一个异步操作,如果你在getUserList里面用了setState的话,也需要加上await来等待setState执行完成.否则更新的速度比较快,state还没来得及更改,所以 dom 不更新.
只是一般react都是直接用setState来触发渲染的,而你还有个update所以不太确定.
设置延时代码,你是要等请求之后等待 300ms 的,所以setTimeout应该写在后面,另外要添加asyncawait关键词,setTimeout是执行一次就销毁的,一般也不用去清理

function setLoading(loading) {
  let timer = null
  return function(target, name, descriptor) {
    const old = descriptor.value
    descriptor.value = async function() {
      const isLoading = this.data[loading]
      if (isLoading) return

      await old.apply(this, arguments)

      timer = setTimeout(() => {
        clearTimeout(timer)
        this.data[loading] = false
        this.$update()
      }, 300)
    }
    return descriptor
  }
}

一点小经验

当修改的方法为箭头函数的时候,描述符对象会不一样,需要做别的适配,因为太麻烦了,所以我一般都是直接用function,但是又不想每次用方法的时候都用myclass.myFun()的方式去调用,而是想直接用myFun()去调用,那就需要在外层在调用一个装饰器用来绑定this了,这样就不会丢失this,core-decoratorsautobind很好用.

// myClass.js
import { autobind } from 'core-decorators'
function log(target, name, descriptor) {
  const oldValue = descriptor.value
  const value = function value(...args) {
    console.log('log')
    oldValue.apply(this, args)
  }
  return { ...descriptor, value }
}
export default class MyClass{
  @autobind
  @log
  function myFun(){}
}

// other.js
import MyClass from './myClass'
const {myFun}=new MyClass()
myFun()

参考文章

推荐问题
宣传栏