前言
最近朋友私訊問了我一個 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
按鈕是無效的,但是 click2
跟 click3
點了卻正常運作,而我們可以看到 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
|
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 ) ], 64 )) } __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 ) ], 64 )) }
|
首先直接看 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 ) ], 64 )) }
|
透過以上分析,你應該就大概知道為什麼第一個按鈕會失效了吧~
那麼這邊應該也會有人好奇 React 的部分,這邊我就額外補上 React 編譯後的程式碼
1 2 3 4 5 6 7 8
| const handleActiveDateClick = dateStr => () => { activeDate.value = dateStr; };
const example = React.createElement("h1", { onClick: "handleActiveDateClick2('08-12')" }, "Hello React");
|
這樣就可以大概知道 Vue 跟 React 實作上是有一定差異的。