Promise对象 3 种妙用

更新日期: 2019-12-16阅读: 2k标签: 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 是否有意义,又或者中途改变异步组件的实现流程是否真的对业务有所帮助也是值得思考的。

博客地址


链接: https://fly63.com/article/detial/6864

你真的了解 Promise 吗?Promise 必知必会(十道题)

Promise 想必大家十分熟悉,想想就那么几个 api,可是你真的了解 Promise 吗?本文根据 Promise 的一些知识点总结了十道题,看看你能做对几道。

剖析Promise内部结构,一步一步实现一个完整的、能通过所有Test case的Promise类

本文写给有一定Promise使用经验的人,如果你还没有使用过Promise,这篇文章可能不适合你,Promise标准中仅指定了Promise对象的then方法的行为,其它一切我们常见的方法/函数都并没有指定.

Async/Await替代Promise的6个理由

Async/Await替代Promise的6个理由:Async/Await是近年来JavaScript添加的最革命性的的特性之一。它会让你发现Promise的语法有多糟糕,而且提供了一个直观的替代方法。

Promise 原理解析与实现(遵循Promise/A+规范)

Promise是JS异步编程中的重要概念,异步抽象处理对象,是目前比较流行Javascript异步编程解决方案之一,Promise 是一个构造函数, new Promise 返回一个 promise对象 接收一个excutor执行函数作为参数

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

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

数组的遍历你都会用了,那Promise版本的呢

在对数组进行一些遍历操作时,发现有些遍历方法对Promise的反馈并不是我们想要的结果。async/await为Promise的语法糖,文中会直接使用async/await替换Promise;map可以说是对Promise最友好的一个函数了,

Promise使用时应注意的问题

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

es6 Promise 的基础用法

想必接触过Node的人都知道,Node是以异步(Async)回调著称的,其异步性提高了程序的执行效率,但同时也减少了程序的可读性。如果我们有几个异步操作,并且后一个操作需要前一个操作返回的数据才能执行

关于 Promise 的 9 个提示

你可以在 .then 里面 return 一个 Promise,每次执行 .then 的时候都会自动创建一个新的 Promise,对调用者来说,Promise 的 resolved/rejected 状态是唯一的,Promise 构造函数不是解决方案,使用 Promise.resolve

手写一款符合Promise/A+规范的Promise

Promise的一些用法在此不多赘述,本篇主要带领你手写一个Promise源码,学完你就会发现:Promise没有你想象中的那么难.本篇大概分为以下步骤:实现简单的同步Promise、增加异步功能、增加链式调用then、增加catch finally方法、增加all race 等方法、实现一个promise的延迟对象defer、最终测试

点击更多...

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