造个轮子,实现一个 Carousel 轮播图组件

503次阅读  |  发布于3年以前

在我们用 JSX 建立组件系统之前,我们先来用一个例子学习一下组件的实现原理和逻辑。这里我们就用一个轮播图的组件作为例子进行学习。轮播图的英文叫做 Carousel,它有一个旋转木马的意思。

要做这个轮播图的组件,我们应该先从一个最简单的 DOM 操作入手。使用 DOM 操作把整个轮播图的功能先实现出来,然后在一步一步去考虑怎么把它设计成一个组件系统。

!! TIPS:在开发中我们往往一开始做一个组件的时候,都会过度思考一个功能应该怎么设计,然后就把它实现的非常复杂。其实更好的方式是反过来的,先把功能实现了,然后通过分析这个功能从而设计出一个组件架构体系。

因为是轮播图,那我们当然需要用到图片,所以这里我准备了 4 张来源于 Unsplash 的开源图片,当然大家也可以换成自己的图片。首先我们把这 4 张图片都放入一个 gallery 的变量当中:

let gallery = [
  'https://source.unsplash.com/Y8lCoTRgHPE/1142x640',
  'https://source.unsplash.com/v7daTKlZzaw/1142x640',
  'https://source.unsplash.com/DlkF4-dbCOU/1142x640',
  'https://source.unsplash.com/8SQ6xjkxkCo/1142x640',
];

而我们的目标就是让这 4 张图可以轮播起来。


组件底层封装

首先我们需要给我们之前写的代码做一下封装,便于我们开始编写这个组件。

这样我们就封装好我们组件的底层框架的代码,代码示例如下:

function createElement(type, attributes, ...children) {
  // 创建元素
  let element;
  if (typeof type === 'string') {
    element = new ElementWrapper(type);
  } else {
    element = new type();
  }

  // 挂上属性
  for (let name in attributes) {
    element.setAttribute(name, attributes[name]);
  }
  // 挂上所有子元素
  for (let child of children) {
    if (typeof child === 'string') child = new TextWrapper(child);
    element.appendChild(child);
  }
  // 最后我们的 element 就是一个节点
  // 所以我们可以直接返回
  return element;
}

export class Component {
  constructor() {
  }
  // 挂載元素的属性
  setAttribute(name, attribute) {
    this.root.setAttribute(name, attribute);
  }
  // 挂載元素子元素
  appendChild(child) {
    child.mountTo(this.root);
  }
  // 挂載当前元素
  mountTo(parent) {
    parent.appendChild(this.root);
  }
}

class ElementWrapper extends Component {
  // 构造函数
  // 创建 DOM 节点
  constructor(type) {
    this.root = document.createElement(type);
  }
}

class TextWrapper extends Component {
  // 构造函数
  // 创建 DOM 节点
  constructor(content) {
    this.root = document.createTextNode(content);
  }
}

实现 Carousel

接下来我们就要继续改造我们的 main.js。首先我们需要把 Div 改为 Carousel 并且让它继承我们写好的 Component 父类,这样我们就可以省略重复实现一些方法。

继承了 Component后,我们就要从 framework.js 中 import 我们的 Component。

这里我们就可以正式开始开发组件了,但是如果每次都需要手动 webpack 打包一下,就特别的麻烦。所以为了让我们可以更方便的调试代码,这里我们就一起来安装一下 webpack dev server 来解决这个问题。

执行一下代码,安装 webpack-dev-server

npm install --save-dev webpack-dev-server webpack-cli

看到上面这个结果,就证明我们安装成功了。我们最好也配置一下我们 webpack 服务器的运行文件夹,这里我们就用我们打包出来的 dist 作为我们的运行目录。

设置这个我们需要打开我们的 webpack.config.js,然后加入 devServer 的参数, contentBase 给予 ./dist 这个路径。

module.exports = {
  entry: './main.js',
  mode: 'development',
  devServer: {
    contentBase: './dist',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env'],
            plugins: [['@babel/plugin-transform-react-jsx', { pragma: 'createElement' }]],
          },
        },
      },
    ],
  },
};

用过 Vue 或者 React 的同学都知道,启动一个本地调试环境服务器,只需要执行 npm 命令就可以了。这里我们也设置一个快捷启动命令。打开我们的 package.json,在 scripts 的配置中添加一行 "start": "webpack start" 即可。

