iT邦幫忙

2023 iThome 鐵人賽

DAY 6
0
Modern Web

從Vue學React!不只要會用,還要真的懂~系列 第 6

【Day 6】元件的一生!Vue的生命週期 vs. React的生命週期

  • 分享至 

  • xImage
  •  

昨天看了渲染機制後,緊接著再往前一步來看看元件的生命週期吧!常寫Vue的朋友們應該對生命週期都不陌生,因為偶爾還是會遇到需要透過生命週期API來實現一些功能的時候,那今天就一起來看看Vue和React的生命週期吧!

什麼是元件的生命週期(Lifecycle)?

生命週期(Lifecycle)是元件在被創建、mount到DOM上、更新和unmount時所經歷的不同階段。了解生命週期能在實現一些邏輯的時候,正確地於不同的階段進行相對應的操作,也能避免一些預期外的問題發生,而想要了解生命週期的話,也需要先了解元件的渲染模式。

Vue 的生命週期

先來快速看一下Vue的生命週期有哪些!
https://ithelp.ithome.com.tw/upload/images/20230910/20130914lzlUz4BkIw.png
(圖片來源:Lifecycle Hooks)

Vue的生命週期主要有「beforeCreate」、「created」、「beforeMount」、「mounted」、「beforeUpdate」、「updated」、「beforeUnmount」、「umounted」這幾個階段。並且有onMounted、onUpdated等的生命週期API可以使用。如果有需要在特定的生命週期時間點做某些操作的話,只要使用對應的生命週期API就可以了。

以元件從初次渲染到重新渲染的整個過程下去思考的話,Vue元件的生命週期會是在一開始初始化的時候執行一次setup()把實體(instance)創建出來,接著才會把我們要用的state、computed、methods等都設定好,但是到這個階段為止,都只有完成一些資料的設置,尚未產生virtual DOM。在created之後,beforeMount之前的這段時間,才會開始解析template的內容,並且將它轉換成virtualDOM,最後將這個產生的virtual DOM轉換成實體DOM插入到目標的實體的DOM上,也就是mounted的階段。如果使用在template上的state有變化,觸發重新產生新的virtual DOM,在尚未更新到實體DOM之前,也就是beforeUpdate的階段,等到更新到實體DOM上時,也就是updated的階段。當這個元件被從DOM上刪除,就到了unmounted的階段,也就是說這個元件的一生結束了。

Vue component的一生,除了一開始會執行setup(),其他重新渲染的時間點,會進行動作的都只有template的部分,而這邊指的會進行的動作主要就是解析template的內容,然後建立virtualDOM,進行新舊對比,再把有差異的地方更新到需要更新的實體的DOM上

補充:Vue的實體(instance)是什麼?

https://ithelp.ithome.com.tw/upload/images/20230910/20130914rFjXfnQHVb.png
vue的實體是一個物件,內含元件的state、methods,還有生命週期的API等內容。如果好奇實體內容長怎樣的話,可以透過以下的方法把它印出來看看,實務上偶爾也需要透過直接存取實體來完成一些操作。

<script setup>
import { getCurrentInstance } from 'vue';
const instance = getCurrentInstance();
console.log('Vue Instance:', instance);
</script>

React Functional Component的生命週期?

在寫React的Class Component時,有mount、unmount、update等的這些生命週期的API可以使用,但是在Functional Component的寫法下,已經沒有提供對應生命週期的API了,不過這並不表示Functional Component沒有生命週期。這裡一樣透過「元件從初次渲染到重新渲染的整個過程」下去思考React Functional Component的生命週期!一開始會先呼叫component function,接著會解析JSX語法產生virtual DOM tree並創建virtual DOM,再來才會轉換成真實的DOM並放到目標的DOM上,這裡也就是mount的階段。當透過setState對state改動,並且在React比較新舊state,確定state有改動時,會再次互叫component function,產生新的virtual DOM,並比較新舊virtual DOM的差異,將真正需要更新的DOM更新上去,而這個階段也就是元件的update階段。當這個元件從DOM被移除,不顯示在畫面上時,就會是umount的階段。

仔細回去看上面的內容的話,會發現一開始mount的階段和update的階段實際上進行的過程好像差不多,都是呼叫component function產生virtual DOM,再更新到真實的DOM,但是在update階段會多了比較新舊DOM的過程。另外,這裡也有一個與Vue不太一樣的地方,就是Vue在更新的階段,只會單純地進行template解析的部分,並不會再重新從setup()部分執行,而React則是以component funtion為單位,並重新執行這個function。也是因為這種模式的元件生命週期,讓我們在寫React的Functional Component時,需要以資料的流動為基準下去思考。

從有生命週期API到沒有對應API的思考方向差異

這裡透過一個實際的例子,來看看有生命週期的API時和沒有時,可以思考差異是什麼?
這裡要實作的情境是「一進入頁面,會有開始一個計算秒數的計時器,並且會依照秒數為單數或單數顯示不同的背景色」。

寫一個class component時,思考方向會是
mount階段要先啟動計算秒數的計時器,在update的階段如果是秒數有變動,就去變更背景色,最後在ummount階段再把計時器清除。

export default class ClassComponent extends Component {
  constructor(props) {
    super(props);

    this.state = {
      second: 0,
      backgroundClass: null
    };
  }
  
