在我們用 JSX 建立組件系統(tǒng)之前,我們先來(lái)用一個(gè)例子學(xué)習(xí)一下組件的實(shí)現(xiàn)原理和邏輯。這里我們就用一個(gè)輪播圖的組件作為例子進(jìn)行學(xué)習(xí)。輪播圖的英文叫做 Carousel,它有一個(gè)旋轉(zhuǎn)木馬的意思。
上一篇文章《使用 JSX 建立 Markup 組件風(fēng)格》中我們實(shí)現(xiàn)的代碼,其實(shí)還不能稱為一個(gè)組件系統(tǒng),頂多是可以充當(dāng) DOM 的一個(gè)簡(jiǎn)單封裝,讓我們有能力定制 DOM。
要做這個(gè)輪播圖的組件,我們應(yīng)該先從一個(gè)最簡(jiǎn)單的 DOM 操作入手。使用 DOM 操作把整個(gè)輪播圖的功能先實(shí)現(xiàn)出來(lái),然后在一步一步去考慮怎么把它設(shè)計(jì)成一個(gè)組件系統(tǒng)。
TIPS:在開(kāi)發(fā)中我們往往一開(kāi)始做一個(gè)組件的時(shí)候,都會(huì)過(guò)度思考一個(gè)功能應(yīng)該怎么設(shè)計(jì),然后就把它實(shí)現(xiàn)的非常復(fù)雜。其實(shí)更好的方式是反過(guò)來(lái)的,先把功能實(shí)現(xiàn)了,然后通過(guò)分析這個(gè)功能從而設(shè)計(jì)出一個(gè)組件架構(gòu)體系。
因?yàn)槭禽啿D,那我們當(dāng)然需要用到圖片,所以這里我準(zhǔn)備了 4 張來(lái)源于 Unsplash 的開(kāi)源圖片,當(dāng)然大家也可以換成自己的圖片。首先我們把這 4 張圖片都放入一個(gè) gallery
的變量當(dāng)中:
- 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',
- ];
而我們的目標(biāo)就是讓這 4 張圖可以輪播起來(lái)。
組件底層封裝
首先我們需要給我們之前寫的代碼做一下封裝,便于我們開(kāi)始編寫這個(gè)組件。
- 根目錄建立 framework.js
-
把
createElement
、ElementWrapper
、TextWrapper
這三個(gè)移到我們的 framework.js 文件中 -
然后
createElement
方法是需要 export 出去讓我們可以引入這個(gè)基礎(chǔ)創(chuàng)建元素的方法。 -
ElementWrapper
、TextWrapper
是不需要 export 的,因?yàn)樗鼈兌紝儆趦?nèi)部給 createElement 使用的 - 封裝 Wrapper 類中公共部分
-
ElementWrapper
、TextWrapper
之中都有一樣的setAttribute
、appendChild
和mountTo
,這些都是重復(fù)并且可公用的 -
所以我們可以建立一個(gè)
Component
類,把這三個(gè)方法封裝進(jìn)入 -
然后讓
ElementWrapper
和TextWrapper
繼承Component
- Component 加入 render() 方法
- 在 Component 類中加入 構(gòu)造函數(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 就是一個(gè)節(jié)點(diǎn)
- // 所以我們可以直接返回
- return element;
- }
- export class Component {
- constructor() {
- }
- // 掛載元素的屬性
- setAttribute(name, attribute) {
- this.root.setAttribute(name, attribute);
- }
- // 掛載元素子元素
- appendChild(child) {
- child.mountTo(this.root);
- }
- // 掛載當(dāng)前元素
- mountTo(parent) {
- parent.appendChild(this.root);
- }
- }
- class ElementWrapper extends Component {
- // 構(gòu)造函數(shù)
- // 創(chuàng)建 DOM 節(jié)點(diǎn)
- constructor(type) {
- this.root = document.createElement(type);
- }
- }
- class TextWrapper extends Component {
- // 構(gòu)造函數(shù)
- // 創(chuàng)建 DOM 節(jié)點(diǎn)
- constructor(content) {
- this.root = document.createTextNode(content);
- }
- }
實(shí)現(xiàn) Carousel
接下來(lái)我們就要繼續(xù)改造我們的 main.js
。首先我們需要把 Div 改為 Carousel 并且讓它繼承我們寫好的 Component 父類,這樣我們就可以省略重復(fù)實(shí)現(xiàn)一些方法。
繼承了 Component后,我們就要從 framework.js
中 import 我們的 Component。
這里我們就可以正式開(kāi)始開(kāi)發(fā)組件了,但是如果每次都需要手動(dòng) webpack 打包一下,就特別的麻煩。所以為了讓我們可以更方便的調(diào)試代碼,這里我們就一起來(lái)安裝一下 webpack dev server 來(lái)解決這個(gè)問(wèn)題。
執(zhí)行一下代碼,安裝 webpack-dev-server
:
- npm install --save-dev webpack-dev-server webpack-cli
看到上面這個(gè)結(jié)果,就證明我們安裝成功了。我們最好也配置一下我們 webpack 服務(wù)器的運(yùn)行文件夾,這里我們就用我們打包出來(lái)的 dist
作為我們的運(yùn)行目錄。
設(shè)置這個(gè)我們需要打開(kāi)我們的 webpack.config.js
,然后加入 devServer
的參數(shù), contentBase
給予 ./dist
這個(gè)路徑。
- 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' }]],
- },
- },
- },
- ],
- },
- };
用過(guò) Vue 或者 React 的同學(xué)都知道,啟動(dòng)一個(gè)本地調(diào)試環(huán)境服務(wù)器,只需要執(zhí)行 npm 命令就可以了。這里我們也設(shè)置一個(gè)快捷啟動(dòng)命令。打開(kāi)我們的 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í)行下面這個(gè)命令啟動(dòng)我們的本地調(diào)試服務(wù)器啦!
- npm start
開(kāi)啟了這個(gè)之后,當(dāng)我們修改任何文件時(shí)都會(huì)被監(jiān)聽(tīng)到,這樣就會(huì)實(shí)時(shí)給我們打包文件,非常方便我們調(diào)試。看到上圖里面表示,我們的實(shí)時(shí)本地服務(wù)器地址就是 http://localhost:8080
。我們?cè)跒g覽器直接打開(kāi)這個(gè)地址就可以訪問(wèn)這個(gè)項(xiàng)目。
這里要注意的一個(gè)點(diǎn),我們把運(yùn)行的目錄改為了 dist,因?yàn)槲覀冎暗?main.html 是放在根目錄的,這樣我們就在 localhost:8080 上就找不到這個(gè) HTML 文件了,所以我們需要把 main.html 移動(dòng)到 dist 目錄下,并且改一下 main.js 的引入路徑。
- <!-- main.html 代碼 -->
- <body></body>
- <script src="./main.js"></script>
打開(kāi)鏈接后我們發(fā)現(xiàn) Carousel 組件已經(jīng)被掛載成功了,這個(gè)證明我們的代碼封裝是沒(méi)有問(wèn)題的。
接下來(lái)我們繼續(xù)來(lái)實(shí)現(xiàn)我們的輪播圖功能,首先要把我們的圖片數(shù)據(jù)傳進(jìn)去我們的 Carousel 組件里面。
- let a = <Carousel src={gallery}/>;
這樣我們的 gallery
數(shù)組就會(huì)被設(shè)置到我們的 src
屬性上。但是我們的這個(gè) src
屬性不是給我們的 Carousel 自身的元素使用的。也就說(shuō)我們不是像之前那樣直接掛載到 this.root
上。
所以我們需要另外儲(chǔ)存這個(gè) src 上的數(shù)據(jù),后面使用它來(lái)生成我們輪播圖的圖片展示元素。在 React 里面是用 props
來(lái)儲(chǔ)存元素屬性,但是這里我們就用一個(gè)更加接近屬性意思的 attributes
來(lái)儲(chǔ)存。
因?yàn)槲覀冃枰獌?chǔ)存進(jìn)來(lái)的屬性到 this.attributes
這個(gè)變量中,所以我們需要在 Component 類的 constructor
中先初始化這個(gè)類屬性。
然后這個(gè) attributes 是需要我們另外存儲(chǔ)到類屬性中,而不是掛載到我們?cè)毓?jié)點(diǎn)上。所以我們需要在組件類中重新定義我們的 setAttribute
方法。
我們需要在組件渲染之前能拿到 src 屬性的值,所以我們需要把 render 的觸發(fā)放在 mountTo
之內(nèi)。
- class Carousel extends Component {
- // 構(gòu)造函數(shù)
- // 創(chuàng)建 DOM 節(jié)點(diǎn)
- 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());
- }
- }
接下來(lái)我們看看實(shí)際運(yùn)行的結(jié)果,看看是不是能夠獲得圖片的數(shù)據(jù)。
接下來(lái)我們就去把這些圖給顯示出來(lái)。這里我們需要改造一下 render 方法,在這里加入渲染圖片的邏輯:
- 首先我們需要把創(chuàng)建的新元素儲(chǔ)起來(lái)
- 循環(huán)我們的圖片數(shù)據(jù),給每條數(shù)據(jù)創(chuàng)建一個(gè) img 元素
- 給每一個(gè) img 元素附上 src = 圖片 url
-
把附上 src 屬性的圖片元素掛載到我們的組件元素
this.root
上 -
最后讓 render 方法返回
this.root
- class Carousel extends Component {
- // 構(gòu)造函數(shù)
- // 創(chuàng)建 DOM 節(jié)點(diǎn)
- 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());
- }
- }
就這樣我們就可以看到我們的圖片被正確的顯示在我們的頁(yè)面上。
排版與動(dòng)畫(huà)
首先我們圖片的元素都是 img 標(biāo)簽,但是使用這個(gè)標(biāo)簽的話,當(dāng)我們點(diǎn)擊并且拖動(dòng)的時(shí)候它自帶就是可以被拖拽的。當(dāng)然這個(gè)也是可以解決的,但是為了更簡(jiǎn)單的解決這個(gè)問(wèn)題,我們就把 img 換成 div,然后使用 background-image。
默認(rèn) div 是沒(méi)有寬高的,所以我們需要在組件的 div 這一層加一個(gè) class 叫 carousel
,然后在 HTML 中加入 css 樣式表,直接選擇 carousel 下的每一個(gè) div,然后給他們合適的樣式。
- // main.js
- class Carousel extends Component {
- // 構(gòu)造函數(shù)
- // 創(chuàng)建 DOM 節(jié)點(diǎn)
- 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,但是如果我們?cè)O(shè)置一個(gè)高是 300px,我們會(huì)發(fā)現(xiàn)圖片的底部出現(xiàn)了一個(gè)圖片重復(fù)的現(xiàn)象。這是因?yàn)閳D片的比例是 1600 x 900
,而 500 x 300
比例與圖片原來(lái)的比例不一致。
所以通過(guò)比例計(jì)算,我們可以得出這樣一個(gè)高度: 500 ÷ 1900 × 900 = 281. x x x 500\div1900\times900 = 281.xxx 500÷1900×900=281.xxx。所以 500px 寬對(duì)應(yīng)比例的高大概就是 281px。這樣我們的圖片就可以正常的顯示在一個(gè) div 里面了。
一個(gè)輪播圖顯然不可能所有的圖片都顯示出來(lái)的,我們認(rèn)知中的輪播圖都是一張一張圖片顯示的。首先我們需要讓圖片外層的 carousel div 元素有一個(gè)和它們一樣寬高的盒子,然后我們?cè)O(shè)置 overflow: hidden
。這樣其他圖片就會(huì)超出盒子所以被隱藏了。
這里有些同學(xué)可能問(wèn):“為什么不把其他圖片改為 display: hidden 或者 opacity:0 呢?” 因?yàn)槲覀兊妮啿D在輪播的時(shí)候,實(shí)際上是可以看到當(dāng)前的圖片和下一張圖片的。所以如果我們用了 display: hidden 這種隱藏屬性,我們后面的效果就不好做了。
然后我們又有一個(gè)問(wèn)題,輪播圖一般來(lái)說(shuō)都是左右滑動(dòng)的,很少見(jiàn)是上下滑動(dòng)的,但是我們這里圖片就是默認(rèn)從上往下排布的。所以這里我們需要調(diào)整圖片的布局,讓它們拍成一行。
這里我們使用正常流就可以了,所以只需要給 div 加上一個(gè) display: inline-block
,就可以讓它們排列成一行,但是只有這個(gè)屬性的話,如果圖片超出了窗口寬度就會(huì)自動(dòng)換行,所以我們還需要在它們父級(jí)加入強(qiáng)制不換行的屬性 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>
接下來(lái)我們來(lái)實(shí)現(xiàn)自動(dòng)輪播效果,在做這個(gè)之前我們先給這些圖片元素加上一些動(dòng)畫(huà)屬性。這里我們用 transition
來(lái)控制元素動(dòng)效的時(shí)間,一般來(lái)說(shuō)我們播一幀會(huì)用 0.5
秒 的 ease
。
Transition 一般來(lái)說(shuō)都只用 ease 這個(gè)屬性,除非是一些非常特殊的情況,ease-in 會(huì)用在推出動(dòng)畫(huà)當(dāng)中,而 ease-out 就會(huì)用在進(jìn)入動(dòng)畫(huà)當(dāng)中。在同一屏幕上的,我們一般默認(rèn)都會(huì)使用 ease,但是 linear 在大部分情況下我們是永遠(yuǎn)不會(huì)去用的。因?yàn)?ease 是最符合人類的感覺(jué)的一種運(yùn)動(dòng)曲線。
- <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>
實(shí)現(xiàn)自動(dòng)輪播
有了動(dòng)畫(huà)效果屬性,我們就可以在 JavaScript 中加入我們的定時(shí)器,讓我們的圖片在每三秒鐘切換一次圖片。我們使用 setInerval()
這個(gè)函數(shù)就可以解決這個(gè)問(wèn)題了。
但是我們?cè)趺床拍茏寛D片輪播,或者移動(dòng)呢?想到 HTML 中的移動(dòng),大家有沒(méi)有想到 CSS 當(dāng)中有什么屬性可以讓我們移動(dòng)元素的呢?
對(duì)沒(méi)錯(cuò),就是使用 transform
,它就是在 CSS 當(dāng)中專門用于挪動(dòng)元素的。所以這里我們的邏輯就是,每 3 秒往左邊挪動(dòng)一次元素自身的長(zhǎng)度,這樣我們就可以挪動(dòng)到下一張圖的開(kāi)始。
但是這樣只能挪動(dòng)一張圖,所以如果我們需要挪動(dòng)第二次,到達(dá)第三張圖,我們就要讓每一張圖偏移 200%,以此類推。所以我們需要一個(gè)當(dāng)前頁(yè)數(shù)的值,叫做 current
,默認(rèn)值為 0。每次挪動(dòng)的時(shí)候時(shí)就加一,這樣偏移的值就是 − 100 × 頁(yè) 數(shù) -100\times頁(yè)數(shù) −100×頁(yè)數(shù)。這樣我們就完成了圖片多次移動(dòng),一張一張圖片展示了。
- class Carousel extends Component {
- // 構(gòu)造函數(shù)
- // 創(chuàng)建 DOM 節(jié)點(diǎn)
- 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)一個(gè)問(wèn)題,這個(gè)輪播是不會(huì)停止的,一直往左偏移沒(méi)有停止。而我們需要輪播到最后一張的時(shí)候是回到一張圖的。
要解決這個(gè)問(wèn)題,我們可以利用一個(gè)數(shù)學(xué)的技巧,如果我們想要一個(gè)數(shù)是在 1 到 N 之間不斷循環(huán),我們就讓它對(duì) n 取余就可以了。在我們?cè)刂校琧hildren 的長(zhǎng)度是 4,所以當(dāng)我們 current 到達(dá) 4 的時(shí)候, 4 ÷ 4 4\div4 4÷4 的余數(shù)就是 0,所以每次把 current 設(shè)置成 current 除以 children 長(zhǎng)度的余數(shù)就可以達(dá)到無(wú)限循環(huán)了。
這里 current 就不會(huì)超過(guò) 4, 到達(dá) 4 之后就會(huì)回到 0。
用這個(gè)邏輯來(lái)實(shí)現(xiàn)我們的輪播,確實(shí)能讓我們的圖片無(wú)限循環(huán),但是如果我們運(yùn)行一下看看的話,我們又會(huì)發(fā)現(xiàn)另外一個(gè)問(wèn)題。當(dāng)我們播放到最后一個(gè)圖片之后,就會(huì)快速滑動(dòng)到第一個(gè)張圖片,我們會(huì)看到一個(gè)快速回退的效果。這個(gè)確實(shí)不是那么好,我們想要的效果是,到達(dá)最后一張圖之后,第一張圖就直接在后面接上。
那么我們就一起去嘗試解決這個(gè)問(wèn)題,經(jīng)過(guò)觀察其實(shí)在屏幕上一次最多就只能看到兩張圖片。那么其實(shí)我們就把這兩張圖片挪到正確的位置就可以了。
所以我們需要找到當(dāng)前看到的圖片,還有下一張圖片,然后每次移動(dòng)到下一張圖片就找到再下一張圖片,把下一張圖片挪動(dòng)到正確的位置。
講到這里可能還是有點(diǎn)懵,但是不要緊,我們來(lái)整理一下邏輯。
獲取當(dāng)前圖片 index 和 下一張圖的 index
- 首先輪播肯定是從第一張圖開(kāi)始,而這張圖在我們的節(jié)點(diǎn)中肯定是第 0 個(gè)
- 因?yàn)槲覀冃枰诳吹揭粡垐D的時(shí)候就準(zhǔn)備第二張圖,所以我們就需要找到下一張圖的位置
- 根據(jù)我們上面說(shuō)的,下一張圖的位置,我們可以使用數(shù)學(xué)里的技巧來(lái)獲得: 下 一 張 圖 的 位 置 = ( 當(dāng) 前 位 置 + 1 ) ÷ 圖 片 數(shù) 量 下一張圖的位置 = (當(dāng)前位置 + 1)\div 圖片數(shù)量下一張圖的位置=(當(dāng)前位置+1)÷圖片數(shù)量 的余數(shù),根據(jù)這個(gè)公式,當(dāng)我們達(dá)到圖片最后一張的時(shí)候,就會(huì)返回 0,回到第一個(gè)圖片的位置
計(jì)算圖片移動(dòng)的距離,保持當(dāng)前圖片后面有一張圖片等著被挪動(dòng)過(guò)來(lái)
- 當(dāng)前顯示的圖片的位置肯定是對(duì)的,所以我們是不需要計(jì)算的
- 但是下一張圖片的位置就需要我們?nèi)ヅ矂?dòng)它的位置,所以這里我們需要計(jì)算這個(gè)圖片需要偏移的距離
- 每一個(gè)圖片移動(dòng)一格的距離就是等于它自身的長(zhǎng)度,加上往左移動(dòng)是負(fù)數(shù),所以每往左邊移動(dòng)一個(gè)格就是 -100%
-
圖片的 index 是從 0 到 n 的,如果我們用它們所在的 index 作為它們距離當(dāng)前圖片相差的圖片數(shù),我們就可以用
index * -100%
,這樣就可以把每一張圖片移動(dòng)到當(dāng)前圖片的位置。 -
但是我們需要的是先把圖片移動(dòng)到當(dāng)前圖片的下一位的位置,所以下一位的所在位置是 index - 1 的圖片距離,也就是說(shuō)我們要移動(dòng)的距離是
(index - 1) * -100%
- 讓第二張圖就位的這個(gè)動(dòng)作,我們不需要它出現(xiàn)任何動(dòng)畫(huà)效果,所以在這個(gè)過(guò)程中我們需要禁止圖片的動(dòng)畫(huà)效果,那就要清楚 transition
第二張圖就位,就可以開(kāi)始執(zhí)行輪播效果
- 因?yàn)樯厦嫖覀冃枰辽僖粠膱D片移動(dòng)時(shí)間,所以執(zhí)行輪播效果之前需要一個(gè) 16 毫秒的延遲 (因?yàn)?16 毫秒剛好是瀏覽器一幀的時(shí)間)
-
首先把行內(nèi)標(biāo)簽中的
transition
重新開(kāi)啟,這樣我們 CSS 中的動(dòng)效就會(huì)重新起效,因?yàn)榻酉聛?lái)的輪播效果是需要有動(dòng)畫(huà)效果的 -
第一步是先把當(dāng)前圖片往右邊移動(dòng)一步,之前我們說(shuō)的 index * -100% 讓任何一張?jiān)?index 位置的圖片移動(dòng)到當(dāng)前位置的公式,那么要再往右邊移動(dòng)多一個(gè)位置,那就是
(index + 1) * -100%
即可 - 第二步就是讓下一張圖移動(dòng)到當(dāng)前顯示的位置,這個(gè)就是直接用 index * -100% 咯
-
最后我們還需要更新一次我們記錄,
currentIndex = nextIndex
,這樣就大功告成了!
接下來(lái)我們把上面的邏輯翻譯成 JavaScript:
- class Carousel extends Component {
- // 構(gòu)造函數(shù)
- // 創(chuàng)建 DOM 節(jié)點(diǎn)
- 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);
- }
- // 當(dāng)前圖片的 index
- let currentIndex = 0;
- setInterval(() => {
- let children = this.root.children;
- // 下一張圖片的 index
- let nextIndex = (currentIndex + 1) % children.length;
- // 當(dāng)前圖片的節(jié)點(diǎn)
- let current = children[currentIndex];
- // 下一張圖片的節(jié)點(diǎn)
- let next = children[nextIndex];
- // 禁用圖片的動(dòng)效
- next.style.transition = 'none';
- // 移動(dòng)下一張圖片到正確的位置
- next.style.transform = `translateX(${-100 * (nextIndex - 1)}%)`;
- // 執(zhí)行輪播效果,延遲了一幀的時(shí)間 16 毫秒
- setTimeout(() => {
- // 啟用 CSS 中的動(dòng)效
- next.style.transition = '';
- // 先移動(dòng)當(dāng)前圖片離開(kāi)當(dāng)前位置
- current.style.transform = `translateX(${-100 * (currentIndex + 1)}%)`;
- // 移動(dòng)下一張圖片到當(dāng)前顯示的位置
- next.style.transform = `translateX(${-100 * nextIndex}%)`;
- // 最后更新當(dāng)前位置的 index
- currentIndex = nextIndex;
- }, 16);
- }, 3000);
- return this.root;
- }
- mountTo(parent) {
- parent.appendChild(this.render());
- }
- }
如果我們先去掉 overflow: hidden
的話,我們就可以很清晰的看到所有圖片移動(dòng)的軌跡了:
實(shí)現(xiàn)拖拽輪播
一般來(lái)說(shuō)我們的輪播組件除了這種自動(dòng)輪播的功能之外,還有可以使用我們的鼠標(biāo)進(jìn)行拖動(dòng)來(lái)輪播。所以接下來(lái)我們一起來(lái)實(shí)現(xiàn)這個(gè)手動(dòng)輪播功能。
因?yàn)樽詣?dòng)輪播和手動(dòng)輪播是有一定的沖突的,所以我們需要把我們前面實(shí)現(xiàn)的自動(dòng)輪播的代碼給注釋掉。然后我們就可以使用這個(gè)輪播組件下的 children (子元素),也就是所有圖片的元素,來(lái)實(shí)現(xiàn)我們的手動(dòng)拖拽輪播功能。
那么拖拽的功能主要就是涉及我們的圖片被拖動(dòng),所以我們需要給圖片加入鼠標(biāo)的監(jiān)聽(tīng)事件。如果我們根據(jù)操作步驟來(lái)想的話,就可以整理出這么一套邏輯:
我們肯定是需要先把鼠標(biāo)移動(dòng)到圖片之上,然后點(diǎn)擊圖片。所以我們第一個(gè)需要監(jiān)聽(tīng)的事件必然就是 mousedown
鼠標(biāo)按下事件。點(diǎn)擊了鼠標(biāo)之后,那么我們就會(huì)開(kāi)始移動(dòng)我們的鼠標(biāo),讓我們的圖片跟隨我們鼠標(biāo)移動(dòng)的方向去走。這個(gè)時(shí)候我們就要監(jiān)聽(tīng) mousemove
鼠標(biāo)移動(dòng)事件。當(dāng)我們把圖片拖動(dòng)到我們想要的位置之后,我們就會(huì)松開(kāi)我們鼠標(biāo)的按鍵,這個(gè)時(shí)候也是我們要計(jì)算這個(gè)圖片是否可以輪播的時(shí)候,這個(gè)就需要我們監(jiān)聽(tīng) mouseup
鼠標(biāo)松開(kāi)事件。
- 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í)行一下以上代碼后,我們就會(huì)在 console 中看到,當(dāng)我們鼠標(biāo)放到圖片上并且移動(dòng)時(shí),我們會(huì)不斷的觸發(fā) mousemove
。但是我們想要的效果是,當(dāng)我們鼠標(biāo)按住時(shí)移動(dòng)才會(huì)觸發(fā) mousemove
,我們鼠標(biāo)單純?cè)趫D片上移動(dòng)是不應(yīng)該觸發(fā)事件的。
所以我們需要把 mousemove 和 mouseup 兩個(gè)事件,放在 mousedown 事件的回調(diào)函數(shù)當(dāng)中,這樣才能正確的在鼠標(biāo)按住的時(shí)候監(jiān)聽(tīng)移動(dòng)和松開(kāi)兩個(gè)動(dòng)作。這里還需要考慮,當(dāng)我們 mouseup 的時(shí)候,我們需要把 mousemove 和 mouseup 兩個(gè)監(jiān)聽(tīng)事件給停掉,所以我們需要用函數(shù)把它們單獨(dú)的存起來(lái)。
- 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);
- });
這里我們?cè)?mouseup 的時(shí)候就把 mousemove 和 mouseup 的事件給移除了。這個(gè)就是一般我們?cè)谧鐾献У臅r(shí)候都會(huì)用到的基礎(chǔ)代碼。
但是我們又會(huì)發(fā)現(xiàn)另外一個(gè)問(wèn)題,鼠標(biāo)點(diǎn)擊拖動(dòng)然后松開(kāi)后,我們鼠標(biāo)再次在圖片上移動(dòng),還是會(huì)出發(fā)到我們的mousemove 事件。
這個(gè)是因?yàn)槲覀兊?mousemove 是在 root
上被監(jiān)聽(tīng)的。其實(shí)我們的 mousedown 已經(jīng)是在 root
上監(jiān)聽(tīng),我們 mousemove 和 mouseup 就沒(méi)有必要在 root
上監(jiān)聽(tīng)了。
所以我們可以在 document
上直接監(jiān)聽(tīng)這兩個(gè)事件,而在現(xiàn)代瀏覽器當(dāng)中,使用 document
監(jiān)聽(tīng)還有額外的好處,即使我們的鼠標(biāo)移出瀏覽器窗口外我們一樣可以監(jiān)聽(tīng)到事件。
- 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);
- });
有了這個(gè)完整的監(jiān)聽(tīng)機(jī)制之后,我們就可以嘗試在 mousemove 里面去實(shí)現(xiàn)輪播圖的移動(dòng)功能了。我們一起來(lái)整理一下這個(gè)功能的邏輯:
要做這個(gè)功能,首先我們要知道鼠標(biāo)的位置,這里可以使用 mousemove 中的 event
參數(shù)去捕獲到鼠標(biāo)的坐標(biāo)。event
上其實(shí)有很多個(gè)鼠標(biāo)的坐標(biāo),比如 offsetX
、offsetY
等等,這些都是根據(jù)不同的參考系所獲得坐標(biāo)的。在這里我們比較推薦使用的是 clientX
和 clientY
這個(gè)坐標(biāo)是相對(duì)于整個(gè)瀏覽器中可渲染區(qū)域的坐標(biāo),它不受任何的因素影響。很多時(shí)候我們組件在瀏覽器這個(gè)容器里面,當(dāng)我們滾動(dòng)了頁(yè)面之后,在一些坐標(biāo)體系中就會(huì)發(fā)生變化。這樣我們就很容易會(huì)出現(xiàn)一些不可調(diào)和的 bug,但是 clientX 和 clientY 就不會(huì)出現(xiàn)這種問(wèn)題。如果要知道我們圖片要往某一個(gè)方向移動(dòng)多少,我們就要知道我們鼠標(biāo)點(diǎn)擊時(shí)的起始坐標(biāo),然后與我們獲取到的 clientX 和 clientY 做對(duì)比。所以我們需要記錄一個(gè) startX
和 startY
,它們的默認(rèn)值就是對(duì)應(yīng)的當(dāng)前 clientX 和 clientY所以我們鼠標(biāo)移動(dòng)的距離就是 終 點(diǎn) 坐 標(biāo) − 起 點(diǎn) 坐 標(biāo) 終點(diǎn)坐標(biāo) - 起點(diǎn)坐標(biāo) 終點(diǎn)坐標(biāo)−起點(diǎn)坐標(biāo),在我們的 move 回調(diào)函數(shù)里面就是 clientX - startX
和 clientY - startY
我們輪播圖只支持左右滑動(dòng)的,所以在我們這個(gè)場(chǎng)景中,就不需要 Y 軸的值。那么我們計(jì)算好移動(dòng)距離,就可以給對(duì)應(yīng)被拖動(dòng)的元素加上 transform,這樣圖片就會(huì)被移動(dòng)了我們之前做自動(dòng)輪播的時(shí)候給圖片元素加入了 transition 動(dòng)畫(huà),我們?cè)谕蟿?dòng)的時(shí)候如果有這個(gè)動(dòng)畫(huà),就會(huì)出現(xiàn)延遲一樣的效果,所以在給圖片加入 transform 的同時(shí),我們還需要禁用它們的 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)了兩個(gè)問(wèn)題:
我們第一次點(diǎn)擊然后拖動(dòng)的時(shí)候圖片的起始位置是對(duì)的,但是我們?cè)冱c(diǎn)擊的時(shí)候圖片的位置就不對(duì)了。我們拖動(dòng)了圖片之后,當(dāng)我們松開(kāi)鼠標(biāo)按鈕,這個(gè)圖片就會(huì)停留在拖動(dòng)結(jié)束的位置了,但是在正常的輪播圖組件中,我們?nèi)绻蟿?dòng)了圖片超過(guò)一定的位置,就會(huì)自動(dòng)輪播到下一張圖的。
要解決這兩個(gè)問(wèn)題,我們可以這么計(jì)算,因?yàn)槲覀冏龅氖且粋€(gè)輪播圖的組件,按照現(xiàn)在一般的輪播組件來(lái)說(shuō),當(dāng)我們把圖片拖動(dòng)在大于半個(gè)圖的位置時(shí),就會(huì)輪播到下一張圖了,如果不到一半的位置的話就會(huì)回到當(dāng)前拖動(dòng)的圖的位置。
按照這樣的一個(gè)需求,我們就需要記錄一個(gè) position
,它記錄了當(dāng)前是第幾個(gè)圖片(從 0 開(kāi)始計(jì)算)。如果我們每張圖片都是 500px 寬,那么第一張圖的 current 就是 0,偏移的距離就是 0 * 500 = 0, 而第二張圖就是 1 * 500 px,第三張圖就是 2 * 500px,以此類推。根據(jù)這樣的規(guī)律,第 N 張圖的偏移位置就是 n ∗ 500 n * 500 n∗500。
首先當(dāng)我們 mousemove 的時(shí)候,我們需要計(jì)算當(dāng)前圖片已經(jīng)從起點(diǎn)移動(dòng)了多遠(yuǎn),這個(gè)就可以通過(guò) N * 500 來(lái)計(jì)算,這里的 N 就是目前的圖片的 position
值。然后我們還需要在 mouseup 的時(shí)候,計(jì)算一下當(dāng)前圖片移動(dòng)的距離是否有超過(guò)半張圖的長(zhǎng)度,如果超過(guò)了,我們直接 transform 到下一張圖的起點(diǎn)位置這里的超出判斷可以使用我們當(dāng)前鼠標(biāo)移動(dòng)的距離 x
除與我們每張圖的 長(zhǎng)度
(我們這個(gè)組件控制了圖片是 500px,所以我們就用 x 除與 500),這樣我們就會(huì)得出一個(gè) 0 到 1 的數(shù)字。如果這個(gè)數(shù)字等于或超過(guò) 0.5 那么就是過(guò)了圖一半的長(zhǎng)度了,就可以直接輪播到下一張圖,如果是小于 0.5 就可以移動(dòng)回去當(dāng)前圖的起始位置。上面計(jì)算出來(lái)的值,還可以結(jié)合我們的 position
,如果大于等于 0.5 就可以四舍五入變成 1, 否則就是 0。這里的 1 代表我們可以把 position
+ 1,如果是 0 那么 position
就不會(huì)變。這樣直接改變 current 的值,在 transform 的時(shí)候就會(huì)自動(dòng)按照新的 current 值做計(jì)算,輪播的效果就達(dá)成了。因?yàn)?x
是可以左右移動(dòng)的距離值,也就是說(shuō)如果我們鼠標(biāo)是往左移動(dòng)的話,x
就會(huì)是負(fù)數(shù),而相反就是正數(shù),我們的輪播組件鼠標(biāo)往左拖動(dòng)就是前進(jìn),而往右拖動(dòng)就是回退。所以這里運(yùn)算這個(gè) 超出值
的時(shí)候就是 position = position - Math.round(x/500)
。比如我們鼠標(biāo)往左邊挪動(dòng)了 400px,當(dāng)前 current 值是 0,那么position = 0 - Math.round(400/500) = 0 - -1 = 0 + 1 = 1
所以最后我們的 current 變成了 1
。根據(jù)上面的邏輯,我們?cè)?mouseup 的事件中要循環(huán)所有輪播中的 child 圖片,給它們都設(shè)置一個(gè)新的 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
作為圖片的長(zhǎng)度,那是因?yàn)槲覀冏约簩懙膱D片組件,它的圖片被我們固定為 500px 寬,而如果我們需要做一個(gè)通用的輪播組件的話,最好就是獲取元素的實(shí)際寬度,Element.clientWith()
。這樣我們的組件是可以隨著使用者去改變的。
做到這里,我們就可以用拖拽來(lái)輪播我們的圖片了,但是當(dāng)我們拖到最后一張圖的時(shí)候,我們就會(huì)發(fā)現(xiàn)最后一張圖之后就是空白了,第一張圖沒(méi)有接著最后一張。
那么接下來(lái)我們就去完善這個(gè)功能。這里其實(shí)和我們的自動(dòng)輪播是非常相似的,在做自動(dòng)輪播的時(shí)候我們就知道,每次輪播圖片的時(shí)候,我們最多就只能看到兩張圖片,可以看到三張圖片的機(jī)率是非常小的,因?yàn)槲覀兊妮啿サ膶挾认鄬?duì)我們的頁(yè)面來(lái)說(shuō)是非常小的,除非用戶有足夠的位置去拖到第二張圖以外才會(huì)出現(xiàn)這個(gè)問(wèn)題。但是這里我們就不考慮這種因素了。
我們確定每次拖拽的時(shí)候只會(huì)看到兩張圖片,所以我們也可以像自動(dòng)輪播那樣去處理拖拽的輪播。但是這里有一個(gè)點(diǎn)是不一樣的,我們自動(dòng)輪播的時(shí)候,圖片只會(huì)走一個(gè)方向,要么左要么右邊。但是我們手動(dòng)就可以往左或者往右拖動(dòng),圖片是可以走任意方向的。所以我們就無(wú)法直接用自動(dòng)輪播的代碼來(lái)實(shí)現(xiàn)這個(gè)功能了。我們就需要自己重新處理一下輪播頭和尾無(wú)限循環(huán)的邏輯。
我們可以從 mousemove 的回調(diào)函數(shù)開(kāi)始改造需要找到當(dāng)前元素在屏幕上的位置,我們給它 一個(gè)變量名叫 current
,它的值與我們之前在 mouseup 計(jì)算的 position 是一樣的 position + Math.round(x/500)
但是當(dāng)前這個(gè)元素是前后都有一張圖,這里我們就不去計(jì)算現(xiàn)在拖動(dòng)是需要拼接它前面還是后面的圖,我們直接就把當(dāng)前元素前后兩個(gè)圖都移動(dòng)到對(duì)應(yīng)的位置即可這里我們直接循環(huán)一個(gè) [-1, 0, 1]
的數(shù)組,對(duì)應(yīng)的是前一個(gè)元素
,當(dāng)前元素
和下一個(gè)元素
,這里我們需要使用這三個(gè)偏移值,獲取到上一個(gè)圖片,當(dāng)前拖動(dòng)的圖片和下一個(gè)圖片的移動(dòng)位置,這三個(gè)位置是跟隨著我們鼠標(biāo)的拖動(dòng)實(shí)時(shí)計(jì)算的接著我們?cè)谶@個(gè)循環(huán)里面需要先計(jì)算出前后兩張圖的位置,圖片位置 = 當(dāng)前圖片位置 + 偏移
,這里可以這么理解如果當(dāng)前圖片是在 2 這個(gè)位置,上一張圖就是在 1,下一張圖就在 3但是這里有一個(gè)問(wèn)題,如果我們當(dāng)前圖是在 0 的位置,我們上一張圖獲取到的位置就是 -1
,按照我們圖片的數(shù)據(jù)結(jié)構(gòu)來(lái)說(shuō),數(shù)組里面是沒(méi)有 -1
這個(gè)位置的。所以當(dāng)我們遇到計(jì)算出來(lái)的位置是負(fù)數(shù)的時(shí)候我們就要把它轉(zhuǎn)成這一列圖片的最后一張圖的位置。按照我們的例子里面的圖片數(shù)據(jù)來(lái)說(shuō)的話,當(dāng)前的圖是在 0 這個(gè)位置,那么上一張圖就應(yīng)該是我們?cè)? 號(hào)位的圖。那么我們?cè)趺茨馨?-1 變成 3, 在結(jié)尾的時(shí)候 4 變成 0 呢?這里需要用到一個(gè)數(shù)學(xué)中的小技巧了,如果我們想讓頭尾的兩個(gè)值超出的時(shí)候可以翻轉(zhuǎn),我們就需要用到一個(gè)公式, 求 (當(dāng)前指針 + 數(shù)組總長(zhǎng)度)/ 數(shù)組總長(zhǎng)度
的 余數(shù)
,這個(gè)獲得的余數(shù)就正好是翻轉(zhuǎn)的。
我們來(lái)證明一下這個(gè)公式是正確的,首先如果我們遇到 current = 0, 那么 0 這個(gè)位置的圖片的上一張就會(huì)獲得 -1 這個(gè)指針,這個(gè)時(shí)候我們用 ( − 1 + 4 ) / 4 = 3 / 4 (-1 + 4) / 4 = 3 / 4 (−1+4)/4=3/4,這里 3 除以 4 的余數(shù)就是 3,而
3
剛好就是這個(gè)數(shù)組的最后一個(gè)圖片。
然后我們來(lái)試試,如果當(dāng)前圖片就是數(shù)組里面的最后一張圖,在我們的例子里面就是 3,3 + 1 = 4, 這個(gè)時(shí)候通過(guò)轉(zhuǎn)換 ( 4 + 4 ) / 4 (4 + 4) / 4 (4+4)/4 余數(shù)就是
0
,顯然我們獲得的數(shù)字就是數(shù)組的第一個(gè)圖片的位置。
通過(guò)這個(gè)公式我們就可以取得上一張和下一張圖片在數(shù)組里面的指針位置,這個(gè)時(shí)候我們就可以用這個(gè)指針獲取到他們?cè)诠?jié)點(diǎn)中的對(duì)象,使用 CSSDOM 來(lái)改變他們的屬性這里我們需要先把所有元素移動(dòng)到當(dāng)前圖片的位置,然后根據(jù) -1、0、1 這三個(gè)偏移的值對(duì)這個(gè)圖片進(jìn)行往左或者往右移動(dòng),最后我們要需要加上當(dāng)前鼠標(biāo)的拖動(dòng)距離
我們已經(jīng)把整個(gè)邏輯給整理了一遍,下來(lái)我們看看 mousemove 這個(gè)事件回調(diào)函數(shù)代碼的應(yīng)該怎么寫:
- 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;
- // 計(jì)算圖片所在 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)`;
- }
- };
講了那么多東西,代碼就那么幾行,確實(shí)代碼簡(jiǎn)單不等于它背后的邏輯就簡(jiǎn)單。所以寫代碼的程序員也可以是深不可測(cè)的。
最后還有一個(gè)小問(wèn)題,在我們拖拽的時(shí)候,我們會(huì)發(fā)現(xiàn)上一張圖和下一張有一個(gè)奇怪跳動(dòng)的現(xiàn)象。
這個(gè)問(wèn)題是我們的 Math.round(x / 500)
所導(dǎo)致的,因?yàn)槲覀冊(cè)?transform 的時(shí)候,加入了 x % 500
, 而在我們的 current 值的計(jì)算中沒(méi)有包含這一部分的計(jì)算,所以在鼠標(biāo)拖動(dòng)的時(shí)候就會(huì)缺少這部分的偏移度。
我們只需要把這里的 Math.round(x / 500)
改為 (x - x % 500) / 500
即可達(dá)到同樣的取整數(shù)的效果,同時(shí)還可以保留我們 x
原有的正負(fù)值。
這里其實(shí)還有比較多的問(wèn)題的,我們還沒(méi)有去改 mouseup 事件里面的邏輯。那么接下來(lái)我們就來(lái)看看 up 中的邏輯我們應(yīng)該怎么去實(shí)現(xiàn)。
這里我們需要改的就是 children 中 for 循環(huán)的代碼,我們要實(shí)現(xiàn)的是讓我們拖動(dòng)圖片超過(guò)一定的位置就會(huì)自動(dòng)輪播到對(duì)應(yīng)方向的下一張圖片。up 這里的邏輯其實(shí)是和 move 是基本一樣的,不過(guò)這里有幾個(gè)地方需要更改的:
首先我們的 transition 禁止是可以去掉了,改為 ' '
空在 transform 中的 + x % 500
就不需要了,因?yàn)檫@里圖片是我們鼠標(biāo)松開(kāi)的時(shí)候,不需要圖片再跟隨我們鼠標(biāo)的位置了在計(jì)算 pos = current + offset
的這里,我們?cè)?up 的回調(diào)中是沒(méi)有 current 的,所以我們需要把 current 改為 position因?yàn)橛幸粋€(gè) z-index 的層次關(guān)系,我們會(huì)看到有圖片在被挪動(dòng)位置的時(shí)候,它在我們當(dāng)前圖片上飛過(guò),但是飛過(guò)去的元素其實(shí)是我們不需要的元素,而這個(gè)飛過(guò)去的元素是來(lái)源于我們之前用的 [-1, 0, 1] 這里面的 -1 和 1 的兩個(gè)元素,所以在 up 這個(gè)邏輯里面我們要把不需要的給去掉。意思就是說(shuō),如果我們鼠標(biāo)是往左移動(dòng)的,那么我們只需要 -1 的元素,相反就是只需要 1 的元素,另外的那邊的元素就可以去掉了。首先 for of
循環(huán)是沒(méi)有順序要求的,所以我們可以把 -1 和 1 這兩個(gè)數(shù)字用一個(gè)公式來(lái)代替,放在我們 0 的后面。但是怎么才能找到我們需要的是哪一邊呢?其實(shí)我們需要計(jì)算的就是圖片在移動(dòng)的方向,所以我們要改動(dòng)的就是 position = position - Math.round(x / 500)
這行代碼,這個(gè)方向可以通過(guò) Math.round(x / 500) - x
獲得。而這個(gè)值就是相對(duì)當(dāng)前元素的中間,他是更偏向左邊(負(fù)數(shù))還是右邊(正數(shù)),其實(shí)這個(gè)數(shù)字是多少并不是最重要的,我們要的是它的符號(hào)也就是 -1 還是 1,所以這里我們就可以使用 - Math.sign(Math.round(x / 500) - x)
來(lái)取得結(jié)果中的符號(hào),這個(gè)函數(shù)最終返回要不就是 -1, 要不就是 1 了, 正好是我們想要的。其實(shí)還有一個(gè)小 bug,當(dāng)我們拖動(dòng)當(dāng)前圖片過(guò)短的時(shí)候,圖片位置的計(jì)算是不正確的。
這個(gè)是因?yàn)槲覀兊?Match.round() 的特性,在 250(500px 剛好一半的位置) 之間是有一定的誤區(qū),讓我們無(wú)法判斷圖片需要往那個(gè)方向移動(dòng)的,所以在計(jì)算往 Match.round 的值之后我們還需要加上 + 250 * Match.sign(x)
,這樣我們的計(jì)算才會(huì)合算出是應(yīng)該往那邊移動(dòng)。
最終我們的代碼就是這樣的:
- 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;
- // 計(jì)算圖片所在 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ù)之后,我們就真正完成了這個(gè)手動(dòng)輪播的組件了。

到此這篇關(guān)于使用JSX實(shí)現(xiàn)Carousel輪播組件的方法(前端組件化)的文章就介紹到這了,更多相關(guān)JSX實(shí)現(xiàn)Carousel輪播組件內(nèi)容請(qǐng)搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://tridiamond.blog.csdn.net/article/details/112656150