Promise对象 3 种妙用

9012 年底,做为一个前端,说不了解 Promise 对象用法的基本不存在,这里就不对功能用法进行介绍了。但本文将会讲述你可能不知道的 Promise 3 种奇妙用法。固然,每种用法都会有其适用的特殊场景。html

Promise 对象能够缓存

需求

对于一个对象而言,可以被缓存并非一件难以理解的事情。缓存使用的意义每每是为了解决性能问题。而对于一个特定请求的 Promise 对象而言,缓存的意义在于同时多个组件的使用该请求,会由于请求未返回而进行屡次请求。一图胜千言,图示以下:前端

别以为丑

由于在某些特定需求或者场景下(甚至由于团队的因素),某个组件在能够在页面单独使用,也能够结合其余组件共同使用。若此时多个组件都须要对某个通用数据进行请求,就会发生屡次请求,对性能不利。但若是所有移植到父组件去请求,又是须要一顿操做,对开发不爽。vue

解决方案

因此这时候咱们基于 api 与 请求参数加缓存。先写一个生成 key 的函数(此函数仅仅只适用简单的请求参数,不适合对象等复杂数据结构,由于是通用型数据,不考虑太复杂的请求参数,若有需求能够自行改造)。ios

// 生成key值错误
const generateKeyError = new Error("Can't generate key from name and argument")

// 根据当前的请求参数生成 key 值
function generateKey(name, argument) {
    // 从arguments 中取得数据而后变为数组
    const params = Array.from(argument).join(',')
    
    try{
        // 返回 字符串,函数名 + 函数参数
        return `${name}:${params}`
    }catch(_) {
        // 返回生成key错误
        return generateKeyError
    }
}
复制代码

下面是数据请求缓存,不过使人以为惋惜的是: 数据请求缓存并不能解决屡次请求的问题。git

const dataCache = new Map()

async getxxx(params1, params2) {
   const key = generateKey('getxxx', [params1, params2]) 
    // 从data 缓存中获取 数据
    let data = dataCache.get(key)
    if (!data) {
        // 没有数据请求服务器
        const res = await request.get('/xxx')
        
        // 其余操做
        ...
        data = ...

        // 设置数据缓存
        dataCache.set(key, data)

    }
    return data
} 
复制代码

由于虽然 js 是单线程的,因此在第二个以及以上的组件请求时候,会由于请求未返回而进行再次请求 api。流程以下:github

  • a 组件请求
  • dataCache.get == null
  • 创建请求(等待返回)
  • 其余操做
  • b 组件请求
  • dataCache.get == null
  • 创建请求(等待返回)
  • 其余操做
  • .... ...
  • 放入缓存且返回数据
  • 放入缓存且返回数据
  • .... ...

若是缓存的是 Promise 对象,则该方案能够解决问题。axios

const promiseCache  = new Map()

async getxxx(params1, params2) {
   const key = generateKey('getxxx', [params1, params2]) 
    // promiseCache 缓存中获取 缓存
    let xxxPromise = promiseCache.get(key);
    // 当前promise缓存中没有 该promise
    if (!xxxPromise) {
        xxxPromise = request.get('/getxxx').then(res => {
            // 对res 进行操做
            ...
        }).catch(error => {
            // 在请求回来后,若是出现问题,把promise从cache中删除 以免第二次请求继续出错
            promiseCache.delete(key)
            return Promise.reject(error)
        })
        promiseCache.set(key, promise)
    }
    return xxxPromise
} 
复制代码

流程以下:小程序

  • a 组件请求
  • promiseCache.get == null
  • 创建请求
  • 返回 promise
  • 其余操做
  • b 组件请求
  • promiseCache.get != null
  • 返回 promise
  • 其余操做
  • .... ...

同时,由于 promise 是异步操做,因此在发生错误时候 catch 中去除缓存以便于缓存了错误的promise。segmentfault

进一步了解与学习

该方案能够减轻同一时间屡次请求同一数据所带来的性能问题。后端

