Vue中watch清除过期副作用的案例详解
无敌小书包 人气:0在这里就不过多说watch的用法了,主要了解一下如何清除过期的副作用
通过一个案例来说吧:
一个可搜索的下拉选择器,用户第一次进行搜索的时候网速比较慢,请求虽然被服务端正确响应了,但是数据一直没有传输到客户端,用户看下拉数据没变化 就第二次搜索。第二次搜索之后网速恢复正常了,第二次请求的数据很快就客户端接收且正确渲染;紧接着第一次的数据也被客户端接收且客户端正确渲染。
这样就可能存在这样一种情况,第一次请求,服务端响应了请求但数据还没被客户端接收的时候,有人修改了数据;然后用户又点击刷新,响应数据 很快被客户端接收且处理,这个时候已经渲染的是最新的数据了。但是第一次请求的响应数据被客户端接收了,如果渲染的话,就不是最新的数据了。 因为第二次请求被成功处理后,第一次的请求就已经属于过期的了。
// 下拉选择器绑定的数据 const queryParams = reactive({ keyword: '' }) // 下拉列表渲染的数据 const listData = [] // 第几次搜索 let askNum = 0 // 每次搜索用的事件 const times = [5000, 100] // 第一次一共用了5s 第二次用了0.1s // 监视书的信息 watch(queryParams, async (newV) => { // 3.查询参数第一次发生变化,响应很慢需要5s // 6.查询参数第二次发生变化,响应非常快 asyncTaskIsExpired(times[askNum++], askNum) .then(() => { console.log(`第${askNum}个任务执行完毕`) // 渲染列表数据 renderSelectData() }) }) /* * @param time // 任务需要的时间 * @param count // 第几个任务 */ function asyncTaskIsExpired(time, count) { return new Promise((resolve) => { console.log(`第${count}个任务开始了`) setTimeout(() => { resolve() }, time) }) } const changeParams = function (str) { queryParams.keyword = str } function renderSelectData () { // do something console.log('渲染数据') } // 第一次搜索 changeBook('xxx') // 第一次搜索过了两秒还没有返回数据 setTimeout(() => { // 就开始了第二次搜索 changeBook('yyy') }, 2000)
更新后的逻辑:
// watch的第三个参数,可以注册一个过期回调,当这个副作用函数的执行过期时将标识修改为true // 换句话说,就是在watch内部每次检测到变化时,在副作用函数执行之前,会先执行通过onValidate注册的回调 watch(book, async (newV, oldV, onInvalidate) => { // 添加一个变量,标识上一次的请求是否过期 let expired = false // 默认是不过期的 onInvalidate(() => { // 过期时,将expired设置为true console.log('副作用函数已过期') expired = true }) // asyncTask(times[askNum++], askNum) asyncTaskIsExpired(times[askNum++], askNum) .then(() => { if (!expired) { console.log(`第${askNum}个任务执行完毕`) renderSelectData() } }) })
更新后执行流程就是: 第一次搜索数据一直没有返回,用户就进行第二次搜索,第二次搜索很快就响应了,客户端在处理第二次搜索响应的数据之前,先将上一次的expired
修改 为true,表示上一次请求已经过期,上一次请求即使成功(同时发起多个请求的时候),也不会执行后续操作了。
前面为什么说是“上一次的expired
”呢,看上面代码,expired实际上是存在于一个闭包中的,因此修改expired不会影响到本次请求。
onInvalidate的实现
// 递归读取对象中的数据(让对象中的所有key都有对应副作用函数) // 从而保证修改响应式对象的任意属性watch都能监听得到然后执行副作用函数 function traverse (value, seen = new Set()) { // 如果要读取的数据是原始值或者已经读取过了,那么什么都不做 if (typeof value !== 'object' || value !== null || seen.has(value)) return // 将数据添加到seen中,待变遍历地读取过了,避免循环引用引起的死循环 seen.add(value) // 假设value是一个对象,使用for-in读取对象的每一个值,并递归调用traverse进行处理 for (const key in value) { traverse(value[k], seen) } } function watch (source, cb, options) { let getter if (typeof source === 'function') { getter = source } else { // 如果watch监听的是一个对象,读取对象的所有属性 getter = () => traverse(source) } let oldValue, newValue // cleanup用来存储用户注册的过期回调 let cleanup function onInvalidate(fn) { // 过期回调保存到cleanup中 cleanup = fn } const job = () => { newValue = effectFn() // 调用回调函数cb之前,如果有国企的回调就先调用过期回调 if (cleanup) { cleanup() } // 将 onInvalidate 作为回调函数的第三个参数,以边用户使用 cb(newValue, oldValue, onInvalidate) oldValue = newValue } const effectFn = effect( () => getter, { lazy: true, scheduler: () => { if (options.flush === 'post') { const p = new Promise() p.then(job) } else { job() } } } ) }
这里 onInvalidate 实际上起到传递过期回调的作用。将程序本身应该处理的逻辑加入到了vue中(类似插件系统)。
加载全部内容