溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Vue.js函數(shù)式組件是什么

發(fā)布時(shí)間:2022-03-04 14:19:26 來源:億速云 閱讀:137 作者:小新 欄目:開發(fā)技術(shù)

這篇文章主要為大家展示了“Vue.js函數(shù)式組件是什么”,內(nèi)容簡(jiǎn)而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領(lǐng)大家一起研究并學(xué)習(xí)一下“Vue.js函數(shù)式組件是什么”這篇文章吧。

    前言

    如果你是一位前端開發(fā)者,又在某些機(jī)會(huì)下閱讀過一些 Java 代碼,可能會(huì)在后者中看到一種類似 ES6 語法中箭頭函數(shù)的寫法

    (String a, String b) -> a.toLowerCase() + b.toLowerCase();

    這種從 Java 8 后出現(xiàn)的 lambda 表達(dá)式,在 C++ / Python 中都有出現(xiàn),它比傳統(tǒng)的 OOP 風(fēng)格代碼更緊湊;雖然 Java 中的這種表達(dá)式本質(zhì)上還是一個(gè)生成類實(shí)例的函數(shù)式接口(functional interface)語法糖,但無論其簡(jiǎn)潔的寫法,還是處理不可變值并映射成另一個(gè)值的行為,都是典型的函數(shù)式編程(FP - functional programming)特征。

    1992 年的圖靈獎(jiǎng)得主 Butler Lampson 有一個(gè)著名的論斷:

    All problems in computer science can be solved by another level of indirection
    計(jì)算機(jī)科學(xué)中的任何問題都可以通過增加一個(gè)間接層次來解決

    這句話中的“間接層次”常被翻譯成“抽象層”,盡管有人曾爭(zhēng)論過其嚴(yán)謹(jǐn)性,但不管怎么翻譯都還說得通。無論如何,OOP 語言擁抱 FP,都是編程領(lǐng)域日益融合并重視函數(shù)式編程的直接體現(xiàn),也印證了通過引入另一個(gè)間接層次來解決實(shí)際問題的這句“軟件工程基本定理”。

    還有另一句同樣未必那么嚴(yán)謹(jǐn)?shù)牧餍姓f辭是:

    OOP 是對(duì)數(shù)據(jù)的抽象,而 FP 用來抽象行為

    不同于面向?qū)ο缶幊讨?,通過抽象出各種對(duì)象并注重其間的解耦問題等;函數(shù)式編程聚焦于最小的單項(xiàng)操作,將復(fù)雜任務(wù)變成一次次 f(x) = y 式的函數(shù)運(yùn)算疊加。函數(shù)是 FP 中的一等公民(First-class object),可以被當(dāng)成函數(shù)參數(shù)或被函數(shù)返回。

    同時(shí)在 FP 中,函數(shù)應(yīng)該不依賴或影響外部狀態(tài),這意味著對(duì)于給定的輸入,將產(chǎn)生相同的輸出 -- 這也就是 FP 中常常使用“不可變(immutable)”、“純函數(shù)(pure)”等詞語的緣由;如果再把前面提過的 “l(fā)ambda 演算”,以及 “curring 柯里化” 等掛在嘴邊,你聽上去就是個(gè) FP 愛好者了。

    以上這些概念及其相關(guān)的理論,集中誕生在 20 世紀(jì)前半葉,眾多科學(xué)家對(duì)數(shù)理邏輯的研究收獲了豐碩的成果;甚至現(xiàn)在熱門的 ML、AI 等都受益于這些成果。比如當(dāng)時(shí)大師級(jí)的美國(guó)波蘭裔數(shù)學(xué)家 Haskell Curry,他的名字就毫不浪費(fèi)地留在了 Haskell 語言和柯里化這些典型的函數(shù)式實(shí)踐中。

    React 函數(shù)式組件

    如果使用過 jQuery / RxJS 時(shí)的“鏈?zhǔn)秸Z法”,其實(shí)就可以算做 FP 中 monad 的實(shí)踐;而近年來大多數(shù)前端開發(fā)者真正接觸到 FP,一是從 ES6 中引入的 map / reduce 等幾個(gè)函數(shù)式風(fēng)格的 Array 實(shí)例方法,另一個(gè)就是從 React 中的函數(shù)式組件(FC - functional component)開始的。

    React 中的函數(shù)式組件也常被叫做無狀態(tài)組件(Stateless Component),更直觀的叫法則是渲染函數(shù)(render function),因?yàn)閷懗鰜碚娴木褪莻€(gè)用來渲染的函數(shù)而已:

    const Welcome = (props) => { 
      return <h2>Hello, {props.name}</h2>; 
    }

    結(jié)合 TypeScript 的話,還可以使用 type 和 FC<propsType> 來對(duì)這個(gè)返回了 jsx 的函數(shù)約束入?yún)ⅲ?/p>

    type GreetingProps = {
     name: string;
    }
     
    const Greeting:React.FC<GreetingProps> = ({ name }) => {
     return <h2>Hello {name}</h2>
    };

    也可以用 interface 和范型,更靈活地定義 props 類型:

    interface IGreeting<T = 'm' | 'f'> {
     name: string;
     gender: T
    }
    export const Greeting = ({ name, gender }: IGreeting<0 | 1>): JSX.Element => {
     return <h2>Hello { gender === 0 ? 'Ms.' : 'Mr.' } {name}</h2>
    };

    Vue(2.x) 中的函數(shù)式組件

    在 Vue 官網(wǎng)文檔的【函數(shù)式組件】章節(jié)中,這樣描述到:

    ...我們可以將組件標(biāo)記為 functional,這意味它無狀態(tài) (沒有響應(yīng)式數(shù)據(jù)),也沒有實(shí)例 (沒有 this 上下文)。一個(gè)函數(shù)式組件就像這樣:
     
    Vue.component('my-component', {
      functional: true,
      // Props 是可選的
      props: {
        // ...
      },
      // 為了彌補(bǔ)缺少的實(shí)例
      // 提供第二個(gè)參數(shù)作為上下文
      render: function (createElement, context) {
        // ...
      }
    })
     
    ...
     
    在 2.5.0 及以上版本中,如果你使用了[單文件組件],那么基于模板的函數(shù)式組件可以這樣聲明:
     
    <template functional>
    </template>

    寫過 React 并第一次閱讀到這個(gè)文檔的開發(fā)者,可能會(huì)下意識(shí)地發(fā)出 “啊這...” 的感嘆,寫上個(gè) functional 就叫函數(shù)式了???

    實(shí)際上在 Vue 3.x 中,你還真的能和 React 一樣寫出那種純渲染函數(shù)的“函數(shù)式組件”,這個(gè)我們后面再說。

    在目前更通用的 Vue 2.x 中,正如文檔中所說,一個(gè)函數(shù)式組件(FC - functional component)就意味著一個(gè)沒有實(shí)例(沒有 this 上下文、沒有生命周期方法、不監(jiān)聽任何屬性、不管理任何狀態(tài))的組件。從外部看,它大抵也是可以被視作一個(gè)只接受一些 prop 并按預(yù)期返回某種渲染結(jié)果的 fc(props) => VNode 函數(shù)的。

    并且,真正的 FP 函數(shù)基于不可變狀態(tài)(immutable state),而 Vue 中的“函數(shù)式”組件也沒有這么理想化 -- 后者基于可變數(shù)據(jù),相比普通組件只是沒有實(shí)例概念而已。但其優(yōu)點(diǎn)仍然很明顯:

    因?yàn)楹瘮?shù)式組件忽略了生命周期和監(jiān)聽等實(shí)現(xiàn)邏輯,所以渲染開銷很低、執(zhí)行速度快

    相比于普通組件中的 v-if 等指令,使用 h 函數(shù)或結(jié)合 jsx 邏輯更清晰

    更容易地實(shí)現(xiàn)高階組件(HOC - higher-order component)模式,即一個(gè)封裝了某些邏輯并條件性地渲染參數(shù)子組件的容器組件

    可以通過數(shù)組返回多個(gè)根節(jié)點(diǎn)

    ? 舉個(gè)栗子:優(yōu)化 el-table 中的自定義列

    先來直觀感受一個(gè)適用 FC 的典型場(chǎng)景:

    Vue.js函數(shù)式組件是什么

    這是 ElementUI 官網(wǎng)中對(duì)自定義表格列給出的例子,其對(duì)應(yīng)的 template 部分代碼為:

    <template>
      <el-table
        :data="tableData"
        >
        <el-table-column
          label="日期"
          width="180">
          <template slot-scope="scope">
            <i class="el-icon-time"></i>
            <span >{{ scope.row.date }}</span>
          </template>
        </el-table-column>
        <el-table-column
          label="姓名"
          width="180">
          <template slot-scope="scope">
            <el-popover trigger="hover" placement="top">
              <p>姓名: {{ scope.row.name }}</p>
              <p>住址: {{ scope.row.address }}</p>
              <div slot="reference" class="name-wrapper">
                <el-tag size="medium">{{ scope.row.name }}</el-tag>
              </div>
            </el-popover>
          </template>
        </el-table-column>
        <el-table-column label="操作">
          <template slot-scope="scope">
            <el-button
              size="mini"
              @click="handleEdit(scope.$index, scope.row)">編輯</el-button>
            <el-button
              size="mini"
              type="danger"
              @click="handleDelete(scope.$index, scope.row)">刪除</el-button>
          </template>
        </el-table-column>
      </el-table>
    </template>

    在實(shí)際業(yè)務(wù)需求中,像文檔示例中這種小表格當(dāng)然存在,但并不會(huì)成為我們關(guān)注的重點(diǎn);ElementUI 自定義表格列被廣泛地用于各種字段繁多、交互龐雜的大型報(bào)表的渲染邏輯中,通常是 20 個(gè)以上的列起步,并且每個(gè)列中圖片列表、視頻預(yù)覽彈窗、需要組合和格式化的段落、根據(jù)權(quán)限或狀態(tài)而數(shù)量不定的操作按鈕等等,不一而足;相關(guān)的 template 部分也經(jīng)常是幾百行甚至更多,除了冗長(zhǎng),不同列直接相似的邏輯難以復(fù)用也是個(gè)問題。

    正如電視劇《老友記》中臺(tái)詞所言:

    歡迎來到現(xiàn)實(shí)世界!它糟糕得要命~ 但你會(huì)愛上它!

    vue 單文件組件中并未提供 include 等拆分 template 的方案 -- 畢竟語法糖可夠多了,沒有最好。

    有潔癖的開發(fā)者會(huì)嘗試將復(fù)雜的列模版部分封裝成獨(dú)立的組件,來解決這個(gè)痛點(diǎn);這樣已經(jīng)很好了,但相比于本來的寫法又產(chǎn)生了性能隱患。

    回想起你在面試時(shí),回答關(guān)于如何優(yōu)化多層節(jié)點(diǎn)渲染問題時(shí)那種氣吞萬里的自信?,我們顯然在應(yīng)該在這次的實(shí)踐中更進(jìn)一步,既能拆分關(guān)注點(diǎn),又要避免性能問題,函數(shù)式組件就是一種這個(gè)場(chǎng)景下合適的方案。

    首先嘗試的是把原本 template 中日期列的部分“平移”到一個(gè)函數(shù)式組件 DateCol.vue 中:

    <template functional>
      <div>
        <i class="el-icon-time"></i>
        <span >{{ props.row.date }}</span>
      </div>
    </template>

    Vue.js函數(shù)式組件是什么

    在容器頁面中 import 后聲明在 components 中并使用:

    Vue.js函數(shù)式組件是什么

    基本是原汁原味;唯一的問題是受限于單個(gè)根元素的限制,多套了一層 div,這一點(diǎn)上也可以用 vue-fragment 等加以解決。

    接下來我們將姓名列重構(gòu)為 NameCol.js:

    export default {
      functional: true,
      render(h, {props}) {
        const {row} = props;
        return h('el-popover', {
            props: {trigger: "hover", placement: "top"},
            scopedSlots: {
              reference: () => h('div', {class: "name-wrapper"}, [
                h('el-tag', {props: {size: 'medium'}}, [row.name + '~'])
              ])
            }
          }, [
              h('p', null, [`姓名: ${ row.name }`]),
              h('p', null, [`住址: ${ row.address }`])
          ])
      }
    }

    Vue.js函數(shù)式組件是什么

    Vue.js函數(shù)式組件是什么

    效果沒得說,還用數(shù)組規(guī)避了單個(gè)根元素的限制;更重要的是,抽象出來的這個(gè)小組件是真正的 js 模塊,你可以不用 <script> 包裝它而將其放入一個(gè) .js 文件中,更可以自由地做你想做的一切事情了。

    h 函數(shù)可能帶來些額外的心智負(fù)擔(dān),只要再配置上 jsx 支持,那就和原版幾無二致了。

    另外這里涉及到的 scopedSlots 以及第三列里將面臨的事件處理等,我們后面慢慢說。

    渲染上下文

    回顧上面提到的文檔章節(jié),render 函數(shù)是這樣的形式:

    render: function (createElement, context) {}

    實(shí)際編碼中一般習(xí)慣性地將 createElement 寫為 h,并且即便在 jsx 用法中表面上不用調(diào)用 h,還是需要寫上的;在 Vue3 中,則可以用 import { h } from 'vue' 全局引入了。

    It comes from the term "hyperscript", which is commonly used in many virtual-dom implementations. "Hyperscript" itself stands for "script that generates HTML structures" because HTML is the acronym for "hyper-text markup language". -- Evan You

    官網(wǎng)文檔繼續(xù)寫到:

    組件需要的一切都是通過 context 參數(shù)傳遞,它是一個(gè)包括如下字段的對(duì)象:
     
        props:提供所有 prop 的對(duì)象
        children:VNode 子節(jié)點(diǎn)的數(shù)組
        slots:一個(gè)函數(shù),返回了包含所有插槽的對(duì)象
        scopedSlots:(2.6.0+) 一個(gè)暴露傳入的作用域插槽的對(duì)象。也以函數(shù)形式暴露普通插槽。
        data:傳遞給組件的整個(gè)數(shù)據(jù)對(duì)象,作為 createElement 的第二個(gè)參數(shù)傳入組件
        parent:對(duì)父組件的引用
        listeners:(2.3.0+) 一個(gè)包含了所有父組件為當(dāng)前組件注冊(cè)的事件監(jiān)聽器的對(duì)象。這是 data.on 的一個(gè)別名。
        injections:(2.3.0+) 如果使用了 inject 選項(xiàng),則該對(duì)象包含了應(yīng)當(dāng)被注入的 property。

    這個(gè) context 也就是被定義為 RenderContext 的一個(gè)接口類型,在 vue 內(nèi)部初始化或更新組件時(shí),是這樣形成的:

    Vue.js函數(shù)式組件是什么

    熟練掌握 RenderContext 接口定義的各種屬性,是我們玩轉(zhuǎn)函數(shù)式組件的基礎(chǔ)。

    template

    在前面的例子中,我們使用一個(gè)帶 functional 屬性的 template 模版,將表格中日期列部分的邏輯抽象為一個(gè)獨(dú)立模塊。

    上面的原理圖中也部分解釋了這一點(diǎn),Vue 的模板實(shí)際上被編譯成了渲染函數(shù),或者說 template 模版和顯式的 render 函數(shù)遵循同樣的內(nèi)部處理邏輯,并且被附加了 $options 等屬性。

    也就是說,處理一些復(fù)雜的邏輯時(shí),我們依然可以借助 js 的力量,比如在 template 中習(xí)慣地調(diào)用 methods 等 -- 當(dāng)然這并非真正的 Vue 組件方法了:

    Vue.js函數(shù)式組件是什么

    emit

    函數(shù)式組件中并沒有 this.$emit() 這樣的方法。

    但事件回調(diào)還是可以正常處理的,需要用到的就是 context.listeners 屬性 -- 正如文檔中提到的,這是 data.on 的一個(gè)別名。比如之前的例子中,我們想在容器頁面中監(jiān)聽日期列的圖標(biāo)被點(diǎn)擊:

    <date-col v-bind="scope" @icon-click="onDateClick" />

    在 DateCol.vue 中,這樣觸發(fā)事件就可以了:

    <i class="el-icon-time" 
          @click="() => listeners['icon-click'](props.row.date)">
        </i>

    唯一需要留意的是,雖然以上寫法足以應(yīng)付大部分情況,但如果外部監(jiān)聽了多個(gè)同名事件,listeners 就會(huì)變?yōu)橐粋€(gè)數(shù)組;所以相對(duì)完備的一種封裝方法是:

    /**
     * 用于函數(shù)式組件的事件觸發(fā)方法
     * @param {object} listeners - context 中的 listeners 對(duì)象
     * @param {string} eventName - 事件名
     * @param {...any} args - 若干參數(shù)
     * @returns {void} - 無
     */
    export const fEmit = (listeners, eventName, ...args) => {
        const cbk = listeners[eventName]
        if (_.isFunction(cbk)) cbk.apply(null, args)
        else if (_.isArray(cbk)) cbk.forEach(f => f.apply(null, args))
    }

    filter

    在 h 函數(shù)或 jsx 的返回結(jié)構(gòu)中,傳統(tǒng) Vue 模板中的 <label>{ title | withColon }</label> 過濾器語法不再奏效。

    好在原本定義的過濾函數(shù)也是普通的函數(shù),所以等效的寫法可以是:

    import filters from '@/filters';
     
    const { withColon } = filters;
     
    //...
     
    // render 返回的 jsx 中
    <label>{ withColon(title) }</label>

    插槽

    普通組件 template 部分中使用使用插槽的方法,在函數(shù)式組件 render 函數(shù)中,包括 jsx 模式下,都無法使用了。

    在前面例子中將姓名列重構(gòu)為 NameCol.js 的時(shí)候,已經(jīng)演示過了相對(duì)應(yīng)的寫法;再看一個(gè) ElementUI 中骨架屏組件的例子,比如普通的 template 用法是這樣的:

    <el-skeleton :loading="skeLoading">
        real text
        <template slot="template">
          <p>loading content</p>
        </template>
    </el-skeleton>

    這里面實(shí)際就涉及了 default 和 template 兩個(gè)插槽,換到函數(shù)式組件 render 函數(shù)中,對(duì)應(yīng)的寫法為:

    export default {
      functional: true,
      props: ['ok'],
      render(h, {props}) {
        return h('el-skeleton' ,{
          props: {loading: props.ok},
          scopedSlots: {
            default: () => 'real text',
            template: () => h('p', null, ['loading context'])
          }
        }, null)
      }
    }

    如果遇到 v-bind:user="user" 這樣傳遞了屬性的作用域插槽,那么將 user 作為插槽函數(shù)的入?yún)?/strong>就可以了。

    官網(wǎng)文檔中還提及了 slots()children 的對(duì)比:

    <my-functional-component>
      <p v-slot:foo>
        first
      </p>
      <p>second</p>
    </my-functional-component>
     
    對(duì)于這個(gè)組件,children 會(huì)給你兩個(gè)段落標(biāo)簽,而 slots().default 只會(huì)傳遞第二個(gè)匿名段落標(biāo)簽,slots().foo 會(huì)傳遞第一個(gè)具名段落標(biāo)簽。同時(shí)擁有 children 和 slots(),因此你可以選擇讓組件感知某個(gè)插槽機(jī)制,還是簡(jiǎn)單地通過傳遞 children,移交給其它組件去處理。

    provide / inject

    除了文檔中提到的 injections 用法,還要注意 Vue 2 中的 provide / inject 終究是非響應(yīng)式的。

    如果評(píng)估后非要使用這種方式,可以試試 vue-reactive-provide

    HTML 內(nèi)容

    Vue 中的 jsx 無法支持普通組件 template 中 v-html 的寫法,對(duì)應(yīng)的元素屬性是 domPropsInnerHTML,如:

    <strong class={type} domPropsInnerHTML={formatValue(item, type)} />

    而在 render 寫法中,又將這個(gè)單詞拆分了一下,寫法為:

    h('p', {
          domProps: {
                innerHTML: '<h2>hello</h2>'
          }
    })

    無論如何寫起來都確實(shí)費(fèi)勁了不少,但值得慶幸的是總比 React 中的 dangerouslySetInnerHTML 好記一些。

    樣式

    如果你采用了純 .js/.ts 的組件,可能唯一的麻煩就是無法再享受 .vue 組件中 scoped 的樣式了;參考 React 的情況,無非是以下幾種方法解決:

    • import 外部樣式并采用 BEM 等命名約定

    • 在 vue-loader 選項(xiàng)中開啟 CSS Modules 并在組件中應(yīng)用 styleMod.foo 的形式

    • 模塊內(nèi)動(dòng)態(tài)構(gòu)建 style 數(shù)組或?qū)ο螅x值給屬性

    • 采用工具方法動(dòng)態(tài)構(gòu)建樣式 class:

    const _insertCSS = css => {
        let $head = document.head || document.getElementsByTagName('head')[0];
        const style = document.createElement('style');
        style.setAttribute('type', 'text/css');
        if (style.styleSheet) {
            style.styleSheet.cssText = css;
        } else {
            style.appendChild(document.createTextNode(css));
        }
        $head.appendChild(style);
        $head = null;
    };

    TypeScript

    無論是 React 還是 Vue,本身都提供了一些驗(yàn)證 props 類型的手段。但這些方法一來配置上都稍顯麻煩,二來對(duì)于輕巧的函數(shù)式組件都有點(diǎn)過“重”了。

    TypeScript 作為一種強(qiáng)類型的 JavaScript 超集,可以被用來更精確的定義和檢查 props 的類型、使用更簡(jiǎn)便,在 VSCode 或其他支持 Vetur 的開發(fā)工具中的自動(dòng)提示也更友好。

    要將 Vue 函數(shù)式組件和 TS 結(jié)合起來的話,正如 interface RenderContext<Props> 定義的那樣,對(duì)于外部輸入的 props,可以使用一個(gè)自定義的 TypeScript 接口聲明其結(jié)構(gòu),如:

    interface IProps {
     year: string;
     quarters: Array<'Q1' | 'Q2' | 'Q3' | 'Q4'>;
     note: {
      content: string;
      auther: stiring;
     }
    }

    而后指定該接口為 RenderContext 的首個(gè)泛型:

    import Vue, { CreateElement, RenderContext } from 'vue';
     
    ...
     
    export default Vue.extend({
      functional: true,
      render: (h: CreateElement, context: RenderContext<IProps>) => {
         console.log(context.props.year);
       //...
      }
    });

    結(jié)合 composition-api

    與 React Hooks 類似的設(shè)計(jì)目的很相似的是,Vue Composition API 也在一定程度上為函數(shù)式組件帶來了響應(yīng)式特征、onMounted 等生命周期式的概念和管理副作用的方法。

    這里只探討 composition-api 特有的一種寫法 -- 在 setup() 入口函數(shù)中返回 render 函數(shù):

    比如定義一個(gè) counter.js:

    import { h, ref } from "@vue/composition-api";
     
    export default {
      model: {
        prop: "value",
        event: "zouni"
      },
      props: {
        value: {
          type: Number,
          default: 0
        }
      },
      setup(props, { emit }) {
        const counter = ref(props.value);
        const increment = () => {
          emit("zouni", ++counter.value);
        };
     
        return () =>
          h("div", null, [h("button", { on: { click: increment } }, ["plus"])]);
      }
    };

    在容器頁面中:

    <el-input v-model="cValue" />
    <counter v-model="cValue" />

    Vue.js函數(shù)式組件是什么

    如果要再結(jié)合 TypeScript 來用,改動(dòng)只有:

    • import { defineComponent } from "@vue/composition-api";

    • export default defineComponent<IProps>({ 組件 })

    單元測(cè)試

    如果使用了 TypeScript 的強(qiáng)類型加持,組件內(nèi)外的參數(shù)類型就有了較好的保障。

    而對(duì)于組件邏輯上,仍需要通過單元測(cè)試完成安全腳手架的搭建。同時(shí),由于函數(shù)式組件一般相對(duì)簡(jiǎn)單,測(cè)試編寫起來也不麻煩。

    在實(shí)踐中,由于 FC 與普通組件的區(qū)別,還是有些小問題需要注意:

    re-render

    由于函數(shù)式組件只依賴其傳入 props 的變化才會(huì)觸發(fā)一次渲染,所以在測(cè)試用例中只靠 nextTick() 是無法獲得更新后的狀態(tài)的,需要設(shè)法手動(dòng)觸發(fā)其重新渲染

    it("批量全選", async () => {
        let result = mockData;
        // 此處實(shí)際上模擬了每次靠外部傳入的 props 更新組件的過程
        // wrapper.setProps() cannot be called on a functional component
        const update = async () => {
          makeWrapper(
            {
              value: result
            },
            {
              listeners: {
                change: m => (result = m)
              }
            }
          );
          await localVue.nextTick();
        };
        await update();
        expect(wrapper.findAll("input")).toHaveLength(6);
     
        wrapper.find("tr.whole label").trigger("click");
        await update();
        expect(wrapper.findAll("input:checked")).toHaveLength(6);
     
        wrapper.find("tr.whole label").trigger("click");
        await update();
        expect(wrapper.findAll("input:checked")).toHaveLength(0);
     
        wrapper.find("tr.whole label").trigger("click");
        await update();
        wrapper.find("tbody>tr:nth-child(3)>td:nth-child(2)>ul>li:nth-child(4)>label").trigger("click");
        await update();
        expect(wrapper.find("tr.whole label input:checked").exists()).toBeFalsy();
      });

    多個(gè)根節(jié)點(diǎn)

    函數(shù)式組件的一個(gè)好處是可以返回一個(gè)元素?cái)?shù)組,相當(dāng)于在 render() 中返回了多個(gè)根節(jié)點(diǎn)(multiple root nodes)。

    這時(shí)候如果直接用 shallowMount 等方式在測(cè)試中加載組件,會(huì)出現(xiàn)報(bào)錯(cuò):

    [Vue warn]: Multiple root nodes returned from render function. Render function should return a single root node.

    解決方式是封裝一個(gè)包裝組件

    import { mount } from '@vue/test-utils'
    import Cell from '@/components/Cell'
     
    const WrappedCell = {
      components: { Cell },
      template: `
        <div>
          <Cell v-bind="$attrs" v-on="$listeners" />
        </div>
      `
    }
     
    const wrapper = mount(WrappedCell, {
      propsData: {
        cellData: {
          category: 'foo',
          description: 'bar'
        }
      }
    });
     
    describe('Cell.vue', () => {
      it('should output two tds with category and description', () => {
        expect(wrapper.findAll('td')).toHaveLength(2);
        expect(wrapper.findAll('td').at(0).text()).toBe('foo');
        expect(wrapper.findAll('td').at(1).text()).toBe('bar');
      });
    });

    fragment 組件

    另一個(gè)可用到 FC 的小技巧是,對(duì)于一些引用了 vue-fragment (一般也是用來解決多節(jié)點(diǎn)問題)的普通組件,在其單元測(cè)試中可以封裝一個(gè)函數(shù)式組件 stub 掉 fragment 組件,從而減少依賴、方便測(cè)試:

    let wrapper = null;
    const makeWrapper = (props = null, opts = null) => {
      wrapper = mount(Comp, {
        localVue,
        propsData: {
          ...props
        },
        stubs: {
          Fragment: {
            functional: true,
            render(h, { slots }) {
              return h("div", slots().default);
            }
          }
        },
        attachedToDocument: true,
        sync: false,
        ...opts
      });
    };

    Vue 3 中的函數(shù)式組件

    這部分內(nèi)容基本和我們之前在 composition-api 中的實(shí)踐是一致的,大致提取一下新官網(wǎng)文檔中的說法吧:

    真正的函數(shù)組件

    在 Vue 3 中,所有的函數(shù)式組件都是用普通函數(shù)創(chuàng)建的。換句話說,不需要定義 { functional: true } 組件選項(xiàng)。

    它們將接收兩個(gè)參數(shù):propscontextcontext 參數(shù)是一個(gè)對(duì)象,包含組件的 attrsslotsemit property。

    此外,h 現(xiàn)在是全局導(dǎo)入的,而不是在 render 函數(shù)中隱式提供:

    import { h } from 'vue'
     
    const DynamicHeading = (props, context) => {
      return h(`h${props.level}`, context.attrs, context.slots)
    }
     
    DynamicHeading.props = ['level']
     
    export default DynamicHeading

    單文件組件

    在 3.x 中,有狀態(tài)組件和函數(shù)式組件之間的性能差異已經(jīng)大大減少,并且在大多數(shù)用例中是微不足道的。因此,在單文件組件上使用 functional 的開發(fā)者的遷移路徑是刪除該 attribute,并props 的所有引用重命名為 $props,以及將 attrs 重命名為 $attrs:

    <template>
      <component
        v-bind:is="`h${$props.level}`"
        v-bind="$attrs"
      />
    </template>
     
    <script>
    export default {
      props: ['level']
    }
    </script>

    主要的區(qū)別在于:

    1. <template> 中移除 functional attribute

    2. listeners 現(xiàn)在作為 $attrs 的一部分傳遞,可以將其刪除

    以上是“Vue.js函數(shù)式組件是什么”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注億速云行業(yè)資訊頻道!

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI