记录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

 

posted @   木杉呀  阅读(150)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示