一、描述

之前写了 rax-slider 在 weex 环境的源码分析,原文地址:rax内置组件 rax-slider 在 weex 环境的源码分析

在 weex 环境中还是比较简单的,因为主要是基于 weex 的 <slider> 组件实现的。

在 web 环境下,是 rax 自己实现的一套轮播组件,而因为高可配置型,导致 web 环境下的源码非常多,将对主要的源码进行分析。

web 环境的 slider 组件由5个文件组成:

  • slider.web.js:
  • isValidSwipe.js:
  • style.js:
  • SwipeEvent.js:

rax-slider 在 web 的核心实现原理仍旧是通过 transform offsetX 的形式实现。

二、slider.web.js

1、组件维护的变量

组件除了 props 外,还维护几个变量:

this.index = 0;    // 当前
this.height = null; // 高度
this.width = null;    // 宽度
this.loopIdx = 0;     // 轮播id
this.offsetX = null;    // 偏移量
this.isSwiping = false;    // 是否轮播中
this.total = 0;      // 总数量

2、componentWillMount

componentWillMount 生命周期中,通过 this.props.children 的长度计算总宽度 this.width,宽度计算是通过 document.documentElement.clientWidth 和 750 的比例计算得到的。

注意有一个 parseFloat 是应当避免的,组件传入的时候,不应该处理这么多东西

componentWillMount() {
  const {children, height, width} = this.props;
  if (children.length < 2) return;
  this.index = 0;
  this.height = height;
  // TODO: Avoid convert unit in component
  this.width = parseFloat(width) * document.documentElement.clientWidth / 750;
  this.loopIdx = 0;
}

3、componentDidMount

组件挂载完成后,会进行 autoPlay 的判断,如果 autoPlay 是 true,并且子组件数量大于 1 的时候,才会进行自动播放。

  componentDidMount() {
    if (this.props.autoPlay && this.total > 1) {
      this.autoPlay();
    }
  }

4、autoPlay 自动播放

如果自动播放,会调用这个方法,进行一次轮播的前提是 this.isSwiping 不能为 true。

同时如果已经存在了 autoPlayTimer ,则需要先置空之前的 autoPlayTimer,并且如果轮播结束(isLoopEnd() 为 true),则不进行轮播,如果都通过了,则会调用 slideTo() 跳转到下一内容。

autoPlay() {
    const autoplayInterval = this.props.autoplayInterval;
    if (this.isSwiping) return;
    this.autoPlayTimer && clearInterval(this.autoPlayTimer);
    this.autoPlayTimer = setInterval(() => {
      if (this.isLoopEnd()) return;
      this.slideTo(this.index, SWIPE_LEFT);
    }, parseFloat(autoplayInterval));
  }

5、slideTo 滚动到某个轮播内容

sliderTo() 算是比较核心的方法了,逐行分析,如果正在滚动(isSwiping = true),则不进行任何行为。

该方法会接收两个参数,分别是 indexdirection,其中 index 并不是要滚动到的索引位置,而是有 direction 决定的,如果 direction 有值,则 index 表示当前的所以,滚动到的索引需要 index + 1index - 1 的计算结果,当然如果没有 direction,则直接使用 this.index。

通过 index,计算偏移量 this.offsetX

最开始就说了,在 web 上实现轮播,还是通过 transform + this.offsetX 实现偏移,因此还是需要通过 findDomNode 获取 DOM 节点,然后控制 style,通过 css 使用 transform。

除了将整个 sliderview 进行样式变更外,还需要子节点,并且设置子节点的样式。

  slideTo(index, direction) {
    if (this.isSwiping) return;

    if (direction) {
      this.index = direction === SWIPE_LEFT ? index + 1 : index - 1;
    } else {
      this.index = index;
    }
    this.offsetX = this.index * this.width;

    const realIndex = this.loopedIndex();

    // translate3d for performance optimization
    let swipeView = findDOMNode(this.refs.swipeView);
    const styleText = `translate3d(${-1 * this.offsetX}px, 0px, 0px)`;
    swipeView.style.transform = styleText;
    swipeView.style.webkitTransform = styleText;

    this.loopIdx = this.index < 0 && realIndex !== 0 ? this.total - realIndex : realIndex;
    let childNum = 'child' + this.loopIdx;
    let childView = findDOMNode(this.refs[childNum]);
    childView.style.left = this.offsetX + 'px';

    this.props.onChange({index: this.loopIdx});
    this.setState({
      offsetX: this.offsetX
    });
  }