{
  "name": "jsx-component",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack serve"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.12.3",
    "@babel/plugin-transform-react-jsx": "^7.12.5",
    "@babel/preset-env": "^7.12.1",
    "babel-loader": "^8.1.0",
    "webpack": "^5.4.0",
    "webpack-cli": "^4.2.0",
    "webpack-dev-server": "^3.11.0"
  },
  "dependencies": {}
}

这样我们就可以直接执行下面这个命令启动我们的本地调试服务器啦!

npm start

开启了这个之后,当我们修改任何文件时都会被监听到,这样就会实时给我们打包文件,非常方便我们调试。看到上图里面表示,我们的实时本地服务器地址就是 http://localhost:8080。我们在浏览器直接打开这个地址就可以访问这个项目。

这里要注意的一个点,我们把运行的目录改为了 dist,因为我们之前的 main.html 是放在根目录的,这样我们就在 localhost:8080 上就找不到这个 HTML 文件了,所以我们需要把 main.html 移动到 dist 目录下,并且改一下 main.js 的引入路径。

<!-- main.html 代码 -->
<body></body>

<script src="./main.js"></script>

打开链接后我们发现 Carousel 组件已经被挂載成功了,这个证明我们的代码封装是没有问题的。

接下来我们继续来实现我们的轮播图功能,首先要把我们的图片数据传进去我们的 Carousel 组件里面。

let a = <Carousel src={gallery}/>;

这样我们的 gallery 数组就会被设置到我们的 src 属性上。但是我们的这个 src 属性不是给我们的 Carousel 自身的元素使用的。也就说我们不是像之前那样直接挂載到 this.root 上。

所以我们需要另外储存这个 src 上的数据,后面使用它来生成我们轮播图的图片展示元素。在 React 里面是用 props 来储存元素属性,但是这里我们就用一个更加接近属性意思的 attributes 来储存。

因为我们需要储存进来的属性到 this.attributes 这个变量中,所以我们需要在 Component 类的 constructor 中先初始化这个类属性。

然后这个 attributes 是需要我们另外存储到类属性中,而不是挂載到我们元素节点上。所以我们需要在组件类中重新定义我们的 setAttribute 方法。

我们需要在组件渲染之前能拿到 src 属性的值,所以我们需要把 render 的触发放在 mountTo 之内。

class Carousel extends Component {
  // 构造函数
  // 创建 DOM 节点
  constructor() {
    super();
    this.attributes = Object.create(null);
  }
  setAttribute(name, value) {
    this.attributes[name] = value;
  }
  render() {
 console.log(this.attributes);
    return document.createElement('div');
  }
  mountTo() {
    parent.appendChild(this.render());
  }
}

接下来我们看看实际运行的结果,看看是不是能够获得图片的数据。

接下来我们就去把这些图给显示出来。这里我们需要改造一下 render 方法,在这里加入渲染图片的逻辑:

class Carousel extends Component {
  // 构造函数
  // 创建 DOM 节点
  constructor() {
    super();
    this.attributes = Object.create(null);
  }
  setAttribute(name, value) {
    this.attributes[name] = value;
  }
  render() {
    this.root = document.createElement('div');

    for (let picture of this.attributes.src) {
      let child = document.createElement('img');
      child.src = picture;
      this.root.appendChild(child);
    }

    return this.root;
  }
  mountTo(parent) {
    parent.appendChild(this.render());
  }
}

就这样我们就可以看到我们的图片被正确的显示在我们的页面上。


排版与动画

首先我们图片的元素都是 img 标签,但是使用这个标签的话,当我们点击并且拖动的时候它自带就是可以被拖拽的。当然这个也是可以解决的,但是为了更简单的解决这个问题,我们就把 img 换成 div,然后使用 background-image。

默认 div 是没有宽高的,所以我们需要在组件的 div 这一层加一个 class 叫 carousel,然后在 HTML 中加入 css 样式表,直接选择 carousel 下的每一个 div,然后给他们合适的样式。

// main.js
class Carousel extends Component {
  // 构造函数
  // 创建 DOM 节点
  constructor() {
    super();
    this.attributes = Object.create(null);
  }
  setAttribute(name, value) {
    this.attributes[name] = value;
  }
  render() {
    this.root = document.createElement('div');
 this.root.addClassList('carousel'); // 加入 carousel class

    for (let picture of this.attributes.src) {
      let child = document.createElement('div');
      child.backgroundImage = `url('${picture}')`;
      this.root.appendChild(child);
    }

    return this.root;
  }
  mountTo(parent) {
    parent.appendChild(this.render());
  }
}
<!-- main.html -->
<head>
  <style>
    .carousel > div {
      width: 500px;
      height: 281px;
      background-size: contain;
    }
  </style>
