背景
在开发中,一些复杂的页面会涉及到令人头疼的交叉逻辑,先来看两个简单的例子:
- 交叉的接口逻辑
打开页面,同时发出两个并行请求A、B,A返回后需要处理逻辑1,B返回后需要处理逻辑2,在A的回调逻辑1中,有逻辑1-1依赖了接口B返回的数据,在B的回调逻辑2中,有一段逻辑2-1依赖了接口A返回的数据
- 交叉的校验逻辑
父组件是一个大的表单页,其中包含了数个小表单,都各自抽象成单独的子组件。点击父组件的提交按钮时,需要对所有的子组件进行校验,根据不同的校验结果,需要在父组件和子组件中同时显示报错信息
交叉接口
对于例子1,逻辑1-1和逻辑2-1其实相当于同时依赖了接口A和B,很容易就会联想到Promise.all,于是我们大概会写下如下代码:
const requestA = axios.get('xxx') const requestB = axios.get('xxx') requestA.then(callback1) requestB.then(callback2) Promise.all([requestA, requestB]).then(callback3) // 处理逻辑1-1和逻辑2-1
回头看看自己的代码,实现优美,逻辑清晰,脸上露出了满意的微笑
但是事实可能比这还要复杂一点
还有一个接口C的入参依赖接口A返回的参数,同时接口C的回调逻辑3依赖接口A、B返回的参数
显然接口C只能在A返回之后才能发出,但是他又依赖了B返回的参数,这时候就只能在callback3中进行处理
const requestA = axios.get('xxx') const requestB = axios.get('xxx') let requestC requestA.then(res => { requestC = axios.get('xxx', res.x) callback1(res) }) requestB.then(callback2) Promise.all([requestA, requestB]).then(res => { callback3(res) requestC.then(callback4) })
实现优美,逻辑清晰 X2
事实上,不论是情况1还是情况2,看起来的优美都只是伪代码造成的假象逻辑上来说是完全正确的,但是真正实现的时候会导致代码变得非常丑陋。
想象一下,对于每一个接口的返回值,我们都需要对数据做一系列的操作,操作过程中会产生很多临时的局部变量来帮助运算,对于同时依赖了多个接口返回值的逻辑处理来说,很多局部变量是完全可以复用的,但是由于代码顺序我们不得不重复一次逻辑或者将局部变量改为全局变量,这两种都不是好的实现。
这时候,主角一号闪亮登场了——Deferred
Deferred原本是一个已经被废弃的Promise规范,但是在某些场景下他真的非常香,所以我们需要自己实现一个Deferred类,代码非常简单
class Deferred { promise reject resolve constructor() { this.promise = new ***Promise\***((resolve, reject)=> { this.reject = reject this.resolve = resolve }) } }
当我们创建一个Deferred实例后,可以在任意时刻改变其状态,让我们看看用Deferred改写之后的代码长什么样:
// 先定义当前页面需要处理的请求序列 const ***RequestList\*** = (() => { const o: any = {} ;['requestA', 'requestB', 'requestC'].forEach(i => { o[i] = new Deferred() }) return o })() const requestA = axios.get('xxx') const requestB = axios.get('xxx') requestA.then(res => { const temp = res.map(xxx) // 操作A产生的临时变量 callback1(res) ***RequestList\*****.**requestB.promise.then(({ resB, tempB })=> { // 这段逻辑本来在Promise.all中,但是现在我们可以在A的回调中书写他并可以复用A、B计算过的所有中间变量 callback3(res, temp, resB, tempB) }) const requestC = axios.get('xxx', res.x).then(callback4) }) requestB.then(resB => { const tempB = resB.map(xxx) // 操作B产生的临时变量 ***RequestList\*****.**requestB.resolve({ resB, tempB }) // 标记接口B已经返回,并resolve了可复用的变量temp })
初次使用可能有点反直觉,但是好好使用会让代码变得非常简练
交叉校验
交叉校验的痛点在于父子组件的通信会导致校验的代码变得很零散,有时为了方便我们可能还会用$refs调用子组件的校验方法,这使得维护成本大大提高,并且也存在上述的复用局部变量的问题。我在对公的代码中实现了一套专门用于校验的通信机制,具体实现就不展示了,下面是其使用方式:
// 父组件 const list = ['component1', 'component2'] // 定义所有需要校验的子组件 const busMap = { validateAll() { return ***Promise\***.all((***Object\***.values(this)).filter(i => i.value).map(i => i.value.validate())) } } list.forEach(k => { const key = ***Math\***.random() // 生成随机的key保持联系 busMap[k] = { key, value: validateBus(key) } }) this.validateBusMap = busMap this.validateBusMap.component1.value.$on('failed', (e, msg) => { // 父组件报错展示 callback1() }) this.validateBusMap.component2.value.$on('failed', () => { callback2() }) // 父组件点击提交,对所有表单进行校验 save() { try { await this.validateBusMap.validateAll() } catch(e) { // 这里不再需要写报错提示的逻辑,所有逻辑都被集中在了校验声明的地方 } ... } // 子组件 this.validator = new Validator({ // 初始化表单校验规则 data: xxx, config: xxx, field: xxx }) this.validateBus.setValidator(this.validator) // 设置校验实例,父子同步 this.validateBus.$on('xx', callback) // 父组件可以向子组件传递时间来修改校验规则或者触发子组件的提示信息