  // mount階段要先啟動計時器
  componentDidMount() {
    this.interval = setInterval(this.addSecond, 1000);
  }
  // update階段要注意現在更新的秒數,有到特定秒數就換背景顏色
  componentDidUpdate(prevProps, prevState) {
    if (prevState.second !== this.state.second) {
      const { second } = this.state;
      const currentBackgroundColorClass = second % 2 === 0 ? "blue" : "yellow";
      this.setState({ backgroundClass: currentBackgroundColorClass });
    }
  }
  // umount的時候移除這個計時器
  componentWillUnmount() {
    clearInterval(this.interval);
  }

  addSecond = () => {
    this.setState((prevState) => ({
      second: prevState.second + 1
    }));
  };
  render() {
    return (
      <div className={this.state.backgroundClass}>
        <div className="title">Class Component</div>
        <div className="count">Second : {this.state.second}</div>
      </div>
    );
  }
}
 

寫一個functional component,思考的方向會是
呼叫component function後,會進行啟動計時器的這個副作用,隨著秒數的變動會需要處理它相依的副作用,在這個情境下也就是變更currentBackgroundColorClass來讓背景色可以改變的部分.離開頁面時,再透過副作用函式中回傳的cleanup函式來清除這個副作用。

function FuncionalComponent() {
  const [second, setSecond] = useState(0);
  const [currentBackgroundClass, setCurrentBackgroundClass] = useState(null);

  const addSecond = () => {
    setSecond((prevSecond) => prevSecond + 1);
  };

  // 利用呼叫component function後的副作用啟動計時器
  useEffect(() => {
    const intervalTimer = setInterval(addSecond, 1000);
    // 離開頁面時,在進行cleaup的動作,把計時器清掉。
    return () => {
      clearInterval(intervalTimer);
    };
  }, []);
  
  // 遂著second的變動來處理相依的副作用,當秒數到指定數字就變更背景色
  useEffect(() => {
    const backgroundClass = second % 2 === 0 ? "blue" : "yellow";
    setCurrentBackgroundClass(backgroundClass);
  }, [second]);

  return (
    <div className={currentBackgroundClass}>
      <div className="title">Functional Component</div>
      <div className="count">Second : {second}</div>
    </div>
  );
}

兩種寫法雖然達到的目的一樣,但因為Functional Component是以function為單位,沒有生命週期的hook可以使用,所以在操作同個情境時,在思考模式下也會有不同,會變成去思考相依值有變動時,是不是會需要處理相關的副作用。

React Strict Mode下的元件生命週期

const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <App />
  </StrictMode>,
  rootElement
);

前面已經了解了React元件的生命週期了,不過如果在開發模式中使用Strict Mode的話,則會產生出與我們已知的生命週期不同的地方。直接來看看是哪裡不一樣吧!

如果我們現在單純起一個空的React專案,讓它印出render,在不做任何變動的情況下,會發生什麼事情呢?我想應該有人會覺得「這有什麼好想的,呼叫一次component function,當然就是印出一次render啊!」不過神奇的地方就是在這個如此單純的情境下,居然莫名其妙印出兩次render啦!

https://ithelp.ithome.com.tw/upload/images/20230910/20130914T6bvxddMLy.png

出現這樣的情況是因為在開發模式下使用StrictMode的話,會透過模擬元件mount後unmount,再mount來檢驗這個元件是否會出現一些問題,所以才有重複console.log兩次的情況。除了重新渲染的部分外,useEffect也會出現setup後cleanup,再setup的狀況,以確保你的元件是pure的,也就是input和output會是一樣的,不會因為多mount再unmount再mount幾次,就出現問題。

這裡舉一個超級新手有可能會不小心踩到的問題,那就是不小心改動到從參數帶進元件的props。如果在函式內改動參數的話,就會讓這個函式不再純粹,因為有可能會出現預期外的結果。

例如:
一個很單純的計算+1的函式

const numberA = 2
// function addOne(a) {
//   return a + 1
// }
function addOne(a) {
  a = 5;
  return a + 1
}

console.log(addOne(numberA))

本來預期是numberA + 1 = 3,但是因為函式裡面改動的參數值,最後的結果也就不是我們預期的3,這也就讓本來應該要pure的函式變得不pure。

回到React的話,讓我們看一個簡單的情境。
我們將一組為陣列的data傳入子元件,並且直接改動這個透過props傳入的data,push多一個元素'd',我們預期出現的畫面應該是A B C D d

// 透過props傳入的data是["A", "B", "C", "D"];
export default function List({ data }) {
  data.push("d");
  return (
    <div className="list-container">
      {data.map((item) => (
        <p>{item}</p>
      ))}
    </div>
  );
}

但實際跑起來的狀況卻多了一個d!
https://ithelp.ithome.com.tw/upload/images/20230910/201309141AjCLSwjGr.png

會出現這樣的結果,就是因為我們是在StrictMode下進行開發。由於這樣改動props參數的作法已經讓函式不pure了,所以也就產生了我們不預期的結果,但也因為出現這樣的結果,而能讓我們提早發現這樣的做法會導致一些問題。

今天從Vue的生命週期看到React的生命週期,也進一步去思考在React Functional Component的寫法下,沒有生命週期的API,應該要用什麼樣的思考方向去進行實作,以及StrictMode讓React的component發生了什麼樣的事情。明天會繼續看和畫面有關的Template和JSX。

參考資料

[vue]Lifecycle Hooks
[React]StrictMode


上一篇
【Day 5】觸發重新渲染後的下一步 - Reconciliation (下)
下一篇
【Day 7】HTML的部分怎麼辦?Template和JSX
系列文
從Vue學React!不只要會用,還要真的懂~30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言