ReactNative总结系列二 --- 小工具&小技巧md

小技巧

条件渲染组件陷阱

  • 如果按这样来判断是否要渲染View progress && <View/> ,在progress是数字0时会报错

Text strings must be rendered within a <Text> component

  • 这是因为当 progress0 时,表达式 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);
  • 解决方案:

    1. 不用throttle,用一个变量根据时间判断
    ts 复制代码
    const lastClickRef = useRef(0);
    const handleClick = useCallback(() => {
      const now = Date.now();
      // 1 秒内直接忽略
      if (now - lastClickRef.current < 1000) {
        return; 
      }
      lastClickRef.current = now;
      setCount(c => c + 1);
    }, []);
    1. 用useCallback保存throttle生成的函数,里面实际执行的函数使用ref每次刷新时保存
    ts 复制代码
    const [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
    ts 复制代码
    export 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是直接推入微任务
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()
相关推荐
humcomm10 小时前
FinClip vs React Native:两大跨平台方案的深度对比
javascript·react native·react.js
水云桐程序员11 小时前
Flutter与React Native的对比分析
flutter·react native·react.js
墨狂之逸才2 天前
React Native 状态管理大比拼:Event Bus 还是 Context?小白一看就懂!
react native
爱滑雪的码农2 天前
React Native 完整开发全流程(从零到上线)
javascript·react native·react.js
沐言人生2 天前
ReactNative 源码分析12——Native View创建流程onBatchComplete
android·react native
沐言人生4 天前
ReactNative 源码分析11——Native View创建流程setChildren和manageChildren
android·react native
沐言人生5 天前
ReactNative 源码分析10——Native View创建流程createView
android·react native
坏小虎5 天前
【聊天列表组件选型建议】FlashList、FlatList、LegendList三种列表组件
javascript·react native·react.js
sealaugh326 天前
react native(学习笔记第五课) 英语打卡微应用(4)- frontend的列表展示
笔记·学习·react native