6、判断循环是否结束

如果 props.loop 为 true,则不会识别为循环结束,只有非 loop = true 并且 realIndex 为 total(轮播到最后一个),则声明结束。

  isLoopEnd() {
    const realIndex = this.loopedIndex();
    const num = this.total;
    if (!this.props.loop && (realIndex === num - 1 || realIndex === 0) ) {
      return true;
    }
    return false;
  }

7、 渲染导航内容

渲染底部的导航内容,实际上没什么特别复杂的,比较关键的他是通过 i === realIndex 来判断 activeDot 的。

发现 this.props.activeDotthis.props.normalDot 没有放在文档中,应该是后面会支持吧,目前使用的仍旧是默认的 <View>

for (let i = 0; i < this.total; i++) {
  dots.push(i === realIndex
    ? cloneElement(ActiveDot, {key: i})
    : cloneElement(NormalDot, {key: i}));
}
  renderPagination() {
    let props = this.props;
    if (this.total <= 1) return;

    Object.assign(styles.defaultPaginationStyle, props.paginationStyle);
    let {itemSelectedColor, itemColor, itemSize} = styles.defaultPaginationStyle;

    const activeStyle = [
      styles.activeDot,
      {
        backgroundColor: itemSelectedColor,
        width: itemSize,
        height: itemSize
      }
    ];

    const normalStyle = [
      styles.normalDot,
      {
        backgroundColor: itemColor,
        width: itemSize,
        height: itemSize
      }
    ];

    let dots = [];
    const ActiveDot = this.props.activeDot || <View style={activeStyle} />;
    const NormalDot = this.props.normalDot || <View style={normalStyle} />;
    const realIndex = this.loopIdx;

    for (let i = 0; i < this.total; i++) {
      dots.push(i === realIndex
        ? cloneElement(ActiveDot, {key: i})
        : cloneElement(NormalDot, {key: i}));
    }

    return (
      <View style={[
        styles.defaultPaginationStyle,
        props.paginationStyle
      ]}>
        {dots}
      </View>
    );
  }

8、getPages() 生成页面列表

这个方法主要是遍历 this.props.children,将其写入数组中,需要注意的是,在 map 的时候,每个子组件挂上了 'child'+ index 的 ref,这也是 sliderTo 方法中使用的 ref。

getPages = () => {
  const children = this.props.children;
  if (!children.length || children.length <= 1) {
    return <View style={styles.childrenStyle}>{children}</View>;
  }

  return children.map((child, index) => {
    let refStr = 'child' + index;
    let translateStyle = {
      width: this.width + 'px',
      height: this.height,
      left: index * this.width + 'px'
    };
    return (
      <View ref={refStr} className={'childWrap' + index}
        style={[styles.childrenStyle, translateStyle]} key={index}>
        {child}
      </View>
    );
  });
}

10、渲染slider

这个方法是应用到了 render 方法中,主要是对整个 Swiper 方法的应用,其中 SwipeEvent 是对手势进行了封装,这里就不详细展开了。

renderSwipeView(pages) {
    const {
      initialVelocityThreshold,
      verticalThreshold,
      vertical,
      horizontalThreshold,
      children
    } = this.props;
    const style = {
      width: this.width + 'px',
      height: this.height
    };

    return children.length && children.length > 1 ?
      <SwipeEvent style={[styles.swipeWrapper, style]}
        onSwipeBegin={this.onSwipeBegin}
        onSwipeEnd={this.onSwipeEnd}
        onSwipe={this.onSwipe}
        initialVelocityThreshold={initialVelocityThreshold}
        verticalThreshold={verticalThreshold}
        vertical={vertical}
        horizontalThreshold={horizontalThreshold}>
        <View ref="swipeView" style={[styles.swipeStyle, style]}>
          {pages}
        </View>
      </SwipeEvent>
      :
      <View ref="swipeView" style={[styles.swipeStyle, style]}>
        {pages}
      </View>
    ;
  }

11、render 方法

render 方法就简单很多了,因为核心的两个方法是在 this.renderSwiperViewthis.getPages() 中。

 render() {
    const that = this;
    const {style, showsPagination, children} = this.props;
    this.total = children.length;
    return (
      <View style={[styles.slideWrapper, style]}>
        {this.renderSwipeView(this.getPages())}
        {showsPagination ? this.renderPagination() : ''}
      </View>
    );
  }

三、效果

GIF.gif