關於在 Vue 的 Single-File Components 上使用 HOF 分析

前言

最近朋友私訊問了我一個 Vue 有趣的地方,是關於它嘗試在 Vue 的 Single-File Components 上使用 Higher-Order Function(HOF)結果卻發生一個很有趣的雷點,所以就來寫一篇文章來分析一下這個問題。

問題程式碼

首先這邊先提供範例程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<script setup>
import { ref } from 'vue'

const activeDate = ref('');
const handleActiveClick = (dateStr) => () => {
activeDate.value = dateStr;
};

const handleActiveClick2 = (dateStr) => {
activeDate.value = dateStr;
};
</script>

<template>
<button type="button" @click="handleActiveClick('click1')">click 1</button>
<button type="button" @click="handleActiveClick('click2')()">click 2</button>
<button type="button" @click="() => handleActiveClick2('click3')">click 3</button>
<p>content:{{ activeDate }}</p>
</template>

<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}

a,
button {
color: #4fc08d;
}

button {
background: none;
border: solid 1px;
border-radius: 2em;
font: inherit;
padding: 0.75em 2em;
cursor: pointer;
}
</style>

如果覺得太長也可以看線上版:Vue SFC Playground

這邊我們透過操作可以發現 click1 按鈕是無效的,但是 click2click3 點了卻正常運作,而我們可以看到 handleActiveClick 採用的方式是 HOF 的方式,而 handleActiveClick2 則是一般的函式,那麼這個問題到底是什麼呢?

所以接下來就來準備分析一下這個問題。

分析問題

首先這個問題是發生在 Vue 的 Single-File Components 上,但是我們又不可能花時間去看 Vue 的原始碼,所以我們可以先透過簡單的方式來分析一下這個問題。

首先 Vue SFC Playground 上方可以看編譯後的結果,你可以點一下「JS」來看一下編譯後的結果,這邊我們可以看到編譯後結果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/* Analyzed bindings: {
"ref": "setup-const",
"activeDate": "setup-ref",
"handleActiveClick": "setup-const",
"handleActiveClick2": "setup-const"
} */
import { ref } from 'vue'


const __sfc__ = {
__name: 'App',
setup(__props, { expose: __expose }) {
__expose();

const activeDate = ref('');
const handleActiveClick = (dateStr) => () => {
activeDate.value = dateStr;
};

const handleActiveClick2 = (dateStr) => {
activeDate.value = dateStr;
};

const __returned__ = { activeDate, handleActiveClick, handleActiveClick2, ref }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}

};
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("button", {
type: "button",
onClick: _cache[0] || (_cache[0] = $event => ($setup.handleActiveClick('click1')))
}, "click 1"),
_createTextVNode("| "),
_createElementVNode("button", {
type: "button",
onClick: _cache[1] || (_cache[1] = $event => ($setup.handleActiveClick('click2')()))
}, "click 2"),
_createTextVNode("| "),
_createElementVNode("button", {
type: "button",
onClick: _cache[2] || (_cache[2] = () => $setup.handleActiveClick2('click3'))
}, "click 3"),
_createElementVNode("p", null, "content:" + _toDisplayString($setup.activeDate), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
__sfc__.render = render
__sfc__.__file = "src/App.vue"
export default __sfc__

那麼很複雜對吧?不用擔心,我們只需要看到底下這幾段地方就好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const handleActiveClick = (dateStr) => () => {
activeDate.value = dateStr;
};

// ...略過

function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("button", {
type: "button",
onClick: _cache[0] || (_cache[0] = $event => ($setup.handleActiveClick('click1')))
}, "click 1"),
_createTextVNode("| "),
_createElementVNode("button", {
type: "button",
onClick: _cache[1] || (_cache[1] = $event => ($setup.handleActiveClick('click2')()))
}, "click 2"),
_createTextVNode("| "),
_createElementVNode("button", {
type: "button",
onClick: _cache[2] || (_cache[2] = () => $setup.handleActiveClick2('click3'))
}, "click 3"),
_createElementVNode("p", null, "content:" + _toDisplayString($setup.activeDate), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}

// ...略過

首先直接看 render 的部分,可以看到會回傳兩個東西,分別是 _openBlock_createElementBlock,這邊只需要注意 _createElementBlock 的部分,_createElementBlock 的參數分別有三個,第一個是 Fragment,第二個是 null,第三個是一個陣列,而我們只需要注意第三個參數,也就是陣列的部分

那麼我們這邊是 <button type="button" @click="handleActiveClick('click1')">click 1</button> 無法正常運作,因此就可以看這一段

1
2
3
4
5
6
// ... 略過
_createElementVNode("button", {
type: "button",
onClick: _cache[0] || (_cache[0] = $event => ($setup.handleActiveClick('click1')))
}, "click 1"),
// ... 略過

_createElementVNode 基本上就是新增一個虛擬 DOM 元素的函式,這邊我們不用太深入,只需要專注於 onClick 這個屬性就好,這邊可以看到 onClick 的值是 _cache[0] || (_cache[0] = $event => ($setup.handleActiveClick('click1')))

這邊可能稍微有一點複雜,首先這邊有一個 || 運算子,在 JavaScript 中當前者是 True 的時候就會回傳前者,若沒有就回傳後者,所以如果 _cache[0] 有值的時候就回傳 _cache[0],若沒有值就回傳 (_cache[0] = $event => ($setup.handleActiveClick('click1'))),但是這邊有一個重點也就是裡面有一個 () 函示呼叫的運算子,因此會優先執行 (_cache[0] = $event => ($setup.handleActiveClick('click1'))) 這一段程式碼,因此 (_cache[0] = $event => ($setup.handleActiveClick('click1'))) 會先變成以下

1
_cache[0] = $event => ($setup.handleActiveClick('click1'))

接著因為裡面還有一個 () 函示呼叫的運算子,因此會先執行 $setup.handleActiveClick('click1'),但是這邊要注意前面我們的 handleActiveClick 是這樣寫

1
2
3
const handleActiveClick = (dateStr) => () => {
activeDate.value = dateStr;
};

因此套用過來時,完整版會變成以下

1
2
3
_cache[0] = $event => () => {
activeDate.value = 'click1';
}

所以完整版就變成了這樣

1
2
3
4
5
6
// ... 略過
_createElementVNode("button", {
type: "button",
onClick: _cache[0] = $event => () => (activeDate.value = 'click1')
}, "click 1"),
// ... 略過

那麼這也是為什麼當我們點擊第一個按鈕時會沒有任何反應的原因,因為其實回傳賦予給 onClick 的只是一個單純沒任何行為的函式,如果這樣不好讀的話,你可以看看以下是否比較好理解

1
2
3
4
5
6
7
8
9
10
// ... 略過
_createElementVNode("button", {
type: "button",
onClick: _cache[0] = $event => {
return () => {
activeDate.value = 'click1';
}
}
}, "click 1"),
// ... 略過

我們都知道這是一個類似閉包(Closure)的寫法,因此通常我們在寫閉包時,都會需要將函式回傳並儲存到一個變數內,如以下範例

1
2
3
4
5
6
7
8
9
10
function count() {
var icash = 1000;
return (price) => {
icash = icash - price;
return icash;
}
}
var item = count();
item(100);
item(200);

而這也就是為什麼第一個按鈕會失效的原因了,因為我們只是將函式回傳給 onClick,但是並沒有執行。

那為什麼第二個按鈕可以正常呢?因為我們在第二個按鈕的 onClick 是這樣寫的

1
2
3
4
5
6
// ... 略過
_createElementVNode("button", {
type: "button",
onClick: _cache[1] || (_cache[1] = $event => ($setup.handleActiveClick('click2')()))
}, "click 2"),
// ... 略過

我們可以看到呼叫了 handleActiveClick 並且執行了函式,因此第二個按鈕回傳的就是以下

1
2
3
4
5
6
7
// ... 略過
_createElementVNode("button", {
type: "button",
onClick: _cache[1] = $event => activeDate.value = 'click2'
}
}, "click 2"),
// ... 略過

