在咱们用 JSX 创建组件系统以前,咱们先来用一个例子学习一下组件的实现原理和逻辑。这里咱们就用一个轮播图的组件做为例子进行学习。轮播图的英文叫作 Carousel,它有一个旋转木马的意思。javascript
上一篇文章《使用 JSX 创建 Markup 组件风格》中咱们实现的代码,其实还不能称为一个组件系统,顶可能是能够充当 DOM 的一个简单封装,让咱们有能力定制 DOM。css
要作这个轮播图的组件,咱们应该先从一个最简单的 DOM 操做入手。使用 DOM 操做把整个轮播图的功能先实现出来,而后在一步一步去考虑怎么把它设计成一个组件系统。html
TIPS:在开发中咱们每每一开始作一个组件的时候,都会过分思考一个功能应该怎么设计,而后就把它实现的很是复杂。其实更好的方式是反过来的,先把功能实现了,而后经过分析这个功能从而设计出一个组件架构体系。java
由于是轮播图,那咱们固然须要用到图片,因此这里我准备了 4 张来源于 Unsplash 的开源图片,固然你们也能够换成本身的图片。首先咱们把这 4 张图片都放入一个 gallery
的变量当中:react
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 张图能够轮播起来。webpack
首先咱们须要给咱们以前写的代码作一下封装,便于咱们开始编写这个组件。程序员
createElement
、 ElementWrapper
、TextWrapper
这三个移到咱们的 framework.js 文件中createElement
方法是须要 export 出去让咱们能够引入这个基础建立元素的方法。ElementWrapper
、TextWrapper
是不须要 export 的,由于它们都属于内部给 createElement 使用的ElementWrapper
、TextWrapper
之中都有同样的 setAttribute
、 appendChild
和 mountTo
,这些都是重复而且可公用的Component
类,把这三个方法封装进入ElementWrapper
和 TextWrapper
继承 Component
这样咱们就封装好咱们组件的底层框架的代码,代码示例以下:web
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);
}
}
复制代码
接下来咱们就要继续改造咱们的 main.js
。首先咱们须要把 Div 改成 Carousel 而且让它继承咱们写好的 Component 父类,这样咱们就能够省略重复实现一些方法。shell
继承了 Component后,咱们就要从 framework.js
中 import 咱们的 Component。npm
这里咱们就能够正式开始开发组件了,可是若是每次都须要手动 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 方法,在这里加入渲染图片的逻辑:
this.root
上this.root
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。
用这个逻辑来实现咱们的轮播,确实能让咱们的图片无限循环,可是若是咱们运行一下看看的话,咱们又会发现另一个问题。当咱们播放到最后一个图片以后,就会快速滑动到第一个张图片,咱们会看到一个快速回退的效果。这个确实不是那么好,咱们想要的效果是,到达最后一张图以后,第一张图就直接在后面接上。
那么咱们就一块儿去尝试解决这个问题,通过观察其实在屏幕上一次最多就只能看到两张图片。那么其实咱们就把这两张图片挪到正确的位置就能够了。
因此咱们须要找到当前看到的图片,还有下一张图片,而后每次移动到下一张图片就找到再下一张图片,把下一张图片挪动到正确的位置。
讲到这里可能仍是有点懵,可是没关系,咱们来整理一下逻辑。
-100%
index - 1
的图片距离,也就是说咱们要移动的距离是 (index - 1) * -100%
transition
transition
从新开启,这样咱们 CSS 中的动效就会从新起效,由于接下来的轮播效果是须要有动画效果的index * -100%
让任何一张在 index 位置的图片移动到当前位置的公式,那么要再往右边移动多一个位置,那就是 (index + 1) * -100%
便可index * -100%
咯currentIndex = nextIndex
,这样就大功告成了!接下来咱们把上面的逻辑翻译成 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 (子元素),也就是全部图片的元素,来实现咱们的手动拖拽轮播功能。
那么拖拽的功能主要就是涉及咱们的图片被拖动,因此咱们须要给图片加入鼠标的监听事件。若是咱们根据操做步骤来想的话,就能够整理出这么一套逻辑:
mousedown
鼠标按下事件。mousemove
鼠标移动事件。mouseup
鼠标松开事件。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 里面去实现轮播图的移动功能了。咱们一块儿来整理一下这个功能的逻辑:
event
参数去捕获到鼠标的坐标。event
上其实有不少个鼠标的坐标,好比 offsetX
、offsetY
等等,这些都是根据不一样的参考系所得到坐标的。在这里咱们比较推荐使用的是 clientX
和 clientY
startX
和 startY
,它们的默认值就是对应的当前 clientX 和 clientYclientX - startX
和 clientY - startY
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);
});
复制代码
好,到了这里咱们发现了两个问题:
要解决这两个问题,咱们能够这么计算,由于咱们作的是一个轮播图的组件,按照如今通常的轮播组件来讲,当咱们把图片拖动在大于半个图的位置时,就会轮播到下一张图了,若是不到一半的位置的话就会回到当前拖动的图的位置。
按照这样的一个需求,咱们就须要记录一个 position
,它记录了当前是第几个图片(从 0 开始计算)。若是咱们每张图片都是 500px 宽,那么第一张图的 current 就是 0,偏移的距离就是 0 * 500 = 0, 而第二张图就是 1 * 500 px,第三张图就是 2 * 500px,以此类推。根据这样的规律,第 N 张图的偏移位置就是
。
position
值。x
除与咱们每张图的 长度
(咱们这个组件控制了图片是 500px,因此咱们就用 x 除与 500),这样咱们就会得出一个 0 到 1 的数字。若是这个数字等于或超过 0.5 那么就是过了图一半的长度了,就能够直接轮播到下一张图,若是是小于 0.5 就能够移动回去当前图的起始位置。position
,若是大于等于 0.5 就能够四舍五入变成 1, 不然就是 0。这里的 1 表明咱们能够把 position
+ 1,若是是 0 那么 position
就不会变。这样直接改变 current 的值,在 transform 的时候就会自动按照新的 current 值作计算,轮播的效果就达成了。x
是能够左右移动的距离值,也就是说若是咱们鼠标是往左移动的话,x
就会是负数,而相反就是正数,咱们的轮播组件鼠标往左拖动就是前进,而往右拖动就是回退。因此这里运算这个 超出值
的时候就是 position = position - Math.round(x/500)
。好比咱们鼠标往左边挪动了 400px,当前 current 值是 0,那么position = 0 - Math.round(400/500) = 0 - -1 = 0 + 1 = 1
因此最后咱们的 current 变成了 1
。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
,它的值与咱们以前在 mouseup 计算的 position 是同样的 position + Math.round(x/500)
[-1, 0, 1]
的数组,对应的是前一个元素
,当前元素
和下一个元素
,这里咱们须要使用这三个偏移值,获取到上一个图片,当前拖动的图片和下一个图片的移动位置,这三个位置是跟随着咱们鼠标的拖动实时计算的图片位置 = 当前图片位置 + 偏移
,这里能够这么理解若是当前图片是在 2 这个位置,上一张图就是在 1,下一张图就在 3-1
,按照咱们图片的数据结构来讲,数组里面是没有 -1
这个位置的。因此当咱们遇到计算出来的位置是负数的时候咱们就要把它转成这一列图片的最后一张图的位置。(当前指针 + 数组总长度)/ 数组总长度
的 余数
,这个得到的余数就正好是翻转的。咱们来证实一下这个公式是正确的,首先若是咱们遇到 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 是基本同样的,不过这里有几个地方须要更改的:
' '
空+ x % 500
就不须要了,由于这里图片是咱们鼠标松开的时候,不须要图片再跟随咱们鼠标的位置了pos = current + offset
的这里,咱们在 up 的回调中是没有 current 的,因此咱们须要把 current 改成 positionfor of
循环是没有顺序要求的,因此咱们能够把 -1 和 1 这两个数字用一个公式来代替,放在咱们 0 的后面。可是怎么才能找到咱们须要的是哪一边呢?position = position - Math.round(x / 500)
这行代码,这个方向能够经过 Math.round(x / 500) - x
得到。而这个值就是相对当前元素的中间,他是更偏向左边(负数)仍是右边(正数),其实这个数字是多少并非最重要的,咱们要的是它的符号也就是 -1 仍是 1,因此这里咱们就可使用 - Math.sign(Math.round(x / 500) - x)
来取得结果中的符号,这个函数最终返回要不就是 -1, 要不就是 1 了, 正好是咱们想要的。+ 250 * Match.sign(x)
,这样咱们的计算才会合算出是应该往那边移动。最终咱们的代码就是这样的:
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 函数以后,咱们就真正完成了这个手动轮播的组件了。
我是来自公众号《技术银河》的三钻,一位正在重塑知识的技术人。下期再见。