</head>

<body></body>

<script src="./main.js"></script>

这里我们的宽是 500px,但是如果我们设置一个高是 300px,我们会发现图片的底部出现了一个图片重复的现象。这是因为图片的比例是 1600 x 900,而 500 x 300 比例与图片原来的比例不一致。

所以通过比例计算,我们可以得出这样一个高度:。所以 500px 宽对应比例的高大概就是 281px。这样我们的图片就可以正常的显示在一个 div 里面了。

一个轮播图显然不可能所有的图片都显示出来的,我们认知中的轮播图都是一张一张图片显示的。首先我们需要让图片外层的 carousel div 元素有一个和它们一样宽高的盒子,然后我们设置 overflow: hidden。这样其他图片就会超出盒子所以被隐藏了。

!! 这里有些同学可能问:“为什么不把其他图片改为 display: hidden 或者 opacity:0 呢?” 因为我们的轮播图在轮播的时候,实际上是可以看到当前的图片和下一张图片的。所以如果我们用了 display: hidden 这种隐藏属性,我们后面的效果就不好做了。

然后我们又有一个问题,轮播图一般来说都是左右滑动的,很少见是上下滑动的,但是我们这里图片就是默认从上往下排布的。所以这里我们需要调整图片的布局,让它们拍成一行。

这里我们使用正常流就可以了,所以只需要给 div 加上一个 display: inline-block,就可以让它们排列成一行,但是只有这个属性的话,如果图片超出了窗口宽度就会自动换行,所以我们还需要在它们父级加入强制不换行的属性 white-space: nowrap。这样我们就大功告成了。

<head>
  <style>
    .carousel {
      width: 500px;
      height: 281px;
      white-space: nowrap;
      overflow: hidden;
    }

    .carousel > div {
      width: 500px;
      height: 281px;
      background-size: contain;
      display: inline-block;
    }
  </style>
</head>

<body></body>

<script src="./main.js"></script>

接下来我们来实现自动轮播效果,在做这个之前我们先给这些图片元素加上一些动画属性。这里我们用 transition 来控制元素动效的时间,一般来说我们播一帧会用 0.5 秒 的 ease

!! Transition 一般来说都只用 ease 这个属性,除非是一些非常特殊的情况,ease-in 会用在推出动画当中,而 ease-out 就会用在进入动画当中。在同一屏幕上的,我们一般默认都会使用 ease,但是 linear 在大部分情况下我们是永远不会去用的。因为 ease 是最符合人类的感觉的一种运动曲线。

<head>
  <style>
    .carousel {
      width: 500px;
      height: 281px;
      white-space: nowrap;
      overflow: hidden;
    }

    .carousel > div {
      width: 500px;
      height: 281px;
      background-size: contain;
      display: inline-block;
      transition: ease 0.5s;
    }
  </style>
</head>

<body></body>

<script src="./main.js"></script>

实现自动轮播

有了动画效果属性,我们就可以在 JavaScript 中加入我们的定时器,让我们的图片在每三秒钟切换一次图片。我们使用 setInerval() 这个函数就可以解决这个问题了。

但是我们怎么才能让图片轮播,或者移动呢?想到 HTML 中的移动,大家有没有想到 CSS 当中有什么属性可以让我们移动元素的呢?

对没错,就是使用 transform,它就是在 CSS 当中专门用于挪动元素的。所以这里我们的逻辑就是,每 3 秒往左边挪动一次元素自身的长度,这样我们就可以挪动到下一张图的开始。

但是这样只能挪动一张图,所以如果我们需要挪动第二次,到达第三张图,我们就要让每一张图偏移 200%,以此类推。所以我们需要一个当前页数的值,叫做 current,默认值为 0。每次挪动的时候时就加一,这样偏移的值就是 页数。这样我们就完成了图片多次移动,一张一张图片展示了。

