- 小工具github地址
- 未完待续,以后会持续补充
小技巧
条件渲染组件陷阱
- 如果按这样来判断是否要渲染View
progress && <View/>,在progress是数字0时会报错
Text strings must be rendered within a
<Text>component
-
这是因为当
progress为0时,表达式0 && <View />的返回值为0,会被直接输出到视图树中。- js中的 && 机制是回第一个为假的对象或者最后一个对象
- progress如果是其他的是没问题的会被忽略,比如false,空字符串,null,undefined
-
解决方案:
- 不要偷懒写三元表达式
{progress ? <View /> : null} - 数值比较将前面转成boolean
{progress > 0 && <View />}
- 不要偷懒写三元表达式
防抖函数可能无效
- 当使用throttle函数来生成防抖函数,防止按钮短时间多次点击时,如果是按下面这样写,那么这个防抖会失效
ts
const [count, setCount] = useState(0);
// ❌ 点击函数直接由throttle生成,但是里面用了setState刷新了界面
// 导致函数重新生成了,新函数还是可以点
const handleClick = _.throttle(() => {
setCount(c => c + 1);
console.log('clicked', count);
}, 1000);
-
解决方案:
- 不用throttle,用一个变量根据时间判断
tsconst lastClickRef = useRef(0); const handleClick = useCallback(() => { const now = Date.now(); // 1 秒内直接忽略 if (now - lastClickRef.current < 1000) { return; } lastClickRef.current = now; setCount(c => c + 1); }, []);- 用useCallback保存throttle生成的函数,里面实际执行的函数使用ref每次刷新时保存
tsconst [count, setCount] = useState(0); // ref 始终持有最新执行逻辑,这里赋初值 const handleClickRef = useRef(() => { setCount(c => c + 1); }); // 每次render更新click函数 handleClickRef.current = () => { setCount(c => c + 1); }; const handleClick = useCallback( _.throttle( () => handleClickRef.current(), 1000, { trailing: false } ), [] );- 也可以封装成自定义hook
tsexport function useThrottle(callback, wait) { const callbackRef = useRef(callback); callbackRef.current = callback; // 返回稳定的触发入口,供 onClick 直接绑定 return useCallback(_.throttle((...args) => callbackRef.current(...args), wait, { trailing: false }), []); } // 使用 const handleClick = useThrottle(() => { setCount(c => c + 1); }, 1000);
promise执行顺序
- 先介绍一下js的宏任务和微任务
- 宏任务:主代码块,setTimeout,setInterval等
new Promise((resolve,reject) => {...})这个里面是也是同步的宏任务
- 微任务:Promise.then() 、process.nextTick、await后的代码 等
await后面的代码相当于await结束后才推入一个微任务,而Promise.resolve().then是直接推入微任务
- 宏任务:主代码块,setTimeout,setInterval等
graph TD
A[宏任务] --> B[执行结束]
B --> C{有微任务?}
C -->|有| D[执行所有微任务]
C -->|无| E[浏览器渲染]
D --> E
E -->|下一个宏任务| A
style A fill:#d4e6ff,stroke:#333,stroke-width:2px
style B fill:#d4e6ff,stroke:#333,stroke-width:2px
style C fill:#ffcccc,stroke:#333,stroke-width:2px
style D fill:#e6ffcc,stroke:#333,stroke-width:2px
style E fill:#fff7cc,stroke:#333,stroke-width:2px
ts
// 模拟异步请求数据
const fetchUserData = (data) => Promise.resolve(data).then(res => res);
// 主业务流程
async function processUserData() {
let userData = await fetchUserData(111);
console.log(userData ? userData : 0);
}
console.log("开始初始化");
processUserData();
// ⚠️这里先将123推入微任务,上面的await比这个慢,是执行完await才推
Promise.resolve(123).then(res => console.log(res));
console.log("初始化完成");
//开始初始化
//初始化完成
//123
//111
表达式内联 await导致结果错误
- ❌ 不要图省事,在表达式里内联await
ts
let count = 0;
const addFetch = async (addCount) => {
// ❌这两种写法结果都是 200
// 因为下面写法相当于 count = 0 + await Promise.resolve(addCount); count的值一开始就确定了
count += await Promise.resolve(addCount);
}
// 这里是 0 + 100 ,并赋值给count,count为100
addFetch(100);
// 虽然上面count的值是100了,但是这里仍然是 0 + 200,count的值被覆盖为200
addFetch(200);
setTimeout(() => {
console.log(`count = ${count}`)
}, 500);
// ✅ 这种写法结果是 300,获取到await的结果后,再操作
const addFetch = async (addCount) => {
const result = await Promise.resolve(addCount);
count += result;
}
Suspense异步转同步原理
- 第一次看到时,我有个疑问,Suspense 是如何做到等待子组件加载后再显示子组件,之前使用fallback,渲染不是同步的么。
- 经过研究和参考,明白了它其实是通过抛出一个异常,异常是个promise来做到的。
graph TD
A["组件首次渲染"] --> B{"缓存有值?"}
B -- No --> C["发起请求,throw Promise"]
C --> D["Suspense 捕获 Promise"]
D --> E["显示 fallback,并等待promise完成"]
E --> F["Promise 完成"]
F --> G["Suspense 触发重渲染"]
G --> A
B -- Yes --> H["读取缓存值,正常渲染"]
tsx
<Suspense fallback={<ActivityIndicator size="large" />}>
<ChildDetail>
</Suspense>
...
// ChildDetail
const ChildDetail = () =>{
// fetchDetail第一次抛出一个异常,异常的值是Promise
// promise执行结束后,suspense会再次渲染childDetail,
// 此时再调用fetchDetail里有缓存,用缓存的值就同步渲染了
const detail = fetchDetail();
return <Text>
{detail.name}
</Text>
}
// fetchDetail 类似这样
function fetchDetail(): any {
// 缓存命中 → 直接返回值
if (cacheData !== undefined) {
return cacheData;
}
// 缓存未命中 → 发起请求
if (!cacheDataPromise) {
cacheDataPromise = fetch('xxx')
.then((res) => res.json())
.then((data) => {
cacheData = data; // 写入缓存
})
}
// 同步抛出 Promise,让suspense渲染fallback
throw cache[url].promise;
}
iOS踩坑
- textinput上的lineHeight和textAlign偶现的bug
- 如果设置了lineHeight,那么在失去焦点时可能会不显示文字超出的部分(也并没有变成...),获得焦点才显示
- 如果不设置textAlign或者lineHeight,那么概率会出现placeholder被挤到输入框下面显示不全

- 上面两个bug就矛盾了,一个要设置,一个不要设置。
- 解决方案:
textAlign: Platform.select({ ios: value ? undefined : 'auto' })- value是textinput的值,有值的时候不设置textAlign,没值的时候设置成auto
- auto和left的区别仅在于根据语言顺序,自动文字顺序是从左到右还是从右到左
小工具
- 安装
npm install @azsxdc12356/utils
createCanceledPromise --- 可取消的 Promise
- 痛点:原生 Promise 一旦创建无法取消。网络请求、页面跳转等场景需要中断正在进行的异步操作,否则结果回来后会更新已不存在的 UI 或引发报错。
- 给 Promise 加上 cancel 方法,调用后返回的 promise 会 reject 并携带
{ canceled: true },用户可据此区分取消和正常错误。
ts
const cancelable = createCanceledPromise(fetchData(), () => abortController.abort())
try {
const data = await cancelable
} catch (e) {
if (e?.canceled) {
// 用户主动取消
} else {
// 真正的错误
}
}
// 需要取消时
cancelable.cancel()
createSinglePromise - 同时只运行一个的promise
- 痛点:多个模块同时请求同一份配置/资源,不加控制会发出多个重复请求。
- createSinglePromise 保证同一时刻只有一个请求,所有调用者共享同一个 promise。
- 多次调用
get()时,如果上一次还没完成,直接返回正在执行的 promise,不会重复执行。
ts
const singleGetConfig = createSinglePromise(() => fetchConfig())
// 多处同时调用,只会发一次请求
const config1Promise = singleGetConfig.get()
const config2Promise = singleGetConfig.get()
createSharedStateHook --- 轻量跨组件共享状态
- 痛点:跨组件共享状态通常需要引入 Context + Provider 或状态管理库,对于简单场景太重了
createSharedStateHook在模块顶层创建一个 hook,多个组件调用同一个 hook 即可共享状态,无需 Context + Provider 包裹。
tsx
const useUserInfo = createSharedStateHook({ name: '', age: 0 })
function Header() {
const [user, setUser] = useUserInfo()
return <Text>{user.name}</Text>
}
function Editor() {
const [user, setUser] = useUserInfo()
return (
<TextInput
value={user.name}
onChangeText={(text) => setUser({ ...user, name: text })}
/>
)
}
onlyUpdate 选项:只获取 setter 不订阅更新,适合只需要写不需要读的场景(避免不必要的重渲染)。
tsx
const [, setUser] = useUserInfo({ onlyUpdate: true })
异步轮询器
- 原生的setTimeout、setInterval 是定时执行,如果内部执行函数是一个promise,它不会去等promise完成,而是到时间直接执行下一次。如果下一次的结果返回得比上一次快 ,那么最后界面上显示的的就不是最新的结果了
共同特性
三者都继承自 AsyncPolling 基类,具有以下特性:
- 重新
start时旧结果不回调 :运行中途重新调用start,上一次请求的结果会被丢弃 stop后结果不回调 :调用stop后,正在执行的请求结果不会回调setCallback动态更新回调:运行过程中可以替换回调函数
AsyncOnce --- 只取最后一次
- 痛点:搜索框输入、按钮连点等场景下快速触发同一异步操作,前面请求的结果回来后会覆盖最新结果或导致 UI 闪烁。
- 快速连续调用同一异步操作时,只回调最后一次的结果,前几次的过期结果会被丢弃。
ts
const once = new AsyncOnce<string, string>(
async (url) => fetch(url).then(r => r.text()),
(result) => console.log(result)
)
once.start('/api/1') // 还没返回
once.start('/api/2') // 还没返回
once.start('/api/3') // 只有这次的结果会回调
AsyncInterval --- 固定间隔轮询(上一个没完不执行下一个)
- 痛点 :原生
setInterval不关心上次请求是否完成,响应慢时请求会并发堆积。AsyncInterval 保证不并发,且stop()和重新start()时旧结果不会回调。 - 按固定间隔执行异步操作,如果上一次还没返回则跳过本次,到下一个interval再检查,如果返回了才再次执行。
ts
const interval = new AsyncInterval<string, Data>(
3000,
async (param) => fetchData(param),
(result) => console.log(result)
)
interval.start('param') // 立即执行一次,之后每 3s 检查一次,完成了再执行,不完成跳过。
// ...
interval.stop() // 停止轮询,正在执行的请求结果不会回调
AsyncTimeout --- 上次完成后等固定间隔再执行
- 痛点 :原生
setInterval不关心上次请求是否完成,响应慢时请求会并发堆积。 - AsyncTimeout 保证上一次完成后才开始计时,上一次异步操作完成后,等待固定间隔再执行下一次。
ts
const timeout = new AsyncTimeout<string, Data>(
3000,
async (param) => fetchData(param),
(result) => console.log(result)
)
timeout.start('param') // 立即执行一次,完成后等 3s 再执行下一次
// ...
timeout.stop() // 停止轮询
ConcurrencyQueue --- 并发控制队列
- 痛点:批量发起网络请求(上传文件、批量下载)时,不加控制会瞬间创建大量并发连接,导致浏览器卡顿或服务端拒绝。
- ConcurrencyQueue 控制同时执行的任务数量,超出并发上限的任务排队等待,完成一个自动执行下一个。让并发数始终在可控范围内。
ts
const queue = new ConcurrencyQueue('upload', 3)
queue.addItem({
id: 'file1',
start: () => uploadFile('file1'),
})
queue.addItem({
id: 'file2',
start: () => uploadFile('file2'),
})
// 移除排队中的任务
queue.removeItem('file2')
// 清空所有排队任务
queue.removeAll()