第三個呢?為什麼正常運作呢?因為我們只是單純的傳入一個函式,因此不會有閉包的問題,因此第三個按鈕的 onClick 會是以下

1
2
3
4
5
// ... 略過
_createElementVNode("button", {
type: "button",
onClick: () => $setup.handleActiveClick2('click3')
}, "click 3"),

最後這邊也分享一下四種 @click 寫法

1
2
3
4
<button type="button" @click="handleActiveClick('click1')">click 1</button>|
<button type="button" @click="handleActiveClick('click2')()">click 2</button>|
<button type="button" @click="() => handleActiveClick2('click3')">click 3</button>|
<button type="button" @click="handleActiveClick2('click4')">click 4</button>

而他們編譯出來會是以下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("button", {
type: "button",
onClick: _cache[0] || (_cache[0] = $event => ($setup.handleActiveClick('click1')))
}, "click 1"),
_createTextVNode("| "),
_createElementVNode("button", {
type: "button",
onClick: _cache[1] || (_cache[1] = $event => ($setup.handleActiveClick('click2')()))
}, "click 2"),
_createTextVNode("| "),
_createElementVNode("button", {
type: "button",
onClick: _cache[2] || (_cache[2] = () => $setup.handleActiveClick2('click3'))
}, "click 3"),
_createTextVNode("| "),
_createElementVNode("button", {
type: "button",
onClick: _cache[3] || (_cache[3] = $event => ($setup.handleActiveClick2('click4')))
}, "click 4"),
_createElementVNode("p", null, "content:" + _toDisplayString($setup.activeDate), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}

透過以上分析,你應該就大概知道為什麼第一個按鈕會失效了吧~

那麼這邊應該也會有人好奇 React 的部分,這邊我就額外補上 React 編譯後的程式碼

1
2
3
4
5
6
7
8
const handleActiveDateClick = dateStr => () => {
activeDate.value = dateStr;
};

const example = /*#__PURE__*/React.createElement("h1", {
onClick: "handleActiveDateClick2('08-12')"
},
"Hello React");

這樣就可以大概知道 Vue 跟 React 實作上是有一定差異的。

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