记录react-mobile-picker使用过程中遇到的bug(ios,部分安卓触发touchcancel时发生)--系统源码也有风险

问题描述一:

在ios,部分安卓,在手指拖动选择的时候,程序切换(其他情况也行,主要能触发TouchCancel),导致触发TouchCancel,源码里面在重置滚动位置的时候,直接取用一个对象,并且我在抛出undefined的同时也设置了value和options,导致计算的时候出现NaN;

解决:需要解构出startScrollerTranslate,在picker抛出undefined时不要设置value值

场景:省市选择器,选择省份的时候同步市的选项(需要不断赋值,之前写的代码也有问题)

分析源码:

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> ); }

  

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

// 覆盖picker的渐变色
:global {
  .picker-container .picker-inner {
    -webkit-mask-box-image: none;
  }
}

  

 

 

 

 

 

 

https://github.com/adcentury/react-mobile-picker/issues/56

 

posted @ 2023-03-20 11:10  木杉呀  阅读(113)  评论(0编辑  收藏  举报