嵌套太深的代码可读性差,可维护性差。让人产生“敬畏感”。比如:
fetchData1(data1 =>
fetchData2(data2 =>
fetchData3(data3 =>
fetchData4(data4 =>
fetchData5(data5 =>
fetchData6(data6 =>
fetchData7(data7 =>
done(data1, data2, data3, dat4, data5, data6, data7)
)
)
)
)
)
)
)
本文介绍 5 种嵌套太深的场景及解决方案。
场景1: 回调地狱
用回调函数的方式来处理多个串行的异步操作,会造成嵌套很深的情况。俗称“回调地狱”。如:
fetchData1(data1 =>
fetchData2(data2 =>
fetchData3(data3 =>
done(data1, data2, data3)
)
)
)
解决方案
方案1: Promise
用 Promise 可以将串行的异步,处理成链式调用。用 Promise 改写上面的代码如下:
let data1, data2, data3
fetchData1()
.then(data => {
data1 = data
return fetchData2()
})
.then(data => {
data2 = data
return fetchData3()
})
.then(data => {
data3 = data
done(data1, data2, data3)
})
改完心情好了很多~
注意:上面 fetchData1
,fetchData2
,fetchData3
的返回值的必须都是 Promise 对象。类似:
function fetchData1 () {
return new Promise(resolve) {
...
// 异步回来了
resolve(data)
}
}
如果这几个异步可以并行操作,可以这么写:
Promise.all([
fetchData1(),
fetchData2(),
fetchData3()
]).then(([data1, data2, data3]) => {
done(data1, data2, data3)
})
方案2: async/await
async/await 比用 Promise 更优雅。用 async/await 改写上面的代码如下:
async function fetch() {
const data1 = await fetchData1()
const data2 = await fetchData2()
const data3 = await fetchData3()
done(data1, data2, data3)
}
注意:上面 fetchData1
,fetchData2
,fetchData3
的返回值也必须都是 Promise 对象。同时,用 await 的函数,必须在函数名前加 async。
场景2: if 嵌套
在条件语句中,如果判断条件很多,会出现嵌套很深或判断条件很长的情况。比如,判断一个值是否是: 1 到 100 之间,能被 3 和 5 整除的偶数。这么写:
const isEvenNum = num => Number.isInteger(num) && num % 2 === 0
const isBetween = num => num > 1 && num < 100
const isDivisible = num => num % 3 === 0 && num % 5 === 0
if (isEvenNum(num)) { // 是偶数
if(isBetween(num)) { // 1 到 100 之间
if(isDivisible(num)) { // 能被 3 和 5 整除
return true
}
return false
}
return false
}
return false
解决方案
方案1: 将判断条件结果放在数组中
将判断条件结果放在数组中,可以将嵌套的条件判断,改成扁平的遍历数组值的判断。代码实现如下:
return [isEvenNum(num), isBetween(num), isDivisible(num)]
.every(flag => flag)
方案2: 用第三方库 Akua
Akua 可以将条件嵌套转化成链式调用。代码实现如下:
const flag = false
new akua()
.inject(isEvenNum(num), 'isEvenNum', () => {
console.log('是偶数')
})
.inject(isBetween(num), 'isEvenNum->isBetween', () => {
console.log('1 到 100 之间')
})
.inject(isDivisible(num), 'isBetween->isDivisible', () => {
console.log('能被 3 和 5 整除')
flag = true
})
.parse()
return flag
场景3: 函数调用嵌套
执行多个函数调用,每个函数输出是下个函数的输入,会造成很深的嵌套。如:
toTable( // 第四步: 上桌
fry( // 第三步: 炒蛋
handle( // 第二步: 打蛋
buy(20) // 第一步: 买蛋
)
)
)
上面的代码模拟的是炒蛋的过程:买蛋 -> 打蛋 -> 炒蛋 -> 上桌。
解决方案
方案1: 分成多步写
分成多步写,用临时变量接收上个函数的调用结果。实现代码如下:
let egg = buy(20)
egg = handle(egg)
egg = fry(egg)
egg = toTable(egg)
console.log(egg)
方案2: 封装成函数
用递归的方式,把上一个函数的执行结果,传递到下一个函数。代码如下:
pipe(20, [
buy,
handle,
fry,
toTable
]);
function pipe(prev, fnArr) {
if(fnArr.length > 0) {
const res = fnArr.shift()(prev)
return pipe(res, fnArr)
}
return prev
}
场景4: React 高阶组件嵌套
在 React 写的应用中,会出现一个组件被很多个高阶组件(HOC)包裹,造成嵌套很深的情况。如:
class Comp extends React.Component {...}
Wrapper5(
Wrapper4(
Wrapper3(
Wrapper2(
Wrapper1(Comp)
)
)
)
)
解决方案
方案1: 用类装饰器
写法如下:
@Wrapper5
@Wrapper4
@Wrapper3
@Wrapper2
@Wrapper1
class Comp extends React.Component {...}
注意:在项目中需要安装些依赖才能使用装饰器。如果是 Webpack 项目,要安装 @babel/plugin-proposal-decorators。具体见: 在React项目中使用自定义装饰器。
方案2: 将高阶组件改成自定义Hook
组件中用自定义Hook 是扁平的结构,不存在嵌套。将类组件改成函数组件,将高阶组件改成自定义Hook 可以解决嵌套的问题。写法如下:
function Comp () {
const tool1 = useWrapper1(...)
const tool2 = useWrapper2(...)
const tool3 = useWrapper3(...)
const tool4 = useWrapper4(...)
const tool5 = useWrapper5(...)
}
这个方案对原有代码的改动很大,仅做参考。
场景5: React Context 嵌套
在 React 写的应用中,可以用 Context 来管理子组件间的数据共享。如果共享数据很多,而且类型不同,容易造成顶部组件 Context 嵌套很深。如:
<PageContext.Provider
value={...}
>
<User.Provider
value={...}
>
<Project.Provider
value={...}
>
<ChildComp />
</Project.Provider>
</User.Provider>
</PageContext.Provider>
解决方案
可以用分成多步写的方式解决上面的嵌套。实现代码:
let content = <ChildComp />
content = (
<Project.Provider
value={...}
>
{content}
</Project.Provider>
)
content = (
<User.Provider
value={...}
>
{content}
</User.Provider>
)
content = (
<PageContext.Provider
value={...}
>
{content}
</PageContext.Provider>
)
总结
嵌套会导致代码的可读性和维护性很差。要解决嵌套问题,本质上是将嵌套的代码转化为扁平的代码。