记录react-mobile-picker使用过程中遇到的bug(ios,部分安卓触发touchcancel时发生)--系统源码也有风险
问题描述一:
在ios,部分安卓,在手指拖动选择的时候,程序切换(其他情况也行,主要能触发TouchCancel),导致触发TouchCancel,源码里面在重置滚动位置的时候,直接取用一个对象,并且我在抛出undefined的同时也设置了value和options,导致计算的时候出现NaN;
解决:需要解构出startScrollerTranslate,在picker抛出undefined时不要设置value值
场景:省市选择器,选择省份的时候同步市的选项(需要不断赋值,之前写的代码也有问题)
分析源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | import React, { useState, useEffect } from "react" ; // import RcPicker from "react-mobile-picker"; import RcPicker from "../RcMobilePicker" ; import styles from "./index.module.scss" ; import "./reset.scss" ; import scrollLock from "../../utils/scroll-lock" ; interface IProps { title?: string; tipMsg?: string; cancelTxt?: string; confirmTxt?: string; onCancel: () => void; onConfirm: (result: object) => void; optionGroups: object; valueGroups: object; onChange?: (name, value) => void; } export default function Picker(props: IProps) { useEffect(() => { scrollLock.enable(); return () => { scrollLock.disable(); }; }, []); const handleChange = (name, value) => { console.log(name, value, "handleChangehandleChange" ); 原先并没有判断value为空的情况,直接触发change,导致value设置部分值为undefined,也为后面的报错留下伏笔(堆栈溢出) if (!!value) { props.onChange && props.onChange(name, value); } }; return ( <div className={styles.pickerModalWrapper}> <div className={styles.pickerModalMask} onClick={() => props.onCancel()} ></div> <div className={`${styles.pickerModal} ${styles.pickerModalToggle}`}> <header> <div className={styles.cancelBtn} onClick={() => props.onCancel()}> {props.cancelTxt || "取消" } </div> <div className={styles.title}>{props.title || "请选择" }</div> <div className={styles.confirmBtn} onClick={props.onConfirm}> {props.confirmTxt || "确定" } </div> </header> {props?.tipMsg ? ( <div className={styles.tipsContainer}> <p className={styles.txt}>{props?.tipMsg}</p> </div> ) : null } <RcPicker optionGroups={props.optionGroups} valueGroups={props.valueGroups} onChange={handleChange} /> </div> </div> ); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | import React, {Component} from 'react' ; import PropTypes from 'prop-types' ; import './style.scss' ; class PickerColumn extends Component { static propTypes = { options: PropTypes.array.isRequired, name: PropTypes.string.isRequired, value: PropTypes.any.isRequired, itemHeight: PropTypes.number.isRequired, columnHeight: PropTypes.number.isRequired, onChange: PropTypes.func.isRequired }; constructor(props) { super (props); this .state = { isMoving: false , startTouchY: 0, startScrollerTranslate: 0, ... this .computeTranslate(props) }; } componentWillReceiveProps(nextProps) { if ( this .state.isMoving) { return ; } this .setState( this .computeTranslate(nextProps)); } computeTranslate = (props) => { const {options, value, itemHeight, columnHeight} = props; let selectedIndex = options.indexOf(value); if (selectedIndex < 0) { // throw new ReferenceError(); console.warn( 'Warning: "' + this .props.name+ '" doesn\'t contain an option of "' + value + '".' ); this .onValueSelected(options[0]); selectedIndex = 0; } return { scrollerTranslate: columnHeight / 2 - itemHeight / 2 - selectedIndex * itemHeight, minTranslate: columnHeight / 2 - itemHeight * options.length + itemHeight / 2, maxTranslate: columnHeight / 2 - itemHeight / 2 }; }; onValueSelected = (newValue) => { this .props.onChange( this .props.name, newValue); }; handleTouchStart = (event) => { const startTouchY = event.targetTouches[0].pageY; this .setState(({scrollerTranslate}) => ({ startTouchY, startScrollerTranslate: scrollerTranslate })); }; handleTouchMove = (event) => { event.preventDefault(); const touchY = event.targetTouches[0].pageY; this .setState(({isMoving, startTouchY, startScrollerTranslate, minTranslate, maxTranslate}) => { if (!isMoving) { return { isMoving: true } } let nextScrollerTranslate = startScrollerTranslate + touchY - startTouchY; if (nextScrollerTranslate < minTranslate) { nextScrollerTranslate = minTranslate - Math.pow(minTranslate - nextScrollerTranslate, 0.8); } else if (nextScrollerTranslate > maxTranslate) { nextScrollerTranslate = maxTranslate + Math.pow(nextScrollerTranslate - maxTranslate, 0.8); } return { scrollerTranslate: nextScrollerTranslate }; }); }; handleTouchEnd = (event) => { if (! this .state.isMoving) { return ; } this .setState({ isMoving: false , startTouchY: 0, startScrollerTranslate: 0 }); setTimeout(() => { const {options, itemHeight} = this .props; const {scrollerTranslate, minTranslate, maxTranslate} = this .state; let activeIndex; if (scrollerTranslate > maxTranslate) { activeIndex = 0; } else if (scrollerTranslate < minTranslate) { activeIndex = options.length - 1; } else { activeIndex = - Math.floor((scrollerTranslate - maxTranslate) / itemHeight); } this .onValueSelected(options[activeIndex]); }, 0); }; handleTouchCancel = (event) => { if (! this .state.isMoving) { return ; } //原来写法,通过分析reactdom可以看到,这个值没有解构,但是在start的时候又有解构,暂不知道是否有具体用意?源码地址:https://github.com/adcentury/react-mobile-picker/blob/master/src/index.js//this.setState(({startScrollerTranslate}) => ({ this.setState(({startScrollerTranslate}) => ({ isMoving: false , startTouchY: 0, startScrollerTranslate: 0, scrollerTranslate: startScrollerTranslate })); }; handleItemClick = (option) => { if (option !== this .props.value) { this .onValueSelected(option); } }; renderItems() { const {options, itemHeight, value} = this .props; return options.map((option, index) => { const style = { height: itemHeight + 'px' , lineHeight: itemHeight + 'px' }; const className = `picker-item${option === value ? ' picker-item-selected' : '' }`; return ( <div key={index} className={className} style={style} onClick={() => this .handleItemClick(option)}>{option}</div> ); }); } render() { const translateString = `translate3d(0, ${ this .state.scrollerTranslate}px, 0)`; const style = { MsTransform: translateString, MozTransform: translateString, OTransform: translateString, WebkitTransform: translateString, transform: translateString }; if ( this .state.isMoving) { style.transitionDuration = '0ms' ; } return ( <div className= "picker-column" > <div className= "picker-scroller" style={style} onTouchStart={ this .handleTouchStart} onTouchMove={ this .handleTouchMove} onTouchEnd={ this .handleTouchEnd} onTouchCancel={ this .handleTouchCancel}> { this .renderItems()} </div> </div> ) } } export default class Picker extends Component { static propTyps = { optionGroups: PropTypes.object.isRequired, valueGroups: PropTypes.object.isRequired, onChange: PropTypes.func.isRequired, itemHeight: PropTypes.number, height: PropTypes.number }; static defaultProps = { itemHeight: 36, height: 216 }; renderInner() { const {optionGroups, valueGroups, itemHeight, height, onChange} = this .props; const highlightStyle = { height: itemHeight, marginTop: -(itemHeight / 2) }; const columnNodes = []; for ( let name in optionGroups) { columnNodes.push( <PickerColumn key={name} name={name} options={optionGroups[name]} value={valueGroups[name]} itemHeight={itemHeight} columnHeight={height} onChange={onChange} /> ); } return ( <div className= "picker-inner" > {columnNodes} <div className= "picker-highlight" style={highlightStyle}></div> </div> ); } render() { const style = { height: this .props.height }; return ( <div className= "picker-container" style={style}> { this .renderInner()} </div> ); } } |
原因:在滚动到临界状态(也就是省份的中间),官方给的demo用例也是可以试出来,就是滚动到中间,切换程序,再回来的时候会发现比没有滚动在其中一个省市上,而是停留在中间,再重新滚动是,会先再触发抛出一个undefined,随后恢复正常),切换程序,触发cancel,抛出undefined,外部程序接收undefined的时候,set入value,重新遍历出options,picker组件接收到错误值;回到picker组件中重新滚动(handleTouchStart),结构出来的scrollerTranslate还是undefined/NaN,往下计算后又抛出undefined,导致死循环。
这里死循环是组件代码和自身代码都有问题导致的。
解决:抛出undefined不处理,picker在touchcancel的时候结构出startScrollerTranslate,这种原先的值,恢复到上次滚动的距离。
问题描述二(样式问题):没有做好兼容性,导致文字被遮挡,不影响功能,但丑
机型:苹果14 IOS16.4 升级了iosbeta版本好像都有问题safari
1 2 3 4 5 6 | // 覆盖picker的渐变色 :global { .picker-container .picker-inner { -webkit-mask-box-image: none ; } } |
https://github.com/adcentury/react-mobile-picker/issues/56
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?