class Carousel extends Component {
  // 构造函数
  // 创建 DOM 节点
  constructor() {
    super();
    this.attributes = Object.create(null);
  }
  setAttribute(name, value) {
    this.attributes[name] = value;
  }
  render() {
    this.root = document.createElement('div');
    this.root.classList.add('carousel');

    for (let picture of this.attributes.src) {
      let child = document.createElement('div');
      child.style.backgroundImage = `url('${picture}')`;
      this.root.appendChild(child);
    }

    let current = 0;
    setInterval(() => {
      let children = this.root.children;
      ++current;
      for (let child of children) {
        child.style.transform = `translateX(-${100 * current}%)`;
      }
    }, 3000);

    return this.root;
  }
  mountTo(parent) {
    parent.appendChild(this.render());
  }
}

这里我们发现一个问题,这个轮播是不会停止的,一直往左偏移没有停止。而我们需要轮播到最后一张的时候是回到一张图的。

要解决这个问题,我们可以利用一个数学的技巧,如果我们想要一个数是在 1 到 N 之间不断循环,我们就让它对 n 取余就可以了。在我们元素中,children 的长度是 4,所以当我们 current 到达 4 的时候, 的余数就是 0,所以每次把 current 设置成 current 除以 children 长度的余数就可以达到无限循环了。

!! 这里 current 就不会超过 4, 到达 4 之后就会回到 0。

用这个逻辑来实现我们的轮播,确实能让我们的图片无限循环,但是如果我们运行一下看看的话,我们又会发现另外一个问题。当我们播放到最后一个图片之后,就会快速滑动到第一个张图片,我们会看到一个快速回退的效果。这个确实不是那么好,我们想要的效果是,到达最后一张图之后,第一张图就直接在后面接上。

那么我们就一起去尝试解决这个问题,经过观察其实在屏幕上一次最多就只能看到两张图片。那么其实我们就把这两张图片挪到正确的位置就可以了。

所以我们需要找到当前看到的图片,还有下一张图片,然后每次移动到下一张图片就找到再下一张图片,把下一张图片挪动到正确的位置。

讲到这里可能还是有点懵,但是不要紧,我们来整理一下逻辑。

接下来我们把上面的逻辑翻译成 JavaScript:

class Carousel extends Component {
  // 构造函数
  // 创建 DOM 节点
  constructor() {
    super();
    this.attributes = Object.create(null);
  }
  setAttribute(name, value) {
    this.attributes[name] = value;
  }
  render() {
    this.root = document.createElement('div');
    this.root.classList.add('carousel');

    for (let picture of this.attributes.src) {
      let child = document.createElement('div');
      child.style.backgroundImage = `url('${picture}')`;
      this.root.appendChild(child);
    }

    // 当前图片的 index
    let currentIndex = 0;
    setInterval(() => {
      let children = this.root.children;
      // 下一张图片的 index
      let nextIndex = (currentIndex + 1) % children.length;

      // 当前图片的节点
      let current = children[currentIndex];
      // 下一张图片的节点
      let next = children[nextIndex]; 

      // 禁用图片的动效
      next.style.transition = 'none'; 
      // 移动下一张图片到正确的位置
      next.style.transform = `translateX(${-100 * (nextIndex - 1)}%)`;

      // 执行轮播效果,延迟了一帧的时间 16 毫秒
      setTimeout(() => {
        // 启用 CSS 中的动效
        next.style.transition = ''; 
        // 先移动当前图片离开当前位置
        current.style.transform = `translateX(${-100 * (currentIndex + 1)}%)`;
        // 移动下一张图片到当前显示的位置
        next.style.transform = `translateX(${-100 * nextIndex}%)`;

        // 最后更新当前位置的 index
        currentIndex = nextIndex;
      }, 16);
    }, 3000);

    return this.root;
  }
  mountTo(parent) {
    parent.appendChild(this.render());
  }
}

如果我们先去掉 overflow: hidden 的话,我们就可以很清晰的看到所有图片移动的轨迹了:


实现拖拽轮播

一般来说我们的轮播组件除了这种自动轮播的功能之外,还有可以使用我们的鼠标进行拖动来轮播。所以接下来我们一起来实现这个手动轮播功能。

因为自动轮播和手动轮播是有一定的冲突的,所以我们需要把我们前面实现的自动轮播的代码给注释掉。然后我们就可以使用这个轮播组件下的 children (子元素),也就是所有图片的元素,来实现我们的手动拖拽轮播功能。

那么拖拽的功能主要就是涉及我们的图片被拖动,所以我们需要给图片加入鼠标的监听事件。如果我们根据操作步骤来想的话,就可以整理出这么一套逻辑:

this.root.addEventListener('mousedown', event => {
  console.log('mousedown');
});

this.root.addEventListener('mousemove', event => {
  console.log('mousemove');
});

