使用JSX實現(xiàn)Carousel輪播組件的方法(前端組件化)
在我們用 JSX 建立組件系統(tǒng)之前,我們先來用一個例子學習一下組件的實現(xiàn)原理和邏輯。這里我們就用一個輪播圖的組件作為例子進行學習。輪播圖的英文叫做 Carousel,它有一個旋轉木馬的意思。
上一篇文章《使用 JSX 建立 Markup 組件風格》中我們實現(xiàn)的代碼,其實還不能稱為一個組件系統(tǒng),頂多是可以充當 DOM 的一個簡單封裝,讓我們有能力定制 DOM。
要做這個輪播圖的組件,我們應該先從一個最簡單的 DOM 操作入手。使用 DOM 操作把整個輪播圖的功能先實現(xiàn)出來,然后在一步一步去考慮怎么把它設計成一個組件系統(tǒng)。
TIPS:在開發(fā)中我們往往一開始做一個組件的時候,都會過度思考一個功能應該怎么設計,然后就把它實現(xiàn)的非常復雜。其實更好的方式是反過來的,先把功能實現(xiàn)了,然后通過分析這個功能從而設計出一個組件架構體系。
因為是輪播圖,那我們當然需要用到圖片,所以這里我準備了 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 張圖可以輪播起來。
組件底層封裝
首先我們需要給我們之前寫的代碼做一下封裝,便于我們開始編寫這個組件。
- 根目錄建立 framework.js
- 把
createElement
、ElementWrapper
、TextWrapper
這三個移到我們的 framework.js 文件中 - 然后
createElement
方法是需要 export 出去讓我們可以引入這個基礎創(chuàng)建元素的方法。 ElementWrapper
、TextWrapper
是不需要 export 的,因為它們都屬于內部給 createElement 使用的- 封裝 Wrapper 類中公共部分
ElementWrapper
、TextWrapper
之中都有一樣的setAttribute
、appendChild
和mountTo
,這些都是重復并且可公用的- 所以我們可以建立一個
Component
類,把這三個方法封裝進入 - 然后讓
ElementWrapper
和TextWrapper
繼承Component
- Component 加入 render() 方法
- 在 Component 類中加入 構造函數(shù)
這樣我們就封裝好我們組件的底層框架的代碼,代碼示例如下:
function createElement(type, attributes, ...children) { // 創(chuàng)建元素 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 就是一個節(jié)點 // 所以我們可以直接返回 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 { // 構造函數(shù) // 創(chuàng)建 DOM 節(jié)點 constructor(type) { this.root = document.createElement(type); } } class TextWrapper extends Component { // 構造函數(shù) // 創(chuàng)建 DOM 節(jié)點 constructor(content) { this.root = document.createTextNode(content); } }
實現(xiàn) Carousel
接下來我們就要繼續(xù)改造我們的 main.js
。首先我們需要把 Div 改為 Carousel 并且讓它繼承我們寫好的 Component 父類,這樣我們就可以省略重復實現(xiàn)一些方法。
繼承了 Component后,我們就要從 framework.js
中 import 我們的 Component。
這里我們就可以正式開始開發(fā)組件了,但是如果每次都需要手動 webpack 打包一下,就特別的麻煩。所以為了讓我們可以更方便的調試代碼,這里我們就一起來安裝一下 webpack dev server 來解決這個問題。
執(zhí)行一下代碼,安裝 webpack-dev-server
:
npm install --save-dev webpack-dev-server webpack-cli
看到上面這個結果,就證明我們安裝成功了。我們最好也配置一下我們 webpack 服務器的運行文件夾,這里我們就用我們打包出來的 dist
作為我們的運行目錄。
設置這個我們需要打開我們的 webpack.config.js
,然后加入 devServer
的參數(shù), 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 的同學都知道,啟動一個本地調試環(huán)境服務器,只需要執(zhí)行 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": {} }
這樣我們就可以直接執(zhí)行下面這個命令啟動我們的本地調試服務器啦!
npm start
開啟了這個之后,當我們修改任何文件時都會被監(jiān)聽到,這樣就會實時給我們打包文件,非常方便我們調試??吹缴蠄D里面表示,我們的實時本地服務器地址就是 http://localhost:8080
。我們在瀏覽器直接打開這個地址就可以訪問這個項目。
這里要注意的一個點,我們把運行的目錄改為了 dist,因為我們之前的 main.html 是放在根目錄的,這樣我們就在 localhost:8080 上就找不到這個 HTML 文件了,所以我們需要把 main.html 移動到 dist 目錄下,并且改一下 main.js 的引入路徑。
<!-- main.html 代碼 --> <body></body> <script src="./main.js"></script>
打開鏈接后我們發(fā)現(xiàn) Carousel 組件已經(jīng)被掛載成功了,這個證明我們的代碼封裝是沒有問題的。
接下來我們繼續(xù)來實現(xiàn)我們的輪播圖功能,首先要把我們的圖片數(shù)據(jù)傳進去我們的 Carousel 組件里面。
let a = <Carousel src={gallery}/>;
這樣我們的 gallery
數(shù)組就會被設置到我們的 src
屬性上。但是我們的這個 src
屬性不是給我們的 Carousel 自身的元素使用的。也就說我們不是像之前那樣直接掛載到 this.root
上。
所以我們需要另外儲存這個 src 上的數(shù)據(jù),后面使用它來生成我們輪播圖的圖片展示元素。在 React 里面是用 props
來儲存元素屬性,但是這里我們就用一個更加接近屬性意思的 attributes
來儲存。
因為我們需要儲存進來的屬性到 this.attributes
這個變量中,所以我們需要在 Component 類的 constructor
中先初始化這個類屬性。
然后這個 attributes 是需要我們另外存儲到類屬性中,而不是掛載到我們元素節(jié)點上。所以我們需要在組件類中重新定義我們的 setAttribute
方法。
我們需要在組件渲染之前能拿到 src 屬性的值,所以我們需要把 render 的觸發(fā)放在 mountTo
之內。
class Carousel extends Component { // 構造函數(shù) // 創(chuàng)建 DOM 節(jié)點 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()); } }
接下來我們看看實際運行的結果,看看是不是能夠獲得圖片的數(shù)據(jù)。
接下來我們就去把這些圖給顯示出來。這里我們需要改造一下 render 方法,在這里加入渲染圖片的邏輯:
- 首先我們需要把創(chuàng)建的新元素儲起來
- 循環(huán)我們的圖片數(shù)據(jù),給每條數(shù)據(jù)創(chuàng)建一個 img 元素
- 給每一個 img 元素附上 src = 圖片 url
- 把附上 src 屬性的圖片元素掛載到我們的組件元素
this.root
上 - 最后讓 render 方法返回
this.root
class Carousel extends Component { // 構造函數(shù) // 創(chuàng)建 DOM 節(jié)點 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 { // 構造函數(shù) // 創(chuàng)建 DOM 節(jié)點 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,我們會發(fā)現(xiàn)圖片的底部出現(xiàn)了一個圖片重復的現(xiàn)象。這是因為圖片的比例是 1600 x 900
,而 500 x 300
比例與圖片原來的比例不一致。
所以通過比例計算,我們可以得出這樣一個高度: 500 ÷ 1900 × 900 = 281. x x x 500\div1900\times900 = 281.xxx 500÷1900×900=281.xxx。所以 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>
接下來我們來實現(xiàn)自動輪播效果,在做這個之前我們先給這些圖片元素加上一些動畫屬性。這里我們用 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>
實現(xiàn)自動輪播
有了動畫效果屬性,我們就可以在 JavaScript 中加入我們的定時器,讓我們的圖片在每三秒鐘切換一次圖片。我們使用 setInerval()
這個函數(shù)就可以解決這個問題了。
但是我們怎么才能讓圖片輪播,或者移動呢?想到 HTML 中的移動,大家有沒有想到 CSS 當中有什么屬性可以讓我們移動元素的呢?
對沒錯,就是使用 transform
,它就是在 CSS 當中專門用于挪動元素的。所以這里我們的邏輯就是,每 3 秒往左邊挪動一次元素自身的長度,這樣我們就可以挪動到下一張圖的開始。
但是這樣只能挪動一張圖,所以如果我們需要挪動第二次,到達第三張圖,我們就要讓每一張圖偏移 200%,以此類推。所以我們需要一個當前頁數(shù)的值,叫做 current
,默認值為 0。每次挪動的時候時就加一,這樣偏移的值就是 − 100 × 頁 數(shù) -100\times頁數(shù) −100×頁數(shù)。這樣我們就完成了圖片多次移動,一張一張圖片展示了。
class Carousel extends Component { // 構造函數(shù) // 創(chuàng)建 DOM 節(jié)點 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()); } }
這里我們發(fā)現(xiàn)一個問題,這個輪播是不會停止的,一直往左偏移沒有停止。而我們需要輪播到最后一張的時候是回到一張圖的。
要解決這個問題,我們可以利用一個數(shù)學的技巧,如果我們想要一個數(shù)是在 1 到 N 之間不斷循環(huán),我們就讓它對 n 取余就可以了。在我們元素中,children 的長度是 4,所以當我們 current 到達 4 的時候, 4 ÷ 4 4\div4 4÷4 的余數(shù)就是 0,所以每次把 current 設置成 current 除以 children 長度的余數(shù)就可以達到無限循環(huán)了。
這里 current 就不會超過 4, 到達 4 之后就會回到 0。
用這個邏輯來實現(xiàn)我們的輪播,確實能讓我們的圖片無限循環(huán),但是如果我們運行一下看看的話,我們又會發(fā)現(xiàn)另外一個問題。當我們播放到最后一個圖片之后,就會快速滑動到第一個張圖片,我們會看到一個快速回退的效果。這個確實不是那么好,我們想要的效果是,到達最后一張圖之后,第一張圖就直接在后面接上。
那么我們就一起去嘗試解決這個問題,經(jīng)過觀察其實在屏幕上一次最多就只能看到兩張圖片。那么其實我們就把這兩張圖片挪到正確的位置就可以了。
所以我們需要找到當前看到的圖片,還有下一張圖片,然后每次移動到下一張圖片就找到再下一張圖片,把下一張圖片挪動到正確的位置。
講到這里可能還是有點懵,但是不要緊,我們來整理一下邏輯。
獲取當前圖片 index 和 下一張圖的 index
- 首先輪播肯定是從第一張圖開始,而這張圖在我們的節(jié)點中肯定是第 0 個
- 因為我們需要在看到一張圖的時候就準備第二張圖,所以我們就需要找到下一張圖的位置
- 根據(jù)我們上面說的,下一張圖的位置,我們可以使用數(shù)學里的技巧來獲得: 下 一 張 圖 的 位 置 = ( 當 前 位 置 + 1 ) ÷ 圖 片 數(shù) 量 下一張圖的位置 = (當前位置 + 1)\div 圖片數(shù)量下一張圖的位置=(當前位置+1)÷圖片數(shù)量 的余數(shù),根據(jù)這個公式,當我們達到圖片最后一張的時候,就會返回 0,回到第一個圖片的位置
計算圖片移動的距離,保持當前圖片后面有一張圖片等著被挪動過來
- 當前顯示的圖片的位置肯定是對的,所以我們是不需要計算的
- 但是下一張圖片的位置就需要我們去挪動它的位置,所以這里我們需要計算這個圖片需要偏移的距離
- 每一個圖片移動一格的距離就是等于它自身的長度,加上往左移動是負數(shù),所以每往左邊移動一個格就是 -100%
- 圖片的 index 是從 0 到 n 的,如果我們用它們所在的 index 作為它們距離當前圖片相差的圖片數(shù),我們就可以用
index * -100%
,這樣就可以把每一張圖片移動到當前圖片的位置。 - 但是我們需要的是先把圖片移動到當前圖片的下一位的位置,所以下一位的所在位置是 index - 1 的圖片距離,也就是說我們要移動的距離是
(index - 1) * -100%
- 讓第二張圖就位的這個動作,我們不需要它出現(xiàn)任何動畫效果,所以在這個過程中我們需要禁止圖片的動畫效果,那就要清楚 transition
第二張圖就位,就可以開始執(zhí)行輪播效果
- 因為上面我們需要至少一幀的圖片移動時間,所以執(zhí)行輪播效果之前需要一個 16 毫秒的延遲 (因為 16 毫秒剛好是瀏覽器一幀的時間)
- 首先把行內標簽中的
transition
重新開啟,這樣我們 CSS 中的動效就會重新起效,因為接下來的輪播效果是需要有動畫效果的 - 第一步是先把當前圖片往右邊移動一步,之前我們說的 index * -100% 讓任何一張在 index 位置的圖片移動到當前位置的公式,那么要再往右邊移動多一個位置,那就是
(index + 1) * -100%
即可 - 第二步就是讓下一張圖移動到當前顯示的位置,這個就是直接用 index * -100% 咯
- 最后我們還需要更新一次我們記錄,
currentIndex = nextIndex
,這樣就大功告成了!
接下來我們把上面的邏輯翻譯成 JavaScript:
class Carousel extends Component { // 構造函數(shù) // 創(chuàng)建 DOM 節(jié)點 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; // 當前圖片的節(jié)點 let current = children[currentIndex]; // 下一張圖片的節(jié)點 let next = children[nextIndex]; // 禁用圖片的動效 next.style.transition = 'none'; // 移動下一張圖片到正確的位置 next.style.transform = `translateX(${-100 * (nextIndex - 1)}%)`; // 執(zhí)行輪播效果,延遲了一幀的時間 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
的話,我們就可以很清晰的看到所有圖片移動的軌跡了:
實現(xiàn)拖拽輪播
一般來說我們的輪播組件除了這種自動輪播的功能之外,還有可以使用我們的鼠標進行拖動來輪播。所以接下來我們一起來實現(xiàn)這個手動輪播功能。
因為自動輪播和手動輪播是有一定的沖突的,所以我們需要把我們前面實現(xiàn)的自動輪播的代碼給注釋掉。然后我們就可以使用這個輪播組件下的 children (子元素),也就是所有圖片的元素,來實現(xiàn)我們的手動拖拽輪播功能。
那么拖拽的功能主要就是涉及我們的圖片被拖動,所以我們需要給圖片加入鼠標的監(jiān)聽事件。如果我們根據(jù)操作步驟來想的話,就可以整理出這么一套邏輯:
我們肯定是需要先把鼠標移動到圖片之上,然后點擊圖片。所以我們第一個需要監(jiān)聽的事件必然就是 mousedown
鼠標按下事件。點擊了鼠標之后,那么我們就會開始移動我們的鼠標,讓我們的圖片跟隨我們鼠標移動的方向去走。這個時候我們就要監(jiān)聽 mousemove
鼠標移動事件。當我們把圖片拖動到我們想要的位置之后,我們就會松開我們鼠標的按鍵,這個時候也是我們要計算這個圖片是否可以輪播的時候,這個就需要我們監(jiān)聽 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'); });
執(zhí)行一下以上代碼后,我們就會在 console 中看到,當我們鼠標放到圖片上并且移動時,我們會不斷的觸發(fā) mousemove
。但是我們想要的效果是,當我們鼠標按住時移動才會觸發(fā) mousemove
,我們鼠標單純在圖片上移動是不應該觸發(fā)事件的。
所以我們需要把 mousemove 和 mouseup 兩個事件,放在 mousedown 事件的回調函數(shù)當中,這樣才能正確的在鼠標按住的時候監(jiān)聽移動和松開兩個動作。這里還需要考慮,當我們 mouseup 的時候,我們需要把 mousemove 和 mouseup 兩個監(jiān)聽事件給停掉,所以我們需要用函數(shù)把它們單獨的存起來。
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 的事件給移除了。這個就是一般我們在做拖拽的時候都會用到的基礎代碼。
但是我們又會發(fā)現(xiàn)另外一個問題,鼠標點擊拖動然后松開后,我們鼠標再次在圖片上移動,還是會出發(fā)到我們的mousemove 事件。
這個是因為我們的 mousemove 是在 root
上被監(jiān)聽的。其實我們的 mousedown 已經(jīng)是在 root
上監(jiān)聽,我們 mousemove 和 mouseup 就沒有必要在 root
上監(jiān)聽了。
所以我們可以在 document
上直接監(jiān)聽這兩個事件,而在現(xiàn)代瀏覽器當中,使用 document
監(jiān)聽還有額外的好處,即使我們的鼠標移出瀏覽器窗口外我們一樣可以監(jiān)聽到事件。
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); });
有了這個完整的監(jiān)聽機制之后,我們就可以嘗試在 mousemove 里面去實現(xiàn)輪播圖的移動功能了。我們一起來整理一下這個功能的邏輯:
要做這個功能,首先我們要知道鼠標的位置,這里可以使用 mousemove 中的 event
參數(shù)去捕獲到鼠標的坐標。event
上其實有很多個鼠標的坐標,比如 offsetX
、offsetY
等等,這些都是根據(jù)不同的參考系所獲得坐標的。在這里我們比較推薦使用的是 clientX
和 clientY
這個坐標是相對于整個瀏覽器中可渲染區(qū)域的坐標,它不受任何的因素影響。很多時候我們組件在瀏覽器這個容器里面,當我們滾動了頁面之后,在一些坐標體系中就會發(fā)生變化。這樣我們就很容易會出現(xiàn)一些不可調和的 bug,但是 clientX 和 clientY 就不會出現(xiàn)這種問題。如果要知道我們圖片要往某一個方向移動多少,我們就要知道我們鼠標點擊時的起始坐標,然后與我們獲取到的 clientX 和 clientY 做對比。所以我們需要記錄一個 startX
和 startY
,它們的默認值就是對應的當前 clientX 和 clientY所以我們鼠標移動的距離就是 終 點 坐 標 − 起 點 坐 標 終點坐標 - 起點坐標 終點坐標−起點坐標,在我們的 move 回調函數(shù)里面就是 clientX - startX
和 clientY - startY
我們輪播圖只支持左右滑動的,所以在我們這個場景中,就不需要 Y 軸的值。那么我們計算好移動距離,就可以給對應被拖動的元素加上 transform,這樣圖片就會被移動了我們之前做自動輪播的時候給圖片元素加入了 transition 動畫,我們在拖動的時候如果有這個動畫,就會出現(xiàn)延遲一樣的效果,所以在給圖片加入 transform 的同時,我們還需要禁用它們的 transition 屬性
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); });
好,到了這里我們發(fā)現(xiàn)了兩個問題:
我們第一次點擊然后拖動的時候圖片的起始位置是對的,但是我們再點擊的時候圖片的位置就不對了。我們拖動了圖片之后,當我們松開鼠標按鈕,這個圖片就會停留在拖動結束的位置了,但是在正常的輪播圖組件中,我們如果拖動了圖片超過一定的位置,就會自動輪播到下一張圖的。
要解決這兩個問題,我們可以這么計算,因為我們做的是一個輪播圖的組件,按照現(xiàn)在一般的輪播組件來說,當我們把圖片拖動在大于半個圖的位置時,就會輪播到下一張圖了,如果不到一半的位置的話就會回到當前拖動的圖的位置。
按照這樣的一個需求,我們就需要記錄一個 position
,它記錄了當前是第幾個圖片(從 0 開始計算)。如果我們每張圖片都是 500px 寬,那么第一張圖的 current 就是 0,偏移的距離就是 0 * 500 = 0, 而第二張圖就是 1 * 500 px,第三張圖就是 2 * 500px,以此類推。根據(jù)這樣的規(guī)律,第 N 張圖的偏移位置就是 n ∗ 500 n * 500 n∗500。
首先當我們 mousemove 的時候,我們需要計算當前圖片已經(jīng)從起點移動了多遠,這個就可以通過 N * 500 來計算,這里的 N 就是目前的圖片的 position
值。然后我們還需要在 mouseup 的時候,計算一下當前圖片移動的距離是否有超過半張圖的長度,如果超過了,我們直接 transform 到下一張圖的起點位置這里的超出判斷可以使用我們當前鼠標移動的距離 x
除與我們每張圖的 長度
(我們這個組件控制了圖片是 500px,所以我們就用 x 除與 500),這樣我們就會得出一個 0 到 1 的數(shù)字。如果這個數(shù)字等于或超過 0.5 那么就是過了圖一半的長度了,就可以直接輪播到下一張圖,如果是小于 0.5 就可以移動回去當前圖的起始位置。上面計算出來的值,還可以結合我們的 position
,如果大于等于 0.5 就可以四舍五入變成 1, 否則就是 0。這里的 1 代表我們可以把 position
+ 1,如果是 0 那么 position
就不會變。這樣直接改變 current 的值,在 transform 的時候就會自動按照新的 current 值做計算,輪播的效果就達成了。因為 x
是可以左右移動的距離值,也就是說如果我們鼠標是往左移動的話,x
就會是負數(shù),而相反就是正數(shù),我們的輪播組件鼠標往左拖動就是前進,而往右拖動就是回退。所以這里運算這個 超出值
的時候就是 position = position - Math.round(x/500)
。比如我們鼠標往左邊挪動了 400px,當前 current 值是 0,那么position = 0 - Math.round(400/500) = 0 - -1 = 0 + 1 = 1
所以最后我們的 current 變成了 1
。根據(jù)上面的邏輯,我們在 mouseup 的事件中要循環(huán)所有輪播中的 child 圖片,給它們都設置一個新的 tranform 值
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()
。這樣我們的組件是可以隨著使用者去改變的。
做到這里,我們就可以用拖拽來輪播我們的圖片了,但是當我們拖到最后一張圖的時候,我們就會發(fā)現(xiàn)最后一張圖之后就是空白了,第一張圖沒有接著最后一張。
那么接下來我們就去完善這個功能。這里其實和我們的自動輪播是非常相似的,在做自動輪播的時候我們就知道,每次輪播圖片的時候,我們最多就只能看到兩張圖片,可以看到三張圖片的機率是非常小的,因為我們的輪播的寬度相對我們的頁面來說是非常小的,除非用戶有足夠的位置去拖到第二張圖以外才會出現(xiàn)這個問題。但是這里我們就不考慮這種因素了。
我們確定每次拖拽的時候只會看到兩張圖片,所以我們也可以像自動輪播那樣去處理拖拽的輪播。但是這里有一個點是不一樣的,我們自動輪播的時候,圖片只會走一個方向,要么左要么右邊。但是我們手動就可以往左或者往右拖動,圖片是可以走任意方向的。所以我們就無法直接用自動輪播的代碼來實現(xiàn)這個功能了。我們就需要自己重新處理一下輪播頭和尾無限循環(huán)的邏輯。
我們可以從 mousemove 的回調函數(shù)開始改造需要找到當前元素在屏幕上的位置,我們給它 一個變量名叫 current
,它的值與我們之前在 mouseup 計算的 position 是一樣的 position + Math.round(x/500)
但是當前這個元素是前后都有一張圖,這里我們就不去計算現(xiàn)在拖動是需要拼接它前面還是后面的圖,我們直接就把當前元素前后兩個圖都移動到對應的位置即可這里我們直接循環(huán)一個 [-1, 0, 1]
的數(shù)組,對應的是前一個元素
,當前元素
和下一個元素
,這里我們需要使用這三個偏移值,獲取到上一個圖片,當前拖動的圖片和下一個圖片的移動位置,這三個位置是跟隨著我們鼠標的拖動實時計算的接著我們在這個循環(huán)里面需要先計算出前后兩張圖的位置,圖片位置 = 當前圖片位置 + 偏移
,這里可以這么理解如果當前圖片是在 2 這個位置,上一張圖就是在 1,下一張圖就在 3但是這里有一個問題,如果我們當前圖是在 0 的位置,我們上一張圖獲取到的位置就是 -1
,按照我們圖片的數(shù)據(jù)結構來說,數(shù)組里面是沒有 -1
這個位置的。所以當我們遇到計算出來的位置是負數(shù)的時候我們就要把它轉成這一列圖片的最后一張圖的位置。按照我們的例子里面的圖片數(shù)據(jù)來說的話,當前的圖是在 0 這個位置,那么上一張圖就應該是我們在3 號位的圖。那么我們怎么能把 -1 變成 3, 在結尾的時候 4 變成 0 呢?這里需要用到一個數(shù)學中的小技巧了,如果我們想讓頭尾的兩個值超出的時候可以翻轉,我們就需要用到一個公式, 求 (當前指針 + 數(shù)組總長度)/ 數(shù)組總長度
的 余數(shù)
,這個獲得的余數(shù)就正好是翻轉的。
我們來證明一下這個公式是正確的,首先如果我們遇到 current = 0, 那么 0 這個位置的圖片的上一張就會獲得 -1 這個指針,這個時候我們用 ( − 1 + 4 ) / 4 = 3 / 4 (-1 + 4) / 4 = 3 / 4 (−1+4)/4=3/4,這里 3 除以 4 的余數(shù)就是 3,而
3
剛好就是這個數(shù)組的最后一個圖片。
然后我們來試試,如果當前圖片就是數(shù)組里面的最后一張圖,在我們的例子里面就是 3,3 + 1 = 4, 這個時候通過轉換 ( 4 + 4 ) / 4 (4 + 4) / 4 (4+4)/4 余數(shù)就是
0
,顯然我們獲得的數(shù)字就是數(shù)組的第一個圖片的位置。
通過這個公式我們就可以取得上一張和下一張圖片在數(shù)組里面的指針位置,這個時候我們就可以用這個指針獲取到他們在節(jié)點中的對象,使用 CSSDOM 來改變他們的屬性這里我們需要先把所有元素移動到當前圖片的位置,然后根據(jù) -1、0、1 這三個偏移的值對這個圖片進行往左或者往右移動,最后我們要需要加上當前鼠標的拖動距離
我們已經(jīng)把整個邏輯給整理了一遍,下來我們看看 mousemove 這個事件回調函數(shù)代碼的應該怎么寫:
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)`; } };
講了那么多東西,代碼就那么幾行,確實代碼簡單不等于它背后的邏輯就簡單。所以寫代碼的程序員也可以是深不可測的。
最后還有一個小問題,在我們拖拽的時候,我們會發(fā)現(xiàn)上一張圖和下一張有一個奇怪跳動的現(xiàn)象。
這個問題是我們的 Math.round(x / 500)
所導致的,因為我們在 transform 的時候,加入了 x % 500
, 而在我們的 current 值的計算中沒有包含這一部分的計算,所以在鼠標拖動的時候就會缺少這部分的偏移度。
我們只需要把這里的 Math.round(x / 500)
改為 (x - x % 500) / 500
即可達到同樣的取整數(shù)的效果,同時還可以保留我們 x
原有的正負值。
這里其實還有比較多的問題的,我們還沒有去改 mouseup 事件里面的邏輯。那么接下來我們就來看看 up 中的邏輯我們應該怎么去實現(xiàn)。
這里我們需要改的就是 children 中 for 循環(huán)的代碼,我們要實現(xiàn)的是讓我們拖動圖片超過一定的位置就會自動輪播到對應方向的下一張圖片。up 這里的邏輯其實是和 move 是基本一樣的,不過這里有幾個地方需要更改的:
首先我們的 transition 禁止是可以去掉了,改為 ' '
空在 transform 中的 + x % 500
就不需要了,因為這里圖片是我們鼠標松開的時候,不需要圖片再跟隨我們鼠標的位置了在計算 pos = current + offset
的這里,我們在 up 的回調中是沒有 current 的,所以我們需要把 current 改為 position因為有一個 z-index 的層次關系,我們會看到有圖片在被挪動位置的時候,它在我們當前圖片上飛過,但是飛過去的元素其實是我們不需要的元素,而這個飛過去的元素是來源于我們之前用的 [-1, 0, 1] 這里面的 -1 和 1 的兩個元素,所以在 up 這個邏輯里面我們要把不需要的給去掉。意思就是說,如果我們鼠標是往左移動的,那么我們只需要 -1 的元素,相反就是只需要 1 的元素,另外的那邊的元素就可以去掉了。首先 for of
循環(huán)是沒有順序要求的,所以我們可以把 -1 和 1 這兩個數(shù)字用一個公式來代替,放在我們 0 的后面。但是怎么才能找到我們需要的是哪一邊呢?其實我們需要計算的就是圖片在移動的方向,所以我們要改動的就是 position = position - Math.round(x / 500)
這行代碼,這個方向可以通過 Math.round(x / 500) - x
獲得。而這個值就是相對當前元素的中間,他是更偏向左邊(負數(shù))還是右邊(正數(shù)),其實這個數(shù)字是多少并不是最重要的,我們要的是它的符號也就是 -1 還是 1,所以這里我們就可以使用 - Math.sign(Math.round(x / 500) - x)
來取得結果中的符號,這個函數(shù)最終返回要不就是 -1, 要不就是 1 了, 正好是我們想要的。其實還有一個小 bug,當我們拖動當前圖片過短的時候,圖片位置的計算是不正確的。
這個是因為我們的 Match.round() 的特性,在 250(500px 剛好一半的位置) 之間是有一定的誤區(qū),讓我們無法判斷圖片需要往那個方向移動的,所以在計算往 Match.round 的值之后我們還需要加上 + 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 函數(shù)之后,我們就真正完成了這個手動輪播的組件了。
到此這篇關于使用JSX實現(xiàn)Carousel輪播組件的方法(前端組件化)的文章就介紹到這了,更多相關JSX實現(xiàn)Carousel輪播組件內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
JavaScript SHA1加密算法實現(xiàn)詳細代碼
這篇文章主要為大家詳細介紹了JavaScript SHA1加密算法實現(xiàn)代碼,具有一定的參考價值,感興趣的朋友可以參考一下2016-10-10javascript學習筆記(七)利用javascript來創(chuàng)建和存儲cookie
今天把javascript如何用來創(chuàng)建及存儲cookie復習了一下,其中的一點體會拿出來和大家討論,懇請高手指點一二。2011-04-04JavaScript實現(xiàn)算術平方根算法-代碼超簡單
實現(xiàn)算術平方根的方法有很多種,本文是通過JavaScript實現(xiàn)的算術平方根算法,代碼超簡單,超管用,感興趣的朋友跟著腳本之家的小編一起學習吧2015-09-09基于JS實現(xiàn)密碼框(password)中顯示文字提示功能代碼
這篇文章主要介紹了實現(xiàn)密碼框(password)中顯示文字提示功能代碼,在項目開發(fā)中經(jīng)常會用到,需要的朋友可以參考下2016-05-05JavaScript實現(xiàn)動態(tài)添加、移除元素或屬性的方法分析
這篇文章主要介紹了JavaScript實現(xiàn)動態(tài)添加、移除元素的方法,結合實例形式分析了javascript針對頁面元素動態(tài)添加、移除、設置等相關函數(shù)與使用技巧,需要的朋友可以參考下2019-01-01