关闭

Promise对象 3 种妙用

时间: 2019-12-16阅读: 730标签: Promise

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


Promise 对象是可以缓存

需求

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


因为在某些特定需求或者场景下(甚至因为团队的因素),某个组件在可以在页面单独使用,也可以结合其他组件共同使用。若此时多个组件都需要对某个通用数据进行请求,就会发生多次请求,对性能不利。但如果全部移植到父组件去请求,又是需要一顿操作,对开发不爽。


解决方案

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

// 生成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
    }
}

下面是数据请求缓存,不过令人觉得可惜的是: 数据请求缓存并不能解决多次请求的问题。

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。流程如下:

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

如果缓存的是 Promise 对象,则该方案可以解决问题。

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。

进一步了解与学习

该方案可以减轻同一时间多次请求同一数据所带来的性能问题。

如果你还想结合过期时间与装饰器来对缓存进行赋能,可以参考我之前的博客文章前端 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 封装的妙用,你可以研究关于微信登陆态的管理。事实上,在没有知道这种用法之前,确实没有很好的办法解决这种问题。


当然,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 添加如下全局方法:alert, 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 是否有意义,又或者中途改变异步组件的实现流程是否真的对业务有所帮助也是值得思考的。

博客地址


站长推荐

1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

2.广告联盟: 整理了目前主流的广告联盟平台,如果你有流量,可以作为参考选择适合你的平台点击进入

链接: http://www.fly63.com/article/detial/6864

关闭

简单模仿实现 Promise 的异步模式

这篇文章是考虑如何自己实现一个简单 Promise,用以理解 Promise。和原生 Promise的调用方法一样,支持链式调用,本文实现的方法只能用于参考Promise的原理,还有很多特性没有实现,比如 race,all 方法的实现。

异步堆栈追踪:为什么 await 胜过 Promise?

与直接使用 Promise 相比,使用 async/await 不仅可以使代码更具可读性,而且还可以在 JavaScript 引擎中实现一些有趣的优化。这篇文章是关于一个这样的优化,涉及异步代码的堆栈追踪。

从零实现一个promise

构造函数入参 executor 自执行函数。会在在 new 的时候同步执行,传入 resolve 和 reject 状态扭转函数。自执行函数内部根据异步任务执行结果(成功或失败)调用状态扭转函数,把状态传递给后续的 then。

Promise使用时应注意的问题

最近在使用axios库时遇到了个问题,后端接口报了500错误,但前端并未捕获到。在axios整体配置的代码中,过滤http code时,调用了filter401()、filter500(),但是这里注意并未将两个filter函数的结果返回,也就是并未返回promise,这就是导致问题出现的原因

关于javascript中的promise的用法和注意事项

promise是javascript中标准的内置对象,用于表示一个异步操作的最终状态(是失败还是成功完成)及其结果值。它让你能够把异步操作最终成功或者失败的原因和响应的处理程序相关联

你真的懂Promise吗

在异步编程中,Promise 扮演了举足轻重的角色,比传统的解决方案(回调函数和事件)更合理和更强大。可能有些小伙伴会有这样的疑问:2020年了,怎么还在谈论Promise?

把 Node.js中的回调转换为Promise

在 ES6 中引入了 Promise 作为这些问题的解决方案。最后通过引入 async/await 关键字来提供更好的体验并提高了可读性。即使有了新的方法,但是仍然有许多使用回调的原生模块和库

这样理解 promise

官网解释 promise 表示一个异步操作的最终结果。翻译 ==可以将promise理解为一个状态机==,它存在三种不同的状态,并在某一时刻只能有一种状态,一个promise是对一个异步操作的封装

Promise 使用、原理以及实现过程

promise 是目前 JS 异步编程的主流解决方案,遵循 Promises/A+ 方案。promise 对象原型上有一个 then 方法,then 方法会返回一个新的 promise 对象,并且将回调函数 return 的结果作为该 promise resolve 的结果

实现Promise的first等各种变体

本篇文章主要是想通过ES6中Promise提供的几个方法,来实现诸如first、last、none、any等各种变体方法!在标准的ES6规范中,提供了Promise.all和Promise.race两种

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!