Promise中同步代码的正确编写方式及代码合理性咨询
我刚看完James Snell的《Broken Promises》时也有过类似的困惑——把纯同步代码硬塞进Promise里确实很容易踩坑。咱们来一步步拆解你的代码,看看哪些地方需要调整,以及为什么。
核心原则回顾
James的核心观点非常明确:如果你的代码完全是同步执行的,就不要用new Promise()来包装它。这样做不仅会额外分配Promise对象带来性能开销,还会让代码逻辑变得冗余,甚至可能隐藏同步错误(因为Promise执行器里的同步错误会被转化为reject,而非直接抛出)。
1. parseHoleNumber:纯同步逻辑,完全不需要Promise
你的parseHoleNumber里所有操作都是同步的——数组过滤、位运算,没有任何异步操作。用new Promise()包裹完全是多余的。
原代码问题:
private parseHoleNumber(buffer: number[]): Promise<number[]> { return new Promise((resolve, reject) => { if (buffer.length < 0) { reject([]); // 错误场景返回空数组,不利于调试 } /* tslint:disable:no-bitwise */ resolve(buffer.filter(n => n !== 0).map(holeNumber => holeNumber >> 3)); /* tslint:enable:no-bitwise */ }); }
改进方案:
先写纯同步版本,如果需要和其他异步函数保持接口一致,再用Promise.resolve()包装:
// 优先写纯同步版本 private parseHoleNumber(buffer: number[]): number[] { if (buffer.length < 0) { throw new Error("Invalid buffer length: cannot be negative"); // 抛出有意义的错误 } /* tslint:disable:no-bitwise */ return buffer.filter(n => n !== 0).map(holeNumber => holeNumber >> 3); /* tslint:enable:no-bitwise */ } // 如果必须返回Promise(适配Promise.all的接口) private parseHoleNumber(buffer: number[]): Promise<number[]> { if (buffer.length < 0) { return Promise.reject(new Error("Invalid buffer length: cannot be negative")); } /* tslint:disable:no-bitwise */ const result = buffer.filter(n => n !== 0).map(holeNumber => holeNumber >> 3); return Promise.resolve(result); /* tslint:enable:no-bitwise */ }
2. sliceBufferIntoPieces:同样是纯同步,别用Promise包裹
这个函数里的for循环、数组push、slice都是同步操作,完全不需要new Promise()。另外原代码中reject([])的设计也不合理——空数组是业务合法场景还是错误?根据逻辑调整,但同步场景下直接返回或抛出错误更清晰。
原代码问题:
private sliceBufferIntoPieces( buffer: number[] | string[], chunkSize: number = 4, ): Promise<Array<number[]>> { const arr = []; return new Promise((resolve, reject) => { for (let i = 0; i < buffer.length; i += chunkSize) { arr.push(buffer.slice(i, i + chunkSize)); } arr.length > 0 ? resolve(arr) : reject([]); }); }
改进方案:
// 纯同步版本 private sliceBufferIntoPieces( buffer: number[] | string[], chunkSize: number = 4, ): Array<number[] | string[]> { const arr = []; for (let i = 0; i < buffer.length; i += chunkSize) { arr.push(buffer.slice(i, i + chunkSize)); } // 如果空数组是错误场景,抛出错误;否则直接返回空数组 if (arr.length === 0) { throw new Error("Buffer is empty, cannot slice into pieces"); } return arr; } // 如果必须返回Promise private sliceBufferIntoPieces( buffer: number[] | string[], chunkSize: number = 4, ): Promise<Array<number[] | string[]>> { const arr = []; for (let i = 0; i < buffer.length; i += chunkSize) { arr.push(buffer.slice(i, i + chunkSize)); } if (arr.length === 0) { return Promise.reject(new Error("Buffer is empty, cannot slice into pieces")); } return Promise.resolve(arr); }
3. parseCoordinate:没必要嵌套Promise,参数可以简化
你的parseCoordinate接收Promise<Array<number[]>>作为参数,然后用await buffer,其实可以直接让参数是同步数组(因为sliceBufferIntoPieces改成同步后,直接传数组即可)。另外,Promise.resolve(await buffer...)是冗余的——async函数本身会自动把返回值包装成Promise。
原代码问题:
private async parseCoordinate(buffer: Promise<Array<number[]>>): Promise<number[]> { const itemsAreZero = (item): boolean => item === 0; return Promise.resolve( (await buffer) // 没必要接收Promise参数 .filter(buff => !buff.every(itemsAreZero)) .map(byte => +(this.read4byteItem(byte) / 360000).toFixed(6)), ); }
改进方案:
// 简化参数为同步数组,去掉冗余的Promise.resolve private async parseCoordinate(buffer: Array<number[]>): Promise<number[]> { const itemsAreZero = (item): boolean => item === 0; // 如果read4byteItem是同步的,直接返回结果即可 return buffer .filter(buff => !buff.every(itemsAreZero)) .map(byte => +(this.read4byteItem(byte) / 360000).toFixed(6)); } // 如果read4byteItem是异步的,再用Promise.all处理 private async parseCoordinate(buffer: Array<number[]>): Promise<number[]> { const itemsAreZero = (item): boolean => item === 0; const filtered = buffer.filter(buff => !buff.every(itemsAreZero)); const results = await Promise.all(filtered.map(async byte => { const value = await this.read4byteItem(byte); return +(value / 360000).toFixed(6); })); return results; }
额外疑问解答
你担心的**"在new Promise执行器中向数组push元素是否安全"**——其实是安全的,因为Promise执行器是同步执行的,在resolve/reject之前,push操作已经完成了。但问题是你根本不需要用Promise来包裹这些同步操作,所以这个担心其实是多余的。
总结你的代码里的Promise误用点
- 不必要的
new Promise()包裹纯同步代码:parseHoleNumber、sliceBufferIntoPieces都是纯同步逻辑,完全不需要Promise包装,增加了不必要的开销。 - 错误处理不规范:用
reject([])这种方式返回错误,没有提供有意义的错误信息,不利于调试。 - 冗余的Promise.resolve:
parseCoordinate里的Promise.resolve(await buffer...)是多余的,async函数会自动把返回值包装成Promise。 - 参数设计复杂:
parseCoordinate接收Promise作为参数,其实可以简化为接收同步数组,让代码逻辑更清晰。
按照James的建议,优先写纯同步代码,只有当确实有异步操作(比如IO、网络请求)时才用Promise。如果为了接口一致性需要返回Promise,用Promise.resolve()/Promise.reject()直接包装结果,而不是用new Promise()。
内容的提问来源于stack exchange,提问作者JeffMinsungKim