若是你还想结合过时时间与装饰器来对缓存进行赋能,能够参考我以前的博客文章 前端 api 请求缓存方案

Promise 能够封装大量异步操做

需求

在写关于异步请求时候,一般是基于请求直接返回 api 请求响应数据,对其进行正常和错误处理。当时屡次异步操做从而返回正确与错误的流程却不多进行梳理。若是在一次请求内有多个异步操做:代码就会变得难以维护。

解决方案

学习 Promise 时候,每每会与有限状态机结合在一块儿说,若是你实现过 Promise,你就清晰的知道: 若是内部没有状态没有发生变化,能够执行大量异步操做。体现为若是没有调用 resolve 或者 reject 函数,则不会对于当前 Promise 的状态和值进行修改,也就不会执行后面的链式调用。

// 异步操做封装
function asyncOpt(opt: any) {
  return new Promise((resolve, reject) => {
    // 传入的 opt 异步操做
      
    // 如 请求失败,失败的逻辑判断后再次请求 
    // 又如多个 异步操做, 在最后一个异步操做成功后执行
      
    reslove(result)


    // 多个 异步操做中的 catch, 在每一个错误中执行
    reject(error)
  })  
}


asyncOpt(data).then(result => {
  // 正常流程
}).catch(error => {
  // 错误流程
})

复制代码

写出如上的代码,就能够在不少业务项内进行操做,诸如某些操做有前置权限请求,或者某些错误代码须要从新请求或者埋点等操做。

进一步了解与学习

若是以为上述的例子不够复杂,不够体现出 Promise 封装的妙用,你能够研究关于微信登录态的管理。事实上,在没有知道这种用法以前,确实没有很好的办法解决这种问题。

wx-request

固然,github 上已经有了开源实践 weRequest,该库实现了无感知登录,且代码风格与结构很是值得学习,能够参考我以前的博客文章 从 WeRequest 登录态管理来聊聊业务代码

同时,能够封装异步操做可并不只仅只是指代异步请求,若是是你使用过Element confirm,必定对以下代码不陌生。

this.$confirm('此操做将永久删除该文件, 是否继续?', '提示', {
  confirmButtonText: '肯定',
  cancelButtonText: '取消',
  type: 'warning'
}).then(() => {
   this.$message({
    type: 'success',
    message: '删除成功!'
   });
}).catch(() => {
   this.$message({
     type: 'info',
     message: '已取消删除'
   });          
});
复制代码

这样的话,不须要再界面上写 confirm 以及一些控制显隐的代码,基于配置(字符串) 触发 promise 开始显示后销毁。

若是你完整引入了 Element,它会为 Vue.prototype 添加以下全局方法:msgbox,alert, confirm 和prompt。所以在 Vue instance 中能够采用本页面中的方式调用 MessageBox。调用参数为:

  • $msgbox(options)
  • $alert(message, title, options)$alert(message, options)
  • $confirm(message, title, options)$confirm(message, options)
  • $prompt(message, title, options)$prompt(message, options)

最后结合全局方法和 渲染函数 甚至也能够实现 Modal 配置化,传入组件,配置以及数据。能够相似于以下写法(固然,事实上用不用 Promise 均可以实现该方案,只不过 Promise 的状态转化很适合,与其本身实现一个状态机,倒不如使用promise):

this.$modal(xxxComponent, componentConfig, propConfig).then(result => {
    // 根据不一样返回结果来处理
}).catch(reason => {
    // 取消处理方法
})
// 甚至还能够加 finally 方法
复制代码

Promise 泄露触发转化方法

需求

最近有小伙伴来找我询问,如何解决后一个请求比前一个请求还要快,由于他写了输入实时查询的功能。我直接让他使用防抖函数,可是他告诉我他已经使用了 500ms 的防抖可是服务端仍旧是会存在问题。

原本考虑再前一个请求成功后再进行下一次,可是考虑到这个方案会慢点很明显,后面考虑请求惟一化,可是由于使用 axios 作请求库,该请求并不特殊,特殊化处理明显是增长了代码复杂度,也是不太好。