this.root.addEventListener('mouseup', event => {
  console.log('mouseup');
});

执行一下以上代码后,我们就会在 console 中看到,当我们鼠标放到图片上并且移动时,我们会不断的触发 mousemove。但是我们想要的效果是,当我们鼠标按住时移动才会触发 mousemove,我们鼠标单纯在图片上移动是不应该触发事件的。

所以我们需要把 mousemove 和 mouseup 两个事件,放在 mousedown 事件的回调函数当中,这样才能正确的在鼠标按住的时候监听移动和松开两个动作。这里还需要考虑,当我们 mouseup 的时候,我们需要把 mousemove 和 mouseup 两个监听事件给停掉,所以我们需要用函数把它们单独的存起来。

this.root.addEventListener('mousedown', event => {
  console.log('mousedown');

  let move = event => {
    console.log('mousemove');
  };

  let up = event => {
    this.root.removeEventListener('mousemove', move);
    this.root.removeEventListener('mouseup', up);
  };

  this.root.addEventListener('mousemove', move);
  this.root.addEventListener('mouseup', up);
});

这里我们在 mouseup 的时候就把 mousemove 和 mouseup 的事件给移除了。这个就是一般我们在做拖拽的时候都会用到的基础代码。

但是我们又会发现另外一个问题,鼠标点击拖动然后松开后,我们鼠标再次在图片上移动,还是会出发到我们的mousemove 事件。

这个是因为我们的 mousemove 是在 root 上被监听的。其实我们的 mousedown 已经是在 root 上监听,我们 mousemove 和 mouseup 就没有必要在 root 上监听了。

所以我们可以在 document 上直接监听这两个事件,而在现代浏览器当中,使用 document 监听还有额外的好处,即使我们的鼠标移出浏览器窗口外我们一样可以监听到事件。

this.root.addEventListener('mousedown', event => {
  console.log('mousedown');

  let move = event => {
    console.log('mousemove');
  };

  let up = event => {
    document.removeEventListener('mousemove', move);
    document.removeEventListener('mouseup', up);
  };

  document.addEventListener('mousemove', move);
  document.addEventListener('mouseup', up);
});

有了这个完整的监听机制之后,我们就可以尝试在 mousemove 里面去实现轮播图的移动功能了。我们一起来整理一下这个功能的逻辑:

this.root.addEventListener('mousedown', event => {
  let children = this.root.children;
  let startX = event.clientX;

  let move = event => {
    let x = event.clientX - startX;
    for (let child of children) {
      child.style.transition = 'none';
      child.style.transform = `translateX(${x}px)`;
    }
  };

  let up = event => {
    document.removeEventListener('mousemove', move);
    document.removeEventListener('mouseup', up);
  };

  document.addEventListener('mousemove', move);
  document.addEventListener('mouseup', up);
});

好,到了这里我们发现了两个问题:

  1. 我们第一次点击然后拖动的时候图片的起始位置是对的,但是我们再点击的时候图片的位置就不对了。
  2. 我们拖动了图片之后,当我们松开鼠标按钮,这个图片就会停留在拖动结束的位置了,但是在正常的轮播图组件中,我们如果拖动了图片超过一定的位置,就会自动轮播到下一张图的。

要解决这两个问题,我们可以这么计算,因为我们做的是一个轮播图的组件,按照现在一般的轮播组件来说,当我们把图片拖动在大于半个图的位置时,就会轮播到下一张图了,如果不到一半的位置的话就会回到当前拖动的图的位置。

按照这样的一个需求,我们就需要记录一个 position,它记录了当前是第几个图片(从 0 开始计算)。如果我们每张图片都是 500px 宽,那么第一张图的 current 就是 0,偏移的距离就是 0 * 500 = 0, 而第二张图就是 1 * 500 px,第三张图就是 2 * 500px,以此类推。根据这样的规律,第 N 张图的偏移位置就是 。

this.root.addEventListener('mousedown', event => {
  let children = this.root.children;
  let startX = event.clientX;

  let move = event => {
    let x = event.clientX - startX;
    for (let child of children) {
      child.style.transition = 'none';
      child.style.transform = `translateX(${x - current * 500}px)`;
    }
  };

  let up = event => {
    let x = event.clientX - startX;
    current = current - Math.round(x / 500);
    for (let child of children) {
      child.style.transition = '';
      child.style.transform = `translateX(${-current * 500}px)`;
    }
    document.removeEventListener('mousemove', move);
    document.removeEventListener('mouseup', up);
  };

  document.addEventListener('mousemove', move);
  document.addEventListener('mouseup', up);
});

