[譯] 使用 Vue 編寫一個長按指令

OFED發表於2019-02-28

原文連結:Building a long press directive in Vue

譯者:OFED

使用 Vue 編寫一個長按指令

Alt text

有沒有想過只需按住一個按鈕幾秒鐘就能在你的 Vue 應用中觸發一個功能?

有沒有想過建立一個按鈕,按下一次就可以清除單次輸入(或者持續按住可以清除所有輸入)?

想過?太好了,英雄所見略同。

本文就是講解如何在按下(或者按住)一個按鈕時,既執行一個函式,又清除輸入。

首先,我會講解如何使用純 JS 實現。而後也會建立一個 Vue 指令。

請繫好安全帶。好戲在後頭呢。

原理

要實現長按,使用者需要按下並按住按鈕幾秒鐘。

想通過程式碼模擬這一效果,我們需要在滑鼠“點選”按下按鈕時,啟動一個計時器監聽使用者按下的時長,如果時間超過我們期望的時長,就執行相應的函式。

非常簡單!然而,我們需要知道使用者何時按住按鈕。

如何實現

當使用者點選按鈕時,在點選事件之前會觸發另外兩個事件: mousedownmouseup

當使用者按下按鈕時觸發 mousedown 事件,使用者鬆開按鈕時呼叫 mouseup 事件。

我們需要做的是:

  1. mousedown 事件觸發時,啟動計時器。
  2. 一旦 mouseup 事件在預期的 2 秒前被觸發,就清除計時器,不要執行相應的函式。就當作一個普通的點選事件。

只要計時器在我們預設的時間內沒有被清除,即 mouseup 事件沒有被觸發——那麼可以斷定使用者沒有釋放按鈕。因此,可以判定為一次長按,可以執行關聯的函式。

實踐

讓我們深入程式碼,完成這一功能。

首先,我們必須定義三件事,即:

  1. 一個 變數 用於儲存計時器。
  2. 一個 啟動 功能函式,用於啟動計時器。
  3. 一個 取消 功能函式,用於取消計時器。

變數

這個變數主要用來儲存 setTimeout 的值,以便當滑鼠 mouseup 事件觸發時我們可以取消它。

let pressTimer = null;
複製程式碼

我們把變數值設定為 null 是為了在執行取消操作前,檢查這個變數的值判斷當前是否有一個正在執行的計時器。

啟動函式

這個函式包括一個 setTimeout,它是 JavaScript 中的一個基本方法,允許在特定時間之後執行一個函式。

注意,click 事件執行的過程中,會觸發另外兩個事件。但是我們需要啟動計時器的是 mousedown 事件。如果只是點選事件,不需要啟動計時器。

// 建立計時器 ( 1s之後執行函式 )
let start = (e) => {
    // 如果是點選事件,不啟動計時器
    if (e.type === 'click' && e.button !== 0) {
        return;
    }
    // 在啟動一個定時器之前確保沒有正在執行的計時器
    if (pressTimer === null) {
        pressTimer = setTimeout(() => {
            // 執行任務 !!!
        }, 1000)
    }
}
複製程式碼

取消函式

這個函式見名知意,用來取消啟動函式建立的 setTimeout

要取消 setTimeout ,可以使用 JavaScript 中的 clearTimeout 方法,它主要用來清除 setTimeout() 方法設定的計時器。

在使用 clearTimeout 之前,需要檢查 pressTimer 變數是否為 null。如果沒有為 null,意味著有一個正在執行的計時器。因此,我們需要先清除它,並且將 pressTimer 變數設定為 null

let cancel = (e) => {
    // 檢查 pressTimer 的值是否為 null
    if (pressTimer !== null) {
        clearTimeout(pressTimer)
        pressTimer = null
    }
}
複製程式碼

一旦 mouseup 事件觸發,這個函式就會被呼叫。

設定觸發器

剩下的就是將事件監聽器新增到想要長按效果的按鈕上。

addEventListener("mousedown", start);
addEventListener("click", cancel);
複製程式碼

以上程式碼合到一起是這樣:

// 定義變數
let pressTimer = null;

// 建立計時器( 1秒後執行函式 )
let start = (e) => {

    if (e.type === 'click' && e.button !== 0) {
        return;
    }

    if (pressTimer === null) {
        pressTimer = setTimeout(() => {

            // 執行任務 !!!

        }, 1000)
    }
}

// 停止計時器
let cancel = (e) => {

    // 檢查是否有正在執行的計時器
    if ( pressTimer !== null ) {
        clearTimeout(pressTimer);
        pressTimer = null;
    }
}

// 選擇 id 為 longPressButton 的元素
let el = document.getElementById('longPressButton');

// 新增事件監聽器
el.addEventListener("mousedown", start);

// 長按事件取消,取消計時器
el.addEventListener("click", cancel);
el.addEventListener("mouseout", cancel);
複製程式碼

用 Vue 指令包裝

建立 Vue 指令時,可以建立全域性或區域性指令,本文中,我們採用全域性指令。

首先,我們必須宣告自定義指令的名稱。

Vue.directive('longpress', {

})
複製程式碼

這就註冊了一個名為 v-longpress 的全域性自定義指令。

