React Native Gesture Handler回调不一致报错及worklet添加后崩溃问题
解决React Native Gesture Handler回调不一致导致的错误及崩溃问题
问题说明
运行React Native Gesture Handler相关代码时,触发以下错误:
ERROR [react-native-gesture-handler] Some of the callbacks in the gesture are worklets and some are not. Either make sure that all calbacks are marked as 'worklet' if you wish to run them on the UI thread or use '.runOnJS(true)' modifier on the gesture explicitly to run all callbacks on the JS thread
尝试给handleGestureEvent添加'worklet'标记后,点击应用直接崩溃。
原代码如下:
import * as React from 'react'; import { View, Text, Dimensions } from 'react-native'; import styles from '../styles/style_home' import * as Animatable from 'react-native-animatable' import {Canvas, Path, Skia} from '@shopify/react-native-skia'; import {curveBasis, line, scaleLinear, scalePoint} from 'd3'; import DataPalmas from '../../assets/csvjson.json' import Gradient from './Gradient' import XAxisText from './XAxisText'; import { clamp, runOnJS, useSharedValue, withDelay, withTiming } from 'react-native-reanimated'; import Cursor from './Cursor' import { Gesture, GestureDetector, PanGestureHandlerEventPayload, } from 'react-native-gesture-handler'; import {getYForX, parse} from 'react-native-redash'; export default function () { const [showCursor, setShowCursor] = React.useState(false) const animationLine = useSharedValue(0) const animationGradient = useSharedValue({x: 0, y: 0}) const cx = useSharedValue(0) const cy = useSharedValue(0) React.useEffect(() => { animationLine.value = withTiming(1, {duration:2000}) animationGradient.value = withDelay(2000, withTiming({x: 0, y: chartHeight}, {duration: 1000})) }, []) const chartHeight = 150 const chartWidth = Dimensions.get('window').width const chartMargin = 20 const data = DataPalmas const xDomain = data.map((dataPoint) => dataPoint.Data) const xRange = [chartMargin, chartWidth - chartMargin] const x = scalePoint().domain(xDomain).range(xRange).padding(0) const stepX = x.step() const max = Math.max(...data.map(val => val.Precipitacao)) const min = Math.min(...data.map(val => val.Precipitacao)) const yDomain = [min, max] const yRange = [chartHeight, 0] const y = scaleLinear().domain(yDomain).range(yRange) const curvedLine = line() .x(d => x(d.Data)) .y(d => y(d.Precipitacao)) .curve(curveBasis)(data) const linePath = Skia.Path.MakeFromSVGString(curvedLine) const path = parse(linePath.toSVGString()) function handleGestureEvent(e){ const clampValue = clamp( Math.floor(e.absoluteX / stepX) * stepX + chartMargin, chartMargin, chartWidth - chartMargin, ) cx.value = clampValue cy.value = getYForX(path, Math.floor(clampValue)) } const pan = Gesture.Pan() .onTouchesDown(() => { setShowCursor(true) }) .onTouchesUp(() => { setShowCursor(false) }) .onBegin(handleGestureEvent) .onChange(handleGestureEvent) return ( <Animatable.View style={styles.container__graphic} animation="fadeInLeft"> <GestureDetector gesture={pan}> <Canvas style={{ width: chartWidth, height: chartHeight+30, }}> <Path path={linePath} style={'stroke'} strokeWidth={2} color={'#0bb861'} strokeCap={'round'} start={0} end={animationLine}/> <Gradient chartHeight = {chartHeight} chartMargin = {chartMargin} chartWidth = {chartWidth} curvedLine = {curvedLine} animationGradient = {animationGradient} /> {data.map((dataPoint, index) => ( <XAxisText x={x(dataPoint.Data)} y={chartHeight} text={dataPoint.Data} key={index} /> ))} {showCursor && <Cursor cx = {cx} cy = {cy} chartHeight = {chartHeight} />} </Canvas> </GestureDetector> </Animatable.View> ); }
添加worklet标记后的崩溃代码片段:
function handleGestureEvent(e){ 'worklet' const clampValue = clamp( Math.floor(e.absoluteX / stepX) * stepX + chartMargin, chartMargin, chartWidth - chartMargin, ) cx.value = clampValue cy.value = getYForX(path, Math.floor(clampValue)) }
解决方案
方案一:统一使用Worklet(UI线程执行)
崩溃原因是:setShowCursor是React的状态更新函数,只能在JS线程调用,而Worklet运行在UI线程,直接调用会导致崩溃。需要用runOnJS包装状态更新函数,让它回到JS线程执行。
修改步骤:
- 用
runOnJS包装setShowCursor,生成可在UI线程调用的函数 - 给所有手势回调添加
'worklet'标记 - 在Worklet中调用包装后的状态更新函数
修改后的代码:
// 包装状态更新函数 const setShowCursorOnJS = runOnJS(setShowCursor); // 修改handleGestureEvent为Worklet function handleGestureEvent(e){ 'worklet' const clampValue = clamp( Math.floor(e.absoluteX / stepX) * stepX + chartMargin, chartMargin, chartWidth - chartMargin, ) cx.value = clampValue cy.value = getYForX(path, Math.floor(clampValue)) } // 修改手势回调,全部标记为Worklet const pan = Gesture.Pan() .onTouchesDown(() => { 'worklet'; setShowCursorOnJS(true); }) .onTouchesUp(() => { 'worklet'; setShowCursorOnJS(false); }) .onBegin(handleGestureEvent) .onChange(handleGestureEvent);
方案二:强制所有回调在JS线程执行(更简单)
直接给手势添加.runOnJS(true)修饰符,这样所有回调都在JS线程运行,不需要标记Worklet,也不用处理跨线程调用问题,适合对性能要求不高的场景。
修改后的代码:
function handleGestureEvent(e){ const clampValue = clamp( Math.floor(e.absoluteX / stepX) * stepX + chartMargin, chartMargin, chartWidth - chartMargin, ) cx.value = clampValue cy.value = getYForX(path, Math.floor(clampValue)) } // 添加.runOnJS(true) const pan = Gesture.Pan() .onTouchesDown(() => { setShowCursor(true) }) .onTouchesUp(() => { setShowCursor(false) }) .onBegin(handleGestureEvent) .onChange(handleGestureEvent) .runOnJS(true);
内容的提问来源于stack exchange,提问作者Heitor Fiuza Oliveira
相关产品推荐
相关产品推荐

