在咱們用 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 函數以後,咱們就真正完成了這個手動輪播的組件了。
我是來自公衆號《技術銀河》的三鑽,一位正在重塑知識的技術人。下期再見。