詳解React?的數(shù)據(jù)流和生命周期
數(shù)據(jù)流和生命周期
如何處理 React 中的數(shù)據(jù),組件之間如何通信,數(shù)據(jù)在 React 中如何流動?
常用的 React 生命周期方法以及開源項(xiàng)目 spug 中使用了哪些生命周期方法?
數(shù)據(jù)和數(shù)據(jù)流
雖然也有很多靜態(tài)網(wǎng)站,但人們使用的大多數(shù)網(wǎng)站都充滿了隨時(shí)間變化的數(shù)據(jù)。
state
和 props
是 React 組件處理數(shù)據(jù)和彼此通信
的兩種主要方法。
React 提供了兩個(gè)數(shù)據(jù)相關(guān)的 api:state 和 props。
前面我們已經(jīng)知道,不要直接修改 state,而是使用 setState(updater[, callback])
updater 函數(shù)簽名:(preState, props) => stateChange
Tip: 建議傳遞函數(shù)
而非對象。React 16 以前的版本允許傳遞對象而不是函數(shù)給 setState 的第一個(gè)參數(shù)。傳遞對象給 state 暗示是同步的,實(shí)際卻并非如此,而使用函數(shù),更符合 React 的異步范式,React 會更新,但不保證時(shí)間。
筆者測試,下面兩種寫法都生效:
this.setState({ date: new Date() }) this.setState(() => ({ date: new Date() }))
假設(shè)我們想根據(jù) props.step 來增加 state,可以這樣:
this.setState((state, props) => { return {counter: state.counter + props.step}; });
setState 是淺合并
,而非替換。請看實(shí)驗(yàn):合并還是替換
如果需要在應(yīng)用更新后觸發(fā)某些動作,可以使用 setState 的回調(diào)函數(shù)(setState(updater, callback)
)
不可變狀態(tài) props
在 React 中,屬性是傳遞不可變數(shù)據(jù)的主要方式,所有組件都可以接收屬性,并能在構(gòu)造函數(shù)、render()
和生命周期方法中使用。
屬性通常來自父組件或自身的默認(rèn)屬性值(defaultProps)。
是否可以將父組件的 state 作為屬性傳給子組件呢?是的。一個(gè)組件的狀態(tài)可以是另一個(gè)組件的屬性。
可以將任何有效的 js 數(shù)據(jù)作為屬性傳遞給子組件
Tip: 前面 我們已經(jīng)測試過了,如果修改 props(this.props.name = 'aName'
),控制臺會報(bào)錯。
屬性可以隨時(shí)間改變,但不是從組件內(nèi)部改變。這是單向數(shù)據(jù)流
的一部分,下面會提到。
只使用 props 的組件
倘若我們希望將狀態(tài)保存在一個(gè)中心位置,而不是分散在各個(gè)組件中,比如 Flux、Redux等。這時(shí)我們可以創(chuàng)建無狀態(tài)函數(shù)組件
(或稱之為函數(shù)組件)。
無狀態(tài)不是沒有任何種類的狀態(tài),而是它不會獲得 React 進(jìn)行管理的支撐實(shí)例,意味著沒有生命周期方法,沒有 state。
無狀態(tài)函數(shù)組件與有支撐實(shí)例的父組件結(jié)合使用時(shí)非常強(qiáng)大。與其在多個(gè)組件間設(shè)置狀態(tài),不如創(chuàng)建單個(gè)有狀態(tài)的父組件并讓其余部分使用輕量級子組件。例如 Redux 會將這個(gè)模式提升到全新的水平。
Tip:請練習(xí),使用一個(gè)組件的狀態(tài)修改另一個(gè)組件的屬性。(下文父子組件
有使用)
組件通信
現(xiàn)在我們能夠用子組件輕易的構(gòu)建新組件,能夠很容易地表示組件間的 has-a、is-a 的關(guān)系。如何讓他們通信呢?
在 React 中,如果想讓組件彼此通信,需要傳遞屬性
,開發(fā)人員會做兩件事:
- 訪問父組件中的數(shù)據(jù)(狀態(tài)或?qū)傩裕?/li>
- 傳遞數(shù)據(jù)給子組件
下面定義 1 個(gè)父組件,2 個(gè)子組件,其中 2 個(gè)子組件中的數(shù)據(jù)來自父組件。請看示例:
const MyComponent = (props) => ( <div> <MyComponentSub1 content={props.cnt1}/> <MyComponentSub2 content={props.cnt2}/> </div> ) const MyComponentSub1 = (props) => ( <p>sub1: {props.content}</p> ) const MyComponentSub2 = (props) => ( <p>sub2: {props.content}</p> ) ReactDOM.render( <MyComponent cnt1="apple" cnt2="orange" />, document.getElementById('root') );
頁面顯示:
sub1: apple sub2: orange
單向數(shù)據(jù)流
數(shù)據(jù)如何流經(jīng)應(yīng)用的不同部分?
在 React 中,UI 是數(shù)據(jù)投射到視圖中的數(shù)據(jù),當(dāng)數(shù)據(jù)變化時(shí),視圖隨之變化。
React 中,數(shù)據(jù)流是單向
的。上層通過 props 傳遞數(shù)據(jù)給子組件,子組件通過回調(diào)函數(shù)將數(shù)據(jù)傳遞給上層。單向數(shù)據(jù)流讓思考數(shù)據(jù)在應(yīng)用中的流動變得更簡單
。
也得益于組件的層次結(jié)構(gòu)以及將屬性和狀態(tài)局限于組件,預(yù)測數(shù)據(jù)在應(yīng)用中如何移動也更加簡單。
數(shù)據(jù)在 React 中是按一個(gè)方向
流動的。屬性由父組件傳遞給子組件(從所有者到擁有者),子組件不能編輯父組件的狀態(tài)或?qū)傩浴C總€(gè)擁有支撐實(shí)例的組件都能修改自身的 state 但無法修改超出自身的東西,除了設(shè)置子組件的屬性。
如果允許從應(yīng)用的部分隨意修改想要修改的東西,聽上去好像不錯,實(shí)際上可能會導(dǎo)致難以捉摸的應(yīng)用并且可能會造成調(diào)試?yán)щy。
React 中的數(shù)據(jù)流是單向
的,從父組件流向子組件,子組件通過回調(diào)函數(shù)
將數(shù)據(jù)回送給父組件,但它不能直接修改父組件的狀態(tài),而父組件也不能直接修改子組件的狀態(tài),組件間通信通過屬性完成。
渲染和生命周期
渲染就是 React 創(chuàng)建和管理用戶界面所做的工作,也就是讓應(yīng)用展現(xiàn)到屏幕上。
和 vue 中生命周期類似,react 也可以分 4 部分:
- 初始 - 組件被實(shí)例化的時(shí)候
- 掛載 - 組件插入到 dom
- 更新 - 通過狀態(tài)或?qū)傩杂眯聰?shù)據(jù)更新組件
- 卸載 - 組件從 dom 中移除
生命周期方法簡介
筆者在前文已經(jīng)分析過一次 react 生命周期相關(guān)方法,這里再次總結(jié)一下:
掛載
時(shí)的順序:constructor()、render()、componentDidMount()(組件掛載后立即調(diào)用)
Tip:
- 掛載是 React 將組件插入到 dom 的過程。React 在實(shí)際 dom 中創(chuàng)建組件之前,組件只存在虛擬 dom 中。
- 容易犯的一個(gè)錯誤是把不該放到 render() 中的東西放到了 render() 里面。render() 通常會在組件的生命周期內(nèi)調(diào)用多次,也無法確定 React 會何時(shí)調(diào)用 render(),因?yàn)槌鲇谛阅?React 會批量更新。
- 掛載(ReactDOM.render)和卸載(ReactDOM.unmountComponentAtNode)由 React.DOM 從外部控制。沒有 ReactDOM 的幫助,組件不能卸載自己。
更新
時(shí)的順序:shouldComponentUpdate、render()、componentDidUpdate(組件更新后被立即調(diào)用)
卸載時(shí):componentWillUnmount
過時(shí)
的生命周期有:componentWillMount、componentWillReceiveProps、componentWillUpdate。官網(wǎng)不建議在新代碼中使用它們。
避免
使用:forceUpdate、shouldComponentUpdate。
Tip:React 采用了復(fù)雜的、先進(jìn)的方法確定應(yīng)該更新什么以及何時(shí)更新。如果最終使用了 shouldComponentUpdate,則應(yīng)該是那些方法由于某種原因不夠用的情況下。
新增
生命周期方法:getDerivedStateFromProps、getSnapshotBeforeUpdate。都屬于不常見的情形。
spug 使用了哪些生命周期方法
React 有那么多生命周期相關(guān)方法,那么像 spug 這種運(yùn)維相關(guān)的開源項(xiàng)目用到了哪些,是否都用到了?
答:只用到了掛載、更新和卸載 3
個(gè)方法。
請看筆者統(tǒng)計(jì):
constructor
共 25 處
幾乎都是初始化 state。沒有發(fā)送數(shù)據(jù)請求。摘錄如下:
constructor(props) { super(props); this.state = { groupMap: {} } }
constructor(props) { super(props); this.textView = null; this.JSONView = null; this.state = { view: '1' } }
constructor(props) { super(props); this.input = null; this.state = { fetching: false, showDot: false, uploading: false, uploadStatus: 'active', pwd: [], objects: [], percent: 0 } }
componentDidMount
共 29 處。都是用于發(fā)送 ajax 請求數(shù)據(jù)。摘錄如下:
store.fetchRecords();
http.post('/api/config/history/'
this.updateValue()
Tip: 感覺請求數(shù)據(jù)時(shí)機(jī)有點(diǎn)遲。因?yàn)?vue 中請求數(shù)據(jù)我們通常會寫在 created 生命周期中,能更快獲取到服務(wù)端數(shù)據(jù),減少頁面loading 時(shí)間。
0 處使用
以下生命周期方法都未
在 spug 中使用。
- shouldComponentUpdate
- componentDidUpdate
- componentWillUnmount
- componentDidCatch(構(gòu)造函數(shù)、渲染、生命周期中未捕獲的錯誤)
- 過時(shí)的生命周期:componentWillMount、componentWillReceiveProps、componentWillUpdate。
- 不建議使用:forceUpdate、shouldComponentUpdate
- 新增生命周期方法:getDerivedStateFromProps、getSnapshotBeforeUpdate
React.useEffect
我們知道 React.useEffect 可以在函數(shù)組件中模擬 componentDidMount、componentDidUpdate、componentWillUnmount。就像這樣:
// 相當(dāng)于 componentDidMount() React.useEffect(() => { console.log('a') }, [])
// 相當(dāng)于 componentDidMount()、componentDidUpdate() React.useEffect(() => { console.log('a') })
// 相當(dāng)于 componentDidMount、componentWillUnmount React.useEffect(() => { console.log('a') return () => { console.log('b') } }, [])
Tip: 有關(guān) React.useEffect 的詳細(xì)介紹請看 這里 -> 體驗(yàn) useEffect
搜索 useEffect
,發(fā)現(xiàn) spug 有 126 處。作用就這 3 點(diǎn):
- 模擬 componentDidMount 發(fā)送網(wǎng)絡(luò)請求,或初始化數(shù)據(jù),或設(shè)置定時(shí)器
- 模擬 componentWillUnmount 刪除定時(shí)器
- 模擬 componentDidUpdate
摘錄如下:
// 模擬 componentDidMount,用于發(fā)送網(wǎng)絡(luò)請求 useEffect(() => { setFetching(true); http.get('/api/app/deploy/') .then(res => setDeploys(res)) .finally(() => setFetching(false)) if (!envStore.records.length) { envStore.fetchRecords().then(_initEnv) } else { _initEnv() } }, [])
// 模擬 componentDidMount 發(fā)送網(wǎng)絡(luò)請求、添加定時(shí)器,模擬 componentWillUnmount 刪除定時(shí)器 useEffect(() => { fetch(); listen(); timer = setInterval(() => { if (ws.readyState === 1) { ws.send('ping') } else if (ws.readyState === 3) { listen() } }, 10000) return () => { if (timer) clearInterval(timer); if (ws.close) ws.close() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [])
React.useEffect(() => { store.fetchGitlabList() store.fetchProjectName() }, [])
// 模擬 componentDidMount()、componentDidUpdate() useEffect(() => { setLoading(true); http.get('/api/home/alarm/', {params}) .then(res => setRes(res)) .finally(() => setLoading(false)) }, [params])
useEffect(() => { let socket; initialTerm() http.get(`/api/repository/${store.record.id}/`) .then(res => { term.write(res.data) setStep(res.step) if (res.status === '1') { socket = _makeSocket(res.index) } else { setStatus('wait') } }) .finally(() => setFetching(false)) return () => socket && socket.close() // eslint-disable-next-line react-hooks/exhaustive-deps }, [])
useEffect(props.request.mode === 'read' ? readDeploy : doDeploy, [])
// 寫多個(gè) useEffect 也可以 useEffect(() => { if (!loading) { http.get('/api/exec/history/') .then(res => setHistories(res)) } }, [loading]) useEffect(() => { return () => { store.host_ids = [] if (store.showConsole) { store.switchConsole() } } }, [])
// 掛載時(shí)注冊事件 resize,卸載時(shí)注銷事件 resize useEffect(() => { store.tag = '' gCurrent = current const fitPlugin = new FitAddon() term.setOption('disableStdin', false) term.setOption('fontFamily', 'Source Code Pro, Courier New, Courier, Monaco, monospace, PingFang SC, Microsoft YaHei') term.setOption('theme', {background: '#f0f0f0', foreground: '#000', selection: '#999', cursor: '#f0f0f0'}) term.loadAddon(fitPlugin) term.open(el.current) fitPlugin.fit() term.write('\x1b[36m### WebSocket connecting ...\x1b[0m') const resize = () => fitPlugin.fit(); window.addEventListener('resize', resize) setTerm(term) return () => window.removeEventListener('resize', resize); // eslint-disable-next-line react-hooks/exhaustive-deps }, [])
父子組件的生命周期
筆者創(chuàng)建父子兩個(gè)組件,加入常見的生命周期方法(如:componentDidMount、shouldComponentUpdate、componentDidUpdate、componentWillUnmount、componentDidCatch),功能很簡單,子組件的文案來自父組件的 state,父組件的 state.text 能被用戶通過 input 修改。
代碼如下:
<script type="text/babel"> class ChildComponent extends React.Component { static propTypes = { name: PropTypes.string }; static defaultProps = (function () { console.log("ChildComponent : defaultProps。孩子的 defaultProps 是"); return {}; })(); constructor(props) { super(props); console.log("ChildComponent: state。孩子的 state 是"); this.state = { text: "peng" }; } // 組件掛載后(插入 DOM 樹中)立即調(diào)用。常做網(wǎng)絡(luò)請求 componentDidMount() { console.log("ChildComponent : componentDidMount"); } shouldComponentUpdate(nextProps, nextState) { console.log("<ChildComponent/> - shouldComponentUpdate()"); return true; } // React 更新 dom 和 refs 后調(diào)用 componentDidUpdate(previousProps, previousState) { console.log("ChildComponent: componentDidUpdate"); } componentWillUnmount() { console.log("ChildComponent: componentWillUnmount"); } render() { if (this.props.content === 'jiali12') { throw new Error("模擬組件報(bào)錯"); } console.log("ChildComponent: render"); // 下面兩種寫法等效。一個(gè)是單根一個(gè)是多根。 // return <div className="subClass"> // sub // <p>{this.props.content}</p> // </div> // 下面這種數(shù)組的寫法需要給每個(gè)元素添加 key,否則報(bào)錯如下: // Warning: Each child in a list should have a unique "key" prop. // React 要求每個(gè)被迭代項(xiàng)傳遞一個(gè) key,對于 render() 方法返回的任何數(shù)組組件亦如此。 return [ <div key="name" className="subClass">sub</div>, <p key="content">{this.props.content}</p> ] } } class ParentComponent extends React.Component { static defaultProps = (function () { console.log("ParentComponent: defaultProps。我的 defaultProps 是"); return { true: false }; })(); constructor(props) { super(props); console.log("ParentComponent: state。我的 state 是"); this.state = { text: "jiali" }; this.onInputChange = this.onInputChange.bind(this); } onInputChange(e) { const text = e.target.value; this.setState(() => ({ text: text })); } componentDidMount() { console.log("ParentComponent: componentDidMount"); } componentDidUpdate(previousProps, previousState) { console.log("ParentComponent: componentDidUpdate"); } componentWillUnmount() { console.log("ParentComponent: componentWillUnmount"); } // 此生命周期在后代組件拋出錯誤后被調(diào)用 componentDidCatch(err, errorInfo) { console.log("componentDidCatch"); this.setState(() => ({ err, errorInfo })); } render() { if (this.state.err) { return <div>降級處理</div> } console.log("ParentComponent: render"); return <div className="parentClass"> <p>parent</p> <input key="input" value={this.state.text} onChange={this.onInputChange} /> <ChildComponent content={this.state.text} /> </div> } } ReactDOM.render( <ParentComponent />, document.getElementById('root') ); </script>
瀏覽器中生成的頁面結(jié)構(gòu)如下:
<div id="root"> <div class="parentClass"> <p>parent</p> <input value="jiali"> <!-- 子組件 --> <div class="subClass">sub</div> <p>jiali</p> <!-- /子組件 --> </div> </div>
控制臺輸出:
ChildComponent : defaultProps。孩子的 defaultProps 是
ParentComponent: defaultProps。我的 defaultProps 是
ParentComponent: state。我的 state 是
ParentComponent: render
ChildComponent: state。孩子的 state 是
ChildComponent: render
ChildComponent : componentDidMount
ParentComponent: componentDidMount
Tip:盡管初始 state 和屬性并不使用特定的生命周期方法,但他們?yōu)榻M件提供數(shù)據(jù)方面發(fā)揮了重要作用,所以有必要把它們作為生命周期的一部分
此刻是初次渲染,也就是掛載時(shí),根據(jù)輸出日志我們知道:
- 先 defaultProps 后 state
- 先 render 后 componentDidMount
- 先 render 父組件,后 render 子組件
- 先掛載子組件,后掛載父組件
為何先 render 父組件,又先掛載子組件?
Tip: 其實(shí) vue 也是這樣,請看 這里
筆者推測如下:
入口是這里:
ReactDOM.render( <ParentComponent />, document.getElementById('root') );
先渲染父組件,發(fā)現(xiàn)里面有子組件,接著渲染子組件。
我們將虛擬 Dom
看做真實(shí) Dom
,虛擬 Dom 如何生成真實(shí) Dom 的我們暫時(shí)不去管它,這是 React 做的事情。虛擬 DOM 樹的生成過程前文我們已經(jīng)說了:像一個(gè)小孩不停的問“里面是什么?”,”里面是什么?“,直到搞懂所有的子孫組件,就可以形成一顆完整的樹。
接著往 input 中輸入 1(此刻顯示jiali1
),控制臺輸出:
ParentComponent: render
<ChildComponent/> - shouldComponentUpdate()
ChildComponent: render
ChildComponent: componentDidUpdate
ParentComponent: componentDidUpdate
同樣,先執(zhí)行父組件的 render 再執(zhí)行子組件的 render,先調(diào)用子組件的 componentDidUpdate 再調(diào)用父組件的 componentDidUpdate。可以翻譯成:
- 要渲染父組件
- 里面有子組件需要渲染
- 子組件更新完畢
- 父組件更新完畢
接著往 input 中輸入 2(此刻顯示jiali12
),觸發(fā)子組件 render 里面報(bào)錯,父組件中的 componentDidCatch 被執(zhí)行,實(shí)現(xiàn)降級處理,頁面顯示 降級處理
。
到此這篇關(guān)于React 的數(shù)據(jù)流和生命周期的文章就介紹到這了,更多相關(guān)React 生命周期內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React+TS+IntersectionObserver實(shí)現(xiàn)視頻懶加載和自動播放功能
通過本文的介紹,我們學(xué)習(xí)了如何使用 React + TypeScript 和 IntersectionObserver API 來實(shí)現(xiàn)一個(gè)視頻播放控制組件,該組件具有懶加載功能,只有在用戶滾動頁面且視頻進(jìn)入視口時(shí)才開始下載視頻資源,需要的朋友可以參考下2023-04-04React-View-UI組件庫封裝Loading加載中源碼
這篇文章主要介紹了React-View-UI組件庫封裝Loading加載樣式,主要包括組件介紹,組件源碼及組件測試源碼,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06React在弱網(wǎng)環(huán)境下限制按鈕多次點(diǎn)擊,防止重復(fù)提交問題
這篇文章主要介紹了React在弱網(wǎng)環(huán)境下限制按鈕多次點(diǎn)擊,防止重復(fù)提交問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06用React實(shí)現(xiàn)一個(gè)類 chatGPT 的交互式問答組件的方法詳解
這篇文章主要給大家詳細(xì)介紹如何用React實(shí)現(xiàn)一個(gè)類 chatGPT 的交互式問答組件的方法,文中有詳細(xì)的代碼示例,對我們學(xué)習(xí)有一定的幫助,需要的朋友可以參考下2023-06-06React實(shí)現(xiàn)單向數(shù)據(jù)流的方法
本文主要介紹了React實(shí)現(xiàn)單向數(shù)據(jù)流的方法2023-04-04React?Native?Modal?的封裝與使用實(shí)例詳解
這篇文章主要介紹了React?Native?Modal?的封裝與使用,本文通過實(shí)例代碼圖文相結(jié)合給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-09-09React項(xiàng)目使用ES6解決方案及JSX使用示例詳解
這篇文章主要為大家介紹了React項(xiàng)目使用ES6解決方案及JSX使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12React實(shí)現(xiàn)Excel文件的導(dǎo)出與在線預(yù)覽功能
這篇文章主要為大家詳細(xì)介紹了如何利用?React?18?的強(qiáng)大功能,演示如何使用?React?18?編寫?Excel?文件的導(dǎo)出與在線預(yù)覽功能,需要的小伙伴可以參考下2023-12-12