昨天看了渲染機制後,緊接著再往前一步來看看元件的生命週期吧!常寫Vue的朋友們應該對生命週期都不陌生,因為偶爾還是會遇到需要透過生命週期API來實現一些功能的時候,那今天就一起來看看Vue和React的生命週期吧!
生命週期(Lifecycle)是元件在被創建、mount到DOM上、更新和unmount時所經歷的不同階段。了解生命週期能在實現一些邏輯的時候,正確地於不同的階段進行相對應的操作,也能避免一些預期外的問題發生,而想要了解生命週期的話,也需要先了解元件的渲染模式。
先來快速看一下Vue的生命週期有哪些!
(圖片來源: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的實體是一個物件,內含元件的state、methods,還有生命週期的API等內容。如果好奇實體內容長怎樣的話,可以透過以下的方法把它印出來看看,實務上偶爾也需要透過直接存取實體來完成一些操作。
<script setup>
import { getCurrentInstance } from 'vue';
const instance = getCurrentInstance();
console.log('Vue Instance:', instance);
</script>
在寫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時和沒有時,可以思考差異是什麼?
這裡要實作的情境是「一進入頁面,會有開始一個計算秒數的計時器,並且會依照秒數為單數或單數顯示不同的背景色」。
寫一個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可以使用,所以在操作同個情境時,在思考模式下也會有不同,會變成去思考相依值有變動時,是不是會需要處理相關的副作用。
const rootElement = document.getElementById("root");
ReactDOM.render(
<StrictMode>
<App />
</StrictMode>,
rootElement
);
前面已經了解了React元件的生命週期了,不過如果在開發模式中使用Strict Mode的話,則會產生出與我們已知的生命週期不同的地方。直接來看看是哪裡不一樣吧!
如果我們現在單純起一個空的React專案,讓它印出render,在不做任何變動的情況下,會發生什麼事情呢?我想應該有人會覺得「這有什麼好想的,呼叫一次component function,當然就是印出一次render啊!」不過神奇的地方就是在這個如此單純的情境下,居然莫名其妙印出兩次render啦!
出現這樣的情況是因為在開發模式下使用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!
會出現這樣的結果,就是因為我們是在StrictMode下進行開發。由於這樣改動props參數的作法已經讓函式不pure了,所以也就產生了我們不預期的結果,但也因為出現這樣的結果,而能讓我們提早發現這樣的做法會導致一些問題。
今天從Vue的生命週期看到React的生命週期,也進一步去思考在React Functional Component的寫法下,沒有生命週期的API,應該要用什麼樣的思考方向去進行實作,以及StrictMode讓React的component發生了什麼樣的事情。明天會繼續看和畫面有關的Template和JSX。
[vue]Lifecycle Hooks
[React]StrictMode