解决方案

后面他告诉我,他已经解决了此问题,由于 axios 有一个方法能够取消请求。也就是若是他进行下一个请求,便会取消上一个请求。下面代码是官方示例:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');
复制代码

其实我是知道 Promise 中是有 cancelble 提案,可是该提案在第一阶段就由于被谷歌的强烈反对而取消了,那么我就去看 axios 源码来看一看如何实现取消。下面代码在 xhr.js 中。

// 若是配置出现 cancelToken
if (config.cancelToken) {
  // Handle cancellation
  // 设定 处理取消方法
  config.cancelToken.promise.then(function onCanceled(cancel) {
    // 请求被置空,直接返回,以免出错
   	if (!request) {
      return;
    }
    // xhr.abort 取消请求
    request.abort();
    // 执行 reject 
    reject(cancel);
    // 请求置空
    request = null;
  });
}
复制代码

先谈谈 abort 函数,abort 是 xhr 对象中的方法,根据 mdn :

若是该请求已被发出,XMLHttpRequest.abort() 方法将终止该请求。当一个请求被终止,它的 readyState 属性将被置为0( UNSENT )。

这个请求指的是 http 请求,这样就会出现一个问题,基于http请求原理,当一个请求从客户端发出去以后,服务器端收到请求后,一个请求过程就结束了,这时就算是客户端abort这个请求,服务器端仍会作出完整的响应,只是这个响应客户端不会接收。因此实质上,后端仍是处理了请求,可是前端不对该方法进行处理。

其中 promise 取消 核心代码以下 CancelToken.js

function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }
  // 设定 resolvePromise
  var resolvePromise;
    
  // xhr config.cancelToken.promise.then 就是当前的 promise 
  this.promise = new Promise(function promiseExecutor(resolve) {
    // 设定 和导出 resolve
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

      
    token.reason = new Cancel(message);
    // 执行 resolvePromise
    resolvePromise(token.reason);
  });
}

/** * 对应使用中 source * axios.get('/user/12345', { * cancelToken: source.token * }) * source.cancel('Operation canceled by the user.'); */
CancelToken.source = function source() {
  var cancel;
  // 返回 cancel token 对象 
  var token = new CancelToken(function executor(c) {
    // 利用 excutor 来把取消函数导出来。也就是 CancelToken excutor函数
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};
复制代码

能够看到 axios 的代码关系仍是有必定的复杂度。固然也是由于当前 Promise 没有办法像 setTimeout 等一些方法在调用时候直接返回取消函数,因此不得不借助另外一个 promise 异步来处理。同时,也是把复杂度留给了本身,因此仍是须要多读几遍。 调用关系以下。

CancelToken 设定了取消的 promise 调用关系, xhr 在有请求配置 cancelToken 的状况下,将当前请求注入的 cancelToken 中的 then 结合,使得调用了 cancel 后能够直接改变 xhr 内部状态。

进一步学习

固然在频繁的页面跳转,同时还有定时请求时候,跳转中的数据请求实际上意义不大: 能够参考 vue axios请求 取消上一个页面全部请求 批量取消请求

取消请求问题其实较为小众的,大部分是能够从请求源头来解决的,同时也由于对于服务端的处理并无减轻,因此事实上不处理其实倒也没什么问题。可是其中也遇到太小程序中有一些全局的服务,在请求完成后因为触发不到页面数据而报错的问题。

虽然是小众问题,可是遇到该特定场景须要提供解决方案。

同时对于上面的异步操做组件来讲,泄露出 resolve 和 reject 函数以便于直接执行 then 或者 catch 是否有意义,又或者中途改变异步组件的实现流程是否真的对业务有所帮助也是值得思考的。

鼓励一下

若是你以为这篇文章不错,但愿能够给与我一些鼓励,在个人 github 博客下帮忙 star 一下。 博客地址

参考

Element confirm

Vue 渲染函数

axios

vue axios请求 取消上一个页面全部请求 批量取消请求

相关文章
相关标签/搜索