!! 注意这里我们用的 500 作为图片的长度,那是因为我们自己写的图片组件,它的图片被我们固定为 500px 宽,而如果我们需要做一个通用的轮播组件的话,最好就是获取元素的实际宽度,Element.clientWith()。这样我们的组件是可以随着使用者去改变的。

做到这里,我们就可以用拖拽来轮播我们的图片了,但是当我们拖到最后一张图的时候,我们就会发现最后一张图之后就是空白了,第一张图没有接着最后一张。

那么接下来我们就去完善这个功能。这里其实和我们的自动轮播是非常相似的,在做自动轮播的时候我们就知道,每次轮播图片的时候,我们最多就只能看到两张图片,可以看到三张图片的机率是非常小的,因为我们的轮播的宽度相对我们的页面来说是非常小的,除非用户有足够的位置去拖到第二张图以外才会出现这个问题。但是这里我们就不考虑这种因素了。

我们确定每次拖拽的时候只会看到两张图片,所以我们也可以像自动轮播那样去处理拖拽的轮播。但是这里有一个点是不一样的,我们自动轮播的时候,图片只会走一个方向,要么左要么右边。但是我们手动就可以往左或者往右拖动,图片是可以走任意方向的。所以我们就无法直接用自动轮播的代码来实现这个功能了。我们就需要自己重新处理一下轮播头和尾无限循环的逻辑。

!! 我们来证明一下这个公式是正确的,首先如果我们遇到 current = 0, 那么 0 这个位置的图片的上一张就会获得 -1 这个指针,这个时候我们用 ,这里 3 除以 4 的余数就是 3,而 3 刚好就是这个数组的最后一个图片。

!! 然后我们来试试,如果当前图片就是数组里面的最后一张图,在我们的例子里面就是 3,3 + 1 = 4, 这个时候通过转换 余数就是 0,显然我们获得的数字就是数组的第一个图片的位置。

我们已经把整个逻辑给整理了一遍,下来我们看看 mousemove 这个事件回调函数代码的应该怎么写:

let move = event => {
  let x = event.clientX - startX;

  let current = position - Math.round(x / 500);

  for (let offset of [-1, 0, 1]) {
    let pos = current + offset;
    // 计算图片所在 index
    pos = (pos + children.length) % children.length;
    console.log('pos', pos);

    children[pos].style.transition = 'none';
    children[pos].style.transform = `translateX(${-pos * 500 + offset * 500 + (x % 500)}px)`;
  }
};

!! 讲了那么多东西,代码就那么几行,确实代码简单不等于它背后的逻辑就简单。所以写代码的程序员也可以是深不可测的。

最后还有一个小问题,在我们拖拽的时候,我们会发现上一张图和下一张有一个奇怪跳动的现象。

这个问题是我们的 Math.round(x / 500) 所导致的,因为我们在 transform 的时候,加入了 x % 500, 而在我们的 current 值的计算中没有包含这一部分的计算,所以在鼠标拖动的时候就会缺少这部分的偏移度。

我们只需要把这里的 Math.round(x / 500) 改为 (x - x % 500) / 500 即可达到同样的取整数的效果,同时还可以保留我们 x 原有的正负值。

这里其实还有比较多的问题的,我们还没有去改 mouseup 事件里面的逻辑。那么接下来我们就来看看 up 中的逻辑我们应该怎么去实现。

这里我们需要改的就是 children 中 for 循环的代码,我们要实现的是让我们拖动图片超过一定的位置就会自动轮播到对应方向的下一张图片。up 这里的逻辑其实是和 move 是基本一样的,不过这里有几个地方需要更改的:

最终我们的代码就是这样的:

let up = event => {
  let x = event.clientX - startX;
  position = position - Math.round(x / 500);

  for (let offset of [0, -Math.sign(Math.round(x / 500) - x + 250 * Math.sign(x))]) {
    let pos = position + offset;
    // 计算图片所在 index
    pos = (pos + children.length) % children.length;

    children[pos].style.transition = '';
    children[pos].style.transform = `translateX(${-pos * 500 + offset * 500}px)`;
  }

  document.removeEventListener('mousemove', move);
  document.removeEventListener('mouseup', up);
};

改好了 up 函数之后,我们就真正完成了这个手动轮播的组件了。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8