竞态的定义
竞态(race condition)是指在多线程编程中,多个线程对同一数据进行读写操作时,最终的结果取决于各个线程的执行顺序
竞态问题
竞态问题是由于多个线程之间的相互影响而导致的,这种影响可能会导致程序出现不可预测的结果
在程序中,竞态问题通常会出现在共享资源的访问上,如共享内存、文件、网络连接等。为了避免竞态问题,我们可以使用锁、信号量、互斥量等同步机制来保证共享资源的访问顺序
这个概念最早应该出自后端,在前后端分离后,到现在前端应用框架等技术越来越成熟完善后,前端领域里面也出现了竞态问题
前端中的竞态问题
在前端开发中,竞态问题通常会出现在异步请求的场景中,如搜索、分页、选项卡切换场景、保存提交、下载等等场景
例如搜索场景中的静态问题,用户输入关键字后,前端会向后端发送异步请求获取数据并展示。如果用户在请求返回前再次输入关键字,那么前端会再次向后端发送异步请求。如果第二次请求返回的速度比第一次请求快,那么就会出现第二次请求的数据覆盖了第一次请求的数据的情况
小扩展
之前写过的一篇文章中遇到的问题也可以称为竞态问题,这里面遇到不是单一性的问题,有各种复杂业务场景以及某些骚操作写法融合在在一起,因此不能通过单一对请求的拦截等处理全部解决项目中的问题,选择了从页面添加 loadind
层的方式解决的实际问题,有兴趣的可以看下
如何避免竞态问题
从请求方面来看,有两种方式,分别是 取消(终止)
和 忽略
请求
取消请求
fetch
, axios
中可以使用 AbortController
接口的 abort()
方法
fetch
下面是 vue2
中的 fetch
中使用 abort()
终止方法
mounted() { this.controller = new AbortController(); this.signal = this.controller.signal; }, methods: { download() { const url = "https://mdn.github.io/dom-examples/abort-api/sintel.mp4"; fetch(url, { signal: this.signal }) .then((response) => { console.log("Download complete", response); }) .catch((err) => { console.error(`Download error: ${err.message}`); }); }, abort() { this.controller.abort(); console.log("Download aborted"); }, },
点击下载按钮下载视频,然后点击终止后,请求终止的效果
axios
axios
新版本终止的效果和 fetch
用法一样
const controller = new AbortController(); axios.get('/foo/bar', { signal: controller.signal }).then(function(response) { ... }); controller.abort()
XMLHttpRequest
XMLHttpRequest
(XHR)对象可以使用 XMLHttpRequest.abort()
方法终止请求
var xhr = new XMLHttpRequest(), method = "GET", url = "https://xxx.xxx/api"; xhr.open(method, url, true); xhr.send(); if (OH_NOES_WE_NEED_TO_CANCEL_RIGHT_NOW_OR_ELSE) { xhr.abort(); }
忽略请求
通过封装 promise
请求的方式,判断请求是否结束,如果结束正常发送请求,如果没有结束则忽略当前请求
如下图示例,如果相同请求未结束提示请求正在进行中
请求封装
通过 pendingRequests map
变量来判断请求是否结束,如果变量为空,则添加到 map
对象中,否则就判断当前请求是否存在,如果存在则忽略当前请求,如果不存在,则添加当前请求到 map
对象中
pendingRequests: new Map() createRequestManager() { const self = this; function sendRequest(data) { console.log('data: ', data) console.log('pendingRequests: ', self.pendingRequests) if (!self.pendingRequests.has(data)) { const promise = self.makeAsyncRequest(data); promise .then((response) => { console.log('promise then') self.pendingRequests.delete(data); }) .catch((error) => { console.log('promise catch') console.error(error); self.pendingRequests.delete(data); }); self.pendingRequests.set(data, promise); console.log('self.pendingRequests', self.pendingRequests) } else { console.log(`Request for data: ${data} is already pending.`); } } return { sendRequest, }; },
调用函数
封装函数中的调用函数,这里使用 setTimeout
模拟实际请求
makeAsyncRequest(data) { return new Promise((resolve, reject) => { // 模拟异步请求,这里可以是实际的网络请求 setTimeout(() => { resolve(`Response for data: ${data}`); }, 2000); }); },
异步请求(添加忽略)
按钮点击事件
download2() { const requestManager = this.createRequestManager(); requestManager.sendRequest("Data 1"); },
防抖,节流
可以使用 lodash
库的 debounce
和 throttle
方法实现防抖和节流,也可以自定义函数实现类似功能,vue
可以使用全局注册的指令来实现防抖节流
Vue2 添加节流
main.js
添加全局指令
// 定义指令 Vue.directive('throttle', { bind: (el, binding) => { let throttleTime = binding.value // 防抖时间 if (!throttleTime) { // 用户若不设置节流时间,则默认2s throttleTime = 2000 } let cbFun el.addEventListener( 'click', (event) => { if (!cbFun) { cbFun = setTimeout(() => { cbFun = null }, throttleTime) } else { event && event.stopImmediatePropagation() } }, true ) } })
在按钮中使用,按需设置 v-throttle
参数即可
<div @click="save" v-throttle > <span style="">保存</span> </div>
示例代码地址
总结
关于前端方面的竞态的问题,请求终止都是使用 abort()
方法进行终止;忽略请求方面是使用了 map
对象进行记录判断请求是否存在的等方案;还可以使用 防抖节流
等方案处理问题;在实际业务场景中需要根据真实的业务场景合理考虑技术方案,在复杂的业务中可能也需要结合 loading
等UI层的效果来提升用户的体验