乾坤大挪移!React 也能 「用上」 computed 屬性

  • 2019 年 12 月 19 日
  • 筆記

前言,關於計算屬性

初次見到計算屬性一詞,是在 Vue 官方文檔 《計算屬性和偵聽器》 一節中,文章中是這樣描述計算屬性的:

「模板內的表達式非常便利,但是設計它們的初衷是用於簡單運算的。在模板中放入太多的邏輯會讓模板過重且難以維護。

回想我們編寫的 React 代碼,是否也在 JSX(render 函數)中放入了太多的邏輯導致 render 函數過於龐大,難以維護?

React 中的計算屬性

說到 React 之前,我們先看下 Vue,在 Vue 中,計算屬性主要有以下兩點特性:

  1. 計算屬性以聲明的方式創建依賴關係,依賴的 data 或 props 變更會觸發重新計算並自動更新。
  2. 計算屬性是基於它們的響應式依賴進行緩存的。

而在 React 中,計算屬性也是經常可見,相信各位熟悉 React 的讀者都寫過類似下面的代碼:

import React, { Fragment, Component } from 'react';    class Example extends Component {    state = {      firstName: '',      lastName: '',    };      render() {      // 在 render 函數中處理邏輯      const { firstName, lastName } = this.state;      const fullName = `${firstName} ${lastName}`;      return <Fragment>{fullName}</Fragment>;    }  }

在上面的代碼里,render 函數里的 fullName 依賴了 props 中的 firstNamelastNamefirstNamelastName 變更之後,變量 fullName 都會自動更新。其實現原理是 props 以及 state 的變化會導致 render 函數調用,進而重新計算衍生值。

雖然能實現計算,但我們還是把計算邏輯放入了 render 函數導致了它的臃腫,這並不優雅。更好的做法是把計算邏輯抽出來,簡化 render 函數邏輯:

class Example extends Component {    state = {      firstName: '',      lastName: '',    };      // 把 render 中的邏輯抽成函數,減少render函數的臃腫    renderFullName() {      const { firstName, lastName } = this.state;      return `${firstName} ${lastName}`;    }      render() {      const fullName = this.renderFullName();      return <Fragment>{fullName}</Fragment>;    }  }

如果你對 Vue 很了解,你肯定知道其 computed 計算屬性,底層是使用了getter,只不過是對象的 getter。那麼在 React 中,我們也可以使用類的 getter 來實現計算屬性:

class Example extends Component {    state = {      firstName: '',      lastName: '',    };      // 通過getter而不是函數形式,減少變量    get fullName() {      const { firstName, lastName } = this.state;      return `${firstName} ${lastName}`;    }      render() {      return <Fragment>{this.fullName}</Fragment>;    }  }

進一步,使用 memoization 優化計算屬性

上文有提到在 Vue 中計算屬性對比函數執行,會有緩存,減少計算。因為計算屬性只有在它的相關依賴發生改變時才會重新求值。

這就意味着只要 firstName 和 lastName 還沒有發生改變,多次訪問 fullName 計算屬性會立即返回之前的計算結果,而不必再次執行函數。

對比之下,React 的 getter 是否也有緩存這個優勢??? 答案是:沒有。React 中的 getter 並沒有做緩存優化

不過不用失望,我們可以使用記憶化技術(memoization)來優化我們的計算屬性,達到和 Vue 中計算屬性一樣的效果。我們需要在項目中引入 memoize-one 庫,代碼如下:

import memoize from 'memoize-one';  import React, { Fragment, Component } from 'react';    class Example extends Component {    state = {      firstName: '',      lastName: '',    };      // 如果和上次參數一樣,`memoize-one` 會重複使用上一次的值。    getFullName = memoize((firstName, lastName) => `${firstName} ${lastName}`);      get fullName() {      return this.getFullName(this.state.firstName, this.state.lastName);    }      render() {      return <Fragment>{this.fullName}</Fragment>;    }  }

再進一步,使用 React Hooks 優化計算屬性

上文在 React 中使用了 memoize-one 庫實現了類似 Vue 計算屬性(computed)的效果 —— 基於依賴緩存計算結果。得益於React 16.8 新推出的 Hooks 特性,我們可以對邏輯進行更優雅的封裝,對 Hooks 還不夠了解的小夥伴可以先閱讀我們團隊另一篇文章 《看完這篇,你也能把 React Hooks 玩出花

此處,我們需要用到 useMemo。官方對 useMemo 的介紹在 這裡(https://zh-hans.reactjs.org/docs/hooks-reference.html#usememo),詳情請移步查看。簡單的說,就是我們傳入一個 回調函數 和一個 依賴列表,React 會在依賴列表中的值變化時,調用這個回調函數,並將回調函數返回的結果進行緩存:

import React, { useState, useMemo } from 'react';    function Example(props) {    const [firstName, setFirstName] = useState('');    const [lastName, setLastName] = useState('');    // 使用 useMemo 函數緩存計算過程    const renderFullName = useMemo(() => `${firstName} ${lastName}`, [      firstName,      lastName,    ]);      return <div>{renderFullName}</div>;  }

總結

本文介紹了在 React 中如何實現類似 Vue 計算屬性(computed)的效果 —— 基於依賴緩存計算結果,實現邏輯計算與視圖渲染的解耦,降低 render 函數的複雜度。

從業務開發角度來講,Vue 提供的 API 極大地提高了開發效率。React 雖然在某些場景下,沒有官方的同類原生 API 支持,但得益於活躍的社區,工作中遇到的問題總能找到解決方案。且在摸索這些解決方案的同時,我們還能學習到諸多經典的編程思想,幫助我們更合理的運用框架,用技術解決業務問題。