vant2x picker源码阅读

2 篇文章 0 订阅
  • picker html结构
    在这里插入图片描述

  • Picker index.js

    // Utils
    import { createNamespace } from '../utils';
    import { preventDefault } from '../utils/dom/event';
    import { BORDER_UNSET_TOP_BOTTOM } from '../utils/constant';
    import { pickerProps, DEFAULT_ITEM_HEIGHT } from './shared';
    import { unitToPx } from '../utils/format/unit';
    
    // Components
    import Loading from '../loading';
    import PickerColumn from './PickerColumn';
    
    const [createComponent, bem, t] = createNamespace('picker');
    
    export default createComponent({
      props: {
        ...pickerProps,
        defaultIndex: {
          type: [Number, String],
          default: 0,
        },
        columns: {
          type: Array,
          default: () => [],
        },
        toolbarPosition: {
          type: String,
          default: 'top',
        },
        valueKey: {
          type: String,
          default: 'text',
        },
      },
    
      data() {
        return {
          children: [],
          formattedColumns: [],
        };
      },
    
      computed: {
        itemPxHeight() {
          return this.itemHeight ? unitToPx(this.itemHeight) : DEFAULT_ITEM_HEIGHT;
        },
    
        dataType() {
          const { columns } = this;
          const firstColumn = columns[0] || {};
    
          if (firstColumn.children) {
            return 'cascade';
          }
    
          if (firstColumn.values) {
            return 'object';
          }
    
          return 'text';
        },
      },
    
      watch: {
        columns: {
          handler: 'format',
          immediate: true,
        },
      },
    
      methods: {
        format() {
          const { columns, dataType } = this;
    
          if (dataType === 'text') {
            this.formattedColumns = [{ values: columns }];
          } else if (dataType === 'cascade') {
            this.formatCascade();
          } else {
            this.formattedColumns = columns;
          }
        },
    
        formatCascade() {
          const formatted = [];
    
          let cursor = { children: this.columns };
    
          while (cursor && cursor.children) {
            const { children } = cursor;
            let defaultIndex = cursor.defaultIndex ?? +this.defaultIndex;
    
            while (children[defaultIndex] && children[defaultIndex].disabled) {
              if (defaultIndex < children.length - 1) {
                defaultIndex++;
              } else {
                defaultIndex = 0;
                break;
              }
            }
    
            formatted.push({
              values: cursor.children,
              className: cursor.className,
              defaultIndex,
            });
    
            cursor = children[defaultIndex];
          }
    
          this.formattedColumns = formatted;
        },
    
        emit(event) {
          if (this.dataType === 'text') {
            this.$emit(event, this.getColumnValue(0), this.getColumnIndex(0));
          } else {
            let values = this.getValues();
    
            // compatible with old version of wrong parameters
            // should be removed in next major version
            // see: https://github.com/vant-ui/vant/issues/5905
            if (this.dataType === 'cascade') {
              values = values.map((item) => item[this.valueKey]);
            }
    
            this.$emit(event, values, this.getIndexes());
          }
        },
    
        onCascadeChange(columnIndex) {
          let cursor = { children: this.columns };
          const indexes = this.getIndexes();
    
          for (let i = 0; i <= columnIndex; i++) {
            cursor = cursor.children[indexes[i]];
          }
    
          while (cursor && cursor.children) {
            columnIndex++;
            this.setColumnValues(columnIndex, cursor.children);
            cursor = cursor.children[cursor.defaultIndex || 0];
          }
        },
    
        onChange(columnIndex) {
          if (this.dataType === 'cascade') {
            this.onCascadeChange(columnIndex);
          }
    
          if (this.dataType === 'text') {
            this.$emit(
              'change',
              this,
              this.getColumnValue(0),
              this.getColumnIndex(0)
            );
          } else {
            let values = this.getValues();
    
            // compatible with old version of wrong parameters
            // should be removed in next major version
            // see: https://github.com/vant-ui/vant/issues/5905
            if (this.dataType === 'cascade') {
              values = values.map((item) => item[this.valueKey]);
            }
    
            this.$emit('change', this, values, columnIndex);
          }
        },
    
        // get column instance by index
        getColumn(index) {
          return this.children[index];
        },
    
        // @exposed-api
        // get column value by index
        getColumnValue(index) {
          const column = this.getColumn(index);
          return column && column.getValue();
        },
    
        // @exposed-api
        // set column value by index
        setColumnValue(index, value) {
          const column = this.getColumn(index);
    
          if (column) {
            column.setValue(value);
    
            if (this.dataType === 'cascade') {
              this.onCascadeChange(index);
            }
          }
        },
    
        // @exposed-api
        // get column option index by column index
        getColumnIndex(columnIndex) {
          return (this.getColumn(columnIndex) || {}).currentIndex;
        },
    
        // @exposed-api
        // set column option index by column index
        setColumnIndex(columnIndex, optionIndex) {
          const column = this.getColumn(columnIndex);
    
          if (column) {
            column.setIndex(optionIndex);
    
            if (this.dataType === 'cascade') {
              this.onCascadeChange(columnIndex);
            }
          }
        },
    
        // @exposed-api
        // get options of column by index
        getColumnValues(index) {
          return (this.children[index] || {}).options;
        },
    
        // @exposed-api
        // set options of column by index
        setColumnValues(index, options) {
          const column = this.children[index];
    
          if (column) {
            column.setOptions(options);
          }
        },
    
        // @exposed-api
        // get values of all columns
        getValues() {
          return this.children.map((child) => child.getValue());
        },
    
        // @exposed-api
        // set values of all columns
        setValues(values) {
          values.forEach((value, index) => {
            this.setColumnValue(index, value);
          });
        },
    
        // @exposed-api
        // get indexes of all columns
        getIndexes() {
          return this.children.map((child) => child.currentIndex);
        },
    
        // @exposed-api
        // set indexes of all columns
        setIndexes(indexes) {
          indexes.forEach((optionIndex, columnIndex) => {
            this.setColumnIndex(columnIndex, optionIndex);
          });
        },
    
        // @exposed-api
        confirm() {
          this.children.forEach((child) => child.stopMomentum());
          this.emit('confirm');
        },
    
        cancel() {
          this.emit('cancel');
        },
    
        genTitle() {
          const titleSlot = this.slots('title');
    
          if (titleSlot) {
            return titleSlot;
          }
    
          if (this.title) {
            return <div class={['van-ellipsis', bem('title')]}>{this.title}</div>;
          }
        },
    
        genCancel() {
          return (
            <button type="button" class={bem('cancel')} onClick={this.cancel}>
              {this.slots('cancel') || this.cancelButtonText || t('cancel')}
            </button>
          );
        },
    
        genConfirm() {
          return (
            <button type="button" class={bem('confirm')} onClick={this.confirm}>
              {this.slots('confirm') || this.confirmButtonText || t('confirm')}
            </button>
          );
        },
    
        genToolbar() {
          if (this.showToolbar) {
            return (
              //class van-picker__toolbar
              <div class={bem('toolbar')}>
                {this.slots() || [
                  this.genCancel(),
                  this.genTitle(),
                  this.genConfirm(),
                ]}
              </div>
            );
          }
        },
    
        genColumns() {
          const { itemPxHeight } = this;
          //高度
          const wrapHeight = itemPxHeight * this.visibleItemCount;
          const frameStyle = { height: `${itemPxHeight}px` };
          const columnsStyle = { height: `${wrapHeight}px` };
          const maskStyle = {
            //宽度、高度
            backgroundSize: `100% ${(wrapHeight - itemPxHeight) / 2}px`,
          };
    
          return (
    
            //阻止事件冒泡
            <div
              class={bem('columns')}
              style={columnsStyle}
              // onTouchmove={preventDefault}
            >
              {this.genColumnItems()}
              {/*渐变背景*/}
              <div class={bem('mask')} style={maskStyle} />
              {/*中间选中背景*/}
              <div
                class={[BORDER_UNSET_TOP_BOTTOM, bem('frame')]}
                style={frameStyle}
              />
             {
               /*
               1,后面两个div中的css样式设置了:pointer-events: none,会导致后面两个div z-index优先级虽然高,但是不能响应所有事件
               2,所有touch、点击事件都会落在genColumnItems中的div上面
                */
             }
            </div>
          );
        },
    
        genColumnItems() {
          //增加column
          return this.formattedColumns.map((item, columnIndex) => (
            <PickerColumn
              readonly={this.readonly}
              valueKey={this.valueKey}
              allowHtml={this.allowHtml}
              className={item.className}
              itemHeight={this.itemPxHeight}
              defaultIndex={item.defaultIndex ?? +this.defaultIndex}
              swipeDuration={this.swipeDuration}
              visibleItemCount={this.visibleItemCount}
              initialOptions={item.values}
              scopedSlots={{
                option: this.$scopedSlots.option,
              }}
              onChange={() => {
                this.onChange(columnIndex);
              }}
            />
          ));
        },
      },
    
      render(h) {
        return (
          //clsss van-picker
          <div class={bem()}>
            {/*顶部工具栏*/}
            {this.toolbarPosition === 'top' ? this.genToolbar() : h()}
            {/*Loading*/}
            {this.loading ? <Loading class={bem('loading')} /> : h()}
            {/**/}
            {this.slots('columns-top')}
            {this.genColumns()}
            {this.slots('columns-bottom')}
            {this.toolbarPosition === 'bottom' ? this.genToolbar() : h()}
          </div>
        );
      },
    });
    
    
  • PickerColumn.js

    import { deepClone } from '../utils/deep-clone';
    import { createNamespace, inBrowser, isObject } from '../utils';
    import { range } from '../utils/format/number';
    import { preventDefault, on, off } from '../utils/dom/event';
    import { TouchMixin } from '../mixins/touch';
    
    const DEFAULT_DURATION = 200;
    
    // 惯性滑动思路:
    // 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `MOMENTUM_LIMIT_TIME` 且 move
    // 距离大于 `MOMENTUM_LIMIT_DISTANCE` 时,执行惯性滑动
    export const MOMENTUM_LIMIT_TIME = 300;
    export const MOMENTUM_LIMIT_DISTANCE = 15;
    
    const [createComponent, bem] = createNamespace('picker-column');
    
    function getElementTranslateY(element) {
      const style = window.getComputedStyle(element);
      const transform = style.transform || style.webkitTransform;
      const translateY = transform.slice(7, transform.length - 1).split(', ')[5];
      return Number(translateY);
    }
    
    function isOptionDisabled(option) {
      return isObject(option) && option.disabled;
    }
    // use standard WheelEvent:
    // https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
    const supportMousewheel = inBrowser && 'onwheel' in window;
    let mousewheelTimer = null;
    
    export default createComponent({
    
      //引入Touch相关代码(会自动跟组件合并)
      mixins: [TouchMixin],//mixins用来抽取组件公共代码
    
      props: {
        valueKey: String,
        readonly: Boolean,
        allowHtml: Boolean,
        className: String,
        itemHeight: Number,//默认44
        defaultIndex: Number,
        swipeDuration: [Number, String],
        visibleItemCount: [Number, String],
        initialOptions: {
          type: Array,
          default: () => [],
        },
      },
    
      data() {
        return {
          offset: 0,
          duration: 0,
          //深拷贝
          options: deepClone(this.initialOptions),
          currentIndex: this.defaultIndex,
        };
      },
    
      created() {
        if (this.$parent.children) {
          this.$parent.children.push(this);
        }
    
        this.setIndex(this.currentIndex);
      },
    
      mounted() {
        //调用TouchMixin中的bindTouchEvent绑定touch事件,其中$el为当前dom
        this.bindTouchEvent(this.$el);
        if (supportMousewheel) {
          on(this.$el, 'wheel', this.onMouseWheel, false);
        }
      },
    
      destroyed() {
        const { children } = this.$parent;
    
        if (children) {
          children.splice(children.indexOf(this), 1);
        }
    
        if (supportMousewheel) {
          off(this.$el, 'wheel');
        }
      },
    
      watch: {
        initialOptions: 'setOptions',
    
        defaultIndex(val) {
          this.setIndex(val);
        },
      },
    
      computed: {
        count() {
          return this.options.length;
        },
    
        baseOffset() {
          //itemHeight = 44
          return (this.itemHeight * (this.visibleItemCount - 1)) / 2;
        },
      },
    
      methods: {
        setOptions(options) {
          if (JSON.stringify(options) !== JSON.stringify(this.options)) {
            this.options = deepClone(options);
            this.setIndex(this.defaultIndex);
          }
        },
    
        onTouchStart(event) {
          if (this.readonly) {
            return;
          }
    
          //x y坐标
          this.touchStart(event);
    
          if (this.moving) {
            //解析当前div translateY
            const translateY = getElementTranslateY(this.$refs.wrapper);
            this.offset = Math.min(0, translateY - this.baseOffset);
            this.startOffset = this.offset;
          } else {
            this.startOffset = this.offset;
          }
    
          this.duration = 0;
          this.transitionEndTrigger = null;
          this.touchStartTime = Date.now();
          this.momentumOffset = this.startOffset;
        },
    
        onTouchMove(event) {
          if (this.readonly) {
            return;
          }
    
          //计算offsetX offsetY
          this.touchMove(event);
    
          if (this.direction === 'vertical') {
            this.moving = true;
            preventDefault(event, true);
          }
    
          //计算偏移量,跟css translate3d y绑定
          this.offset = range(
            this.startOffset + this.deltaY,
            -(this.count * this.itemHeight),
            this.itemHeight
          );
    
    
          const now = Date.now();
          if (now - this.touchStartTime > MOMENTUM_LIMIT_TIME) {
            this.touchStartTime = now;
            this.momentumOffset = this.offset;
          }
        },
    
        onTouchEnd() {
          if (this.readonly) {
            return;
          }
    
          const distance = this.offset - this.momentumOffset;
          const duration = Date.now() - this.touchStartTime;
          const allowMomentum =
            duration < MOMENTUM_LIMIT_TIME &&
            Math.abs(distance) > MOMENTUM_LIMIT_DISTANCE;
    
          if (allowMomentum) {
            this.momentum(distance, duration);
            return;
          }
    
          //根据offset获取索引
          const index = this.getIndexByOffset(this.offset);
          this.duration = DEFAULT_DURATION;
          //根据index设置offset
          this.setIndex(index, true);
    
          // compatible with desktop scenario
          // use setTimeout to skip the click event Emitted after touchstart
          setTimeout(() => {
            this.moving = false;
          }, 0);
        },
    
        onMouseWheel(event) {
          if (this.readonly) {
            return;
          }
          preventDefault(event, true);
          // simply combine touchstart and touchmove
          const translateY = getElementTranslateY(this.$refs.wrapper);
          this.startOffset = Math.min(0, translateY - this.baseOffset);
          this.momentumOffset = this.startOffset;
          this.transitionEndTrigger = null;
    
          // directly use deltaY, see https://caniuse.com/?search=deltaY
          // use deltaY to detect direction for not special setting device
          // https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event
          const { deltaY } = event;
          if (this.startOffset === 0 && deltaY < 0) {
            return;
          }
    
          // get offset
          // if necessary, can adjust distance value to make scrolling smoother
          const distance = -deltaY;
          this.offset = range(
            this.startOffset + distance,
            -(this.count * this.itemHeight),
            this.itemHeight
          );
    
          if (mousewheelTimer) {
            clearTimeout(mousewheelTimer);
          }
    
          mousewheelTimer = setTimeout(() => {
            this.onTouchEnd();
            this.touchStartTime = 0;
          }, MOMENTUM_LIMIT_TIME);
        },
    
        //动画完成
        onTransitionEnd() {
          this.stopMomentum();
        },
    
        onClickItem(index) {
          if (this.moving || this.readonly) {
            return;
          }
    
          this.transitionEndTrigger = null;
          this.duration = DEFAULT_DURATION;
          this.setIndex(index, true);
        },
    
        adjustIndex(index) {
          index = range(index, 0, this.count);
    
          for (let i = index; i < this.count; i++) {
            if (!isOptionDisabled(this.options[i])) return i;
          }
    
          for (let i = index - 1; i >= 0; i--) {
            if (!isOptionDisabled(this.options[i])) return i;
          }
        },
    
        getOptionText(option) {
          if (isObject(option) && this.valueKey in option) {
            return option[this.valueKey];
          }
          return option;
        },
    
        setIndex(index, emitChange) {
          index = this.adjustIndex(index) || 0;
    
          const offset = -index * this.itemHeight;
    
          const trigger = () => {
            if (index !== this.currentIndex) {
              this.currentIndex = index;
    
              if (emitChange) {
                //通知父组件
                this.$emit('change', index);
              }
            }
          };
    
          // trigger the change event after transitionend when moving
          if (this.moving && offset !== this.offset) {
            this.transitionEndTrigger = trigger;
          } else {
            trigger();
          }
    
          this.offset = offset;
        },
    
        setValue(value) {
          const { options } = this;
          for (let i = 0; i < options.length; i++) {
            if (this.getOptionText(options[i]) === value) {
              return this.setIndex(i);
            }
          }
        },
    
        getValue() {
          return this.options[this.currentIndex];
        },
    
        getIndexByOffset(offset) {
          //获取index(取中间值)
          return range(Math.round(-offset / this.itemHeight), 0, this.count - 1);
        },
    
        momentum(distance, duration) {
          const speed = Math.abs(distance / duration);
    
          distance = this.offset + (speed / 0.003) * (distance < 0 ? -1 : 1);
    
          const index = this.getIndexByOffset(distance);
    
          this.duration = +this.swipeDuration;
          this.setIndex(index, true);
        },
    
        stopMomentum() {
          this.moving = false;
          this.duration = 0;
    
          if (this.transitionEndTrigger) {
            this.transitionEndTrigger();
            this.transitionEndTrigger = null;
          }
        },
    
        genOptions() {
          const optionStyle = {
            height: `${this.itemHeight}px`,
          };
    
          return this.options.map((option, index) => {
            const text = this.getOptionText(option);
            const disabled = isOptionDisabled(option);
    
            const data = {
              style: optionStyle,
              attrs: {
                role: 'button',
                tabindex: disabled ? -1 : 0,
              },
              class: [
                bem('item', {
                  disabled,
                  selected: index === this.currentIndex,
                }),
              ],
              on: {
                click: () => {
                  this.onClickItem(index);
                },
              },
            };
    
            const childData = {
              //dom class属性
              class: 'van-ellipsis',
              //dom内容
              domProps: {
                [this.allowHtml ? 'innerHTML' : 'textContent']: text,
              },
            };
    
            return (
              <li {...data}>
                {this.slots('option', option) || <div {...childData} />}
              </li>
            );
          });
        },
      },
    
      render() {
        const wrapperStyle = {
          //y轴移动
          transform: `translate3d(0, ${this.offset + this.baseOffset}px, 0)`,
          //动画时间
          transitionDuration: `${this.duration}ms`,
          //过渡效果
          transitionProperty: this.duration ? 'all' : 'none',
        };
    
        return (
          //touch事件在mounted方法中绑定在当前div中
          <div class={[bem(), this.className]}>
            <ul
              ref="wrapper"
              style={wrapperStyle}
              class={bem('wrapper')}
              onTransitionend={this.onTransitionEnd}
            >
              {this.genOptions()}
            </ul>
          </div>
        );
      },
    });
    
    
  • 事件处理主要逻辑

    
    
    事件处理:
       1、mounted方法中调用TouchMixin中的this.bindTouchEvent(this.$el)方法绑定touch事件
       2、bindTouchEvent绑定 touchstart、touchmove方法
       3、touch事件处理:
          onTouchStart:
            this.startX = event.touches[0].clientX;
            this.startY = event.touches[0].clientY;
    
            const translateY = getElementTranslateY(this.$refs.wrapper);
            this.offset = Math.min(0, translateY - this.baseOffset);
    
          onTouchMove:
            const touch = event.touches[0];
            // safari back will set clientX to negative number
            this.deltaX = touch.clientX < 0 ? 0 : touch.clientX - this.startX;
            this.deltaY = touch.clientY - this.startY;
            this.offsetX = Math.abs(this.deltaX);
            this.offsetY = Math.abs(this.deltaY);
    
            this.offset = this.startOffset + this.deltaY //
    
          onTouchEnd:
            const index = this.getIndexByOffset(this.offset);
    
        注意:touch事件的处理,利用了css的pointer-events: none属性,导致z-index虽然越高,但是不能响应事件,事件会交由低z-index的div容器处理
    
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值