问题背景
我们项目中有个保存功能,但是这个保存是一个异步函数,内部很多逻辑,比如说校验表单数据,获取子组件数据,数据处理,数据提交给后端获取中间值,最后保存。说明一下,我们的项目是vue2项目,下面的方法其实都在methoths
里面this
指向vue
实例,大概逻辑是这样的,下面是简化逻辑:
javascript
// 入口函数,页面点击提交按钮执行的函数
aysnc function submitHandel() {
await saveData()
// 提示保存成功,并且刷新列表
this.showAlart('保存成功')
this.queryList()
}
// 保存函数
async function saveData(){
// 校验组件a表单数据
const {checkA, resultA} = this.$refs.comA.checkData()
if (!checkA) return Promise.reject()
// 校验组件b表单数据
const {checkB, resultB} = this.$refs.comB.checkData()
if (!checkB) return Promise.reject()
// 处理数据
const params = transactionData(resultA, resultB)
// 一堆其他处理,里面还有异步代码,最后保存提交
await saveApi(params)
}
整体逻辑大概就是上面这样的。结果在经历N多个迭代后,产品突然提了一个奇葩的需求,需要在保存过程中,增加一层校验,然后弹框告诉用户结果,然后用户只有点击确定后,才能保存数据。大概流程图和原型如下:
上面的需求导致一个很明显的结果,就是我们上面一部分函数的逻辑要挪到弹框里面的确认按钮之后了。改造后大概逻辑如下:
javascript
async function saveData(){
// 校验组件a表单数据
const {checkA, resultA} = this.$refs.comA.checkData()
if (!checkA) return Promise.reject()
// 校验组件b表单数据
const {checkB, resultB} = this.$refs.comB.checkData()
if (!checkB) return Promise.reject()
// 处理数据
const params = transactionData(resultA, resultB)
// 新需求,增加弹框校验
const newCheck = await checkApi(params)
if (newCheck) {
// 保存提交参数,我们实际需求不止这一个参数,有n多个参数后面要用到
this.submitParams = params
// 展示弹框
this.showDialog = true
}
else{
confirmSave()
}
}
// 弹框确认函数,需要挪动大量代码
function confirmSave() {
// 一堆其他处理,里面还有异步代码
await saveApi(this.params)
}
上面的改造很明显能看出有2个问题
- 我需要增加一个确认函数,将原来函数获取参数后的逻辑挪到用户点击确认后再执行
- 我需要增加一个变量来保存参数,因为参数需要在确认函数内提交到后端
重点来了
但是我们的代码不是上面那么少的逻辑,我在挪动后面的逻辑时,不是1行2行代码就能搞定的,我可能需要拷贝几十上百行代码,内部用的变量也不是一个,因为很多异步,很多层层嵌套,这样改造风险太大。于是我想到了我们这次的主角Generator
函数。
Generator用途
- 生成迭代器(Iterator) 利用next()来迭代下一项
js
function* generateNumbers() {
yield 1;
yield 2;
yield 3;
}
const iterator = generateNumbers();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
- 异步编程
javascript
function* asyncTask() {
const result1 = yield asyncOperation1();
const result2 = yield asyncOperation2(result1);
return result2;
}
// 使用异步操作执行生成器函数
const iterator = asyncTask();
const promise1 = iterator.next().value;
promise1.then((value1) => {
const promise2 = iterator.next(value1).value;
promise2.then((value2) => {
const finalResult = iterator.next(value2).value;
console.log(finalResult); // 输出最终结果
});
});
我这里场景跟上面的用途很像,首先我们是一个异步函数,提交完了需要在then
里面继续执行逻辑。第二个我们的弹框也可以认为是一次异步逻辑,用户点击确认,则为异步成功,点击否,则为异步结束。于是我将saveData
函数改为generator
函数,在需要弹框的时候增加yield
。用户确认之后继续执行next(true)
,用户点击否则next(false)
,让函数继续执行下去。这样好处很明显:
- 不用将参数后面的逻辑全部提取到新的函数,只需要弹出时增加yield来暂停执行
- 不用将保存用到的参数(params)挂载到this上(因为后面的逻辑需要用到这些参数)
下面是改造后的主要逻辑
javascript
// 页面点击保存执行的函数
aysnc function submitHandel() {
await middleSaveData()
// 提示保存成功,并且刷新列表
this.showAlart('保存成功')
this.queryList()
}
// 增加一个中间函数,来处理generatorFun函数的结果
function middleSaveData(){
// 写一个promise,提供resolve和reject给主函数结束了调用
return new Promise(resolve, reject) => {
this.generatorFun = saveData(resolve, reject)
// 开始执行主函数
this.generatorFun.next()
})
}
// 页面确认或取消的回调函数
function btnHandel(result){
// 这里的result就是用户反馈的 是 或 否
// 继续next,主函数会根据结果来最最终的逻辑resolve或者reject
this.generatorFun.next(result)
}
// 增加*号,改造为generator函数,增加2个参数,resolve和reject
async function* saveData(resolve, reject){
// 校验组件a表单数据
const {checkA, resultA} = this.$refs.comA.checkData()
if (!checkA) return Promise.reject()
// 校验组件b表单数据
const {checkB, resultB} = this.$refs.comB.checkData()
if (!checkB) return Promise.reject()
// 处理数据
const params = transactionData(resultA, resultB)
// 新需求,增加弹框校验
const newCheck = await checkApi(params)
if (newCheck) {
// 保存提交参数,我们实际需求不止这一个参数,有n多个参数后面要用到
this.submitParams = params
// 展示弹框
this.showDialog = true
// 重点在这里,增加yiled暂停还是执行,等待用户操作后再执行next继续往后跑
// 这里yield一次,相当于是一次很长时间的异步,由用户点击确认或取消来结束异步
const result = yield
// 用户拒绝
if (!result) {
reject()
}
}
// 一堆其他处理,里面还有异步代码
await saveApi(params)
// 提交完成
resolve()
}
改造完毕,总体来看修改的点没有减少,但是最主要的是我们少了一个挪动大量代码(校验之后的部分)的逻辑。减少了bug
的产生,因为后续逻辑是多个补丁版本加上的,并不是同一个开发来实现的,如果对需求不了解,很容易造成bug
和大量的测试。上面的改造用到了generator
函数增加yield
来暂停逻辑,让用户反馈确认或取消后继续执行,来实现我们的需求,而主要的saveData
几乎的逻辑几乎没有改变,只是增加了一个yield
逻辑,对原来的逻辑没有破坏性。
最后
generator
是es6(2015年)增加的,一直知道这个特性,奈何一直没有用武之地,这次虽然有点牵强,但总数用到实战中了,还是挺开心的。
很多时候我们学到一个知识点,然后用到项目中才算真正学以致用,但前提是你要知道有这个知识点,像这样的知识点公众号已经收录了500+。可以关注我的公众号:程序员每日三问。每天向你推送面试题,算法及干货,期待你的点赞和关注。