接下來,我們新增帶引數的 bind 鉤子函式,它允許我們引用指令繫結的元素,獲取傳遞給指令的值,並標識指令使用的元件。

Vue.directive('longpress', {
    bind: function(el, binding, vNode) {

    }
})
複製程式碼

接下來,我們在 bind 函式中新增長按功能的程式碼。

Vue.directive('longpress', {
    bind: function(el, binding, vNode) {

        // 定義變數
        let pressTimer = null;

        // 定義函式處理程式
        // 建立計時器( 1秒後執行函式 )
        let start = (e) => {

            if (e.type === 'click' && e.button !== 0) {
                return;
            }

            if (pressTimer === null) {
                pressTimer = setTimeout(() => {

                    // 執行任務 !!!

                }, 1000)
            }
        }

        // 取消計時器
        let cancel = (e) => {

            // 檢查是否有正在執行的計時器
            if ( pressTimer !== null ) {
                clearTimeout(pressTimer);
                pressTimer = null;
            }
        }

        // 新增事件監聽器
        el.addEventListener("mousedown", start);

        // 取消計時器
        el.addEventListener("click", cancel);
        el.addEventListener("mouseout", cancel);
    }
})
複製程式碼

接下來,我們需要新增一個函式來執行傳遞給 longpress 指令的方法。

Vue.directive('longpress', {
    bind: function(el, binding, vNode) {

        // 定義變數
        let pressTimer = null;

        // 定義函式處理程式
        // 建立計時器( 1秒後執行函式 )
        let start = (e) => {

            if (e.type === 'click' && e.button !== 0) {
                return;
            }

            if (pressTimer === null) {
                pressTimer = setTimeout(() => {
                    // 執行函式
                    handler();
                }, 1000)
            }
        }

        // 停止計時器
        let cancel = (e) => {

            // 檢查是否有正在執行的計時器
            if ( pressTimer !== null ) {
                clearTimeout(pressTimer);
                pressTimer = null;
            }
        }

        // 執行函式
        const handler = (e) => {
            // 執行傳遞給指令的方法
            binding.value(e)
        }

        // 新增事件監聽器
        el.addEventListener("mousedown", start);

        // 取消計時器
        el.addEventListener("click", cancel);
        el.addEventListener("mouseout", cancel);
    }
})
複製程式碼

現在,可以在 Vue 應用中使用這個指令了,除非使用者給指令傳入的值不是一個函式。因此,我們需要通過警告反饋給使用者。

為了反饋給使用者,我們在 bind 函式中新增了以下內容:

// 確保提供的表示式是函式
if (typeof binding.value !== 'function') {
    // 獲取元件名稱
    const compName = vNode.context.name;
    // 將警告傳遞給控制檯
    let warn = `[longpress:] provided expression '${binding.expression}' is not a function, but has to be `;
    if (compName) { warn += `Found in component '${compName}' ` }

    console.warn(warn);
}
複製程式碼

最後,如果這個指令也適用於觸屏裝置,那會是極好的。因此,我們新增了 touchstarttouchendtouchcancel 事件監聽器。

最終程式碼如下:

Vue.directive('longpress', {
    bind: function(el, binding, vNode) {

        // 確保提供的表示式是函式
        if (typeof binding.value !== 'function') {
            // 獲取元件名稱
            const compName = vNode.context.name;
            // 將警告傳遞給控制檯
            let warn = `[longpress:] provided expression '${binding.expression}' is not a function, but has to be `;
            if (compName) { warn += `Found in component '${compName}' `}

            console.warn(warn);
        }

        // 定義變數
        let pressTimer = null;

        // 定義函式處理程式
        // 建立計時器( 1秒後執行函式 )
        let start = (e) => {

            if (e.type === 'click' && e.button !== 0) {
                return;
            }

            if (pressTimer === null) {
                pressTimer = setTimeout(() => {
                    // 執行函式
                    handler();
                }, 1000)
            }
        }

        // 取消計時器
        let cancel = (e) => {

            // 檢查計時器是否有值
            if ( pressTimer !== null ) {
                clearTimeout(pressTimer);
                pressTimer = null;
            }
        }

        // 執行函式
        const handler = (e) => {
            // 執行傳遞給指令的方法
            binding.value(e)
        }

        // 新增事件監聽器
        el.addEventListener("mousedown", start);
        el.addEventListener("touchstart", start);

        // 取消計時器
        el.addEventListener("click", cancel);
        el.addEventListener("mouseout", cancel);
        el.addEventListener("touchend", cancel);
        el.addEventListener("touchcancel", cancel);
    }
})
複製程式碼

現在可以在 Vue 元件裡使用了:

<template>
    <div>
        <button v-longpress="incrementPlusTen" @click="incrementPlusOne">{{value}}</button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            value: 10
        }
    },
    methods: {
        // 增加1
        incrementPlusOne() {
            this.value++
        },
        // 增加10
        incrementPlusTen() {
            this.value += 10
        }
    }
}
</script>
複製程式碼
. . .

如果你想知道更多關於 自定義指令、可用的 鉤子函式、可以傳遞到這個鉤子函式中的 引數函式簡寫 的資訊, 參照 @vuejs 官方文件,作者做了很好的解釋。

收工,乾杯!

. . .

相關文章