首先簡單談談react和vue的區別:
如果你寫過vue,會發現組件的視圖指令已編譯為修改視圖的函數存放在綁定的state里的屬性里,所以能夠做到靶向修改,而react會以組件為根,重新渲染整個組件子樹。所以應避免這些不必要的render。
0、setState和shouldComponentUpdate
setState特性:
1、setState是異步操作函數,很多時候,我們需要想要的state狀態更新完成后再進行某些操作。此時,我們可以選擇在componentWillUpdate生命周期或者componentDidUpdate生命周期的回調函數去執行我們的操作。雖然也可以達到預期效果,但是這樣做不是最佳方法,代碼變得破碎,可讀性也不好。
因此,此時我們就需要保證setState的同步更新。
有兩種方案:
通過回調函數
通過async/await來異步轉同步
2、setState會造成不必要的渲染
3、setState並不能很有效的管理所有的組件狀態
默認情況下僅對React事件處理程序內的更新進行批處理。
如:
class Container extends React.Component { constructor(props) { super(props); this.state = { a: false, b: false }; } render() { return <Button onClick={this.handleClick}/> } handleClick = () => { this.setState({ a: true }); // setState 是不保證同步的所以打印的state依然是舊的,可以在setState中寫回調函數得到改變后的state console.log(this.state) this.setState({ b: true }, ( )=> { console.log(this.state) }); } } // 不會出現只改變b的情況 class SuperContainer extends React.Component { constructor(props) { super(props); this.state = { a: false }; } render() { return <Container setParentState={this.setState.bind(this)}/> } } class Container extends React.Component { constructor(props) { super(props); this.state = { b: false }; } render() { return <Button onClick={this.handleClick}/> } handleClick = () => { this.props.setParentState({ a: true }); this.setState({ b: true }); } }
在類組件中,如果一個li發生變化,那么整個ul都會重新渲染。所以子組件變化,會導致父組件整個發生重新渲染。調用 setState 方法總是會觸發 render 方法從而進行 vdom re-render 相關邏輯,哪怕實際上你沒有更改到 Component.state。
為了避免這種性能上的浪費,React 提供了一個 shouldComponentUpdate 來控制觸發 vdom re-render 邏輯的條件。根據 shouldComponentUpdate() 的返回值,判斷 React 組件的輸出是否受當前 state 或 props 更改的影響。默認行為是 state 每次發生變化組件都會重新渲染。大部分情況下,你應該遵循默認行為。
注:后調用的 setState() 將覆蓋同一周期內先調用 setState 的值,因此商品數僅增加一次。
1、函數組件:
特點:沒有自身的狀態、無生命周期。
//沒有自身的狀態,相同的props輸入必然會獲得完全相同的組件展示。不需要關心組件的一些生命周期函數和渲染的鈎子更簡潔。
import React, { Component } from "react"; const Button = ({ day }) => { return ( <div> <button className="btn btn-warning">我是 {day}</button> </div> ); }; class Greeting extends Component { render() { return <Button day="純函數組件"></Button>; } } export default Greeting;
使用場景:
一些只渲染一次的純展示的場景,比如一個列表、表格等。
2、純組件(pureComponent):
當 props 或 state 發生變化時,shouldComponentUpdate() 會在渲染執行之前被調用。返回值默認為 true。除非 shouldComponentUpdate() 返回 false,否則 setState() 將始終執行重新渲染操作。首次渲染或使用 forceUpdate() 時不會調用該方法。所以pureComponent就相當於是一個寫好shouldComponentUpdate的類組件。
和類組件的區別:自帶渲染性能優化(shouldComponentUpdate)
但是:React.PureComponent 通過props和state的淺對比來實現 shouldComponentUpate()
如果對象包含復雜的數據結構,它可能會因深層的數據不一致而產生錯誤的否定判斷(表現為對象深層的數據已改變視圖卻沒有更新)
解決方法:
注:但是無論是普通組件還是pure純組件,發生狀態變化時候,如果state不是一個普通對象,shouldComponentUpdate就無法攔截復雜數據結構的數據變化,因為地址沒有變,比如push數據到一個數組,新的數組和舊的數組指向同一個地址,無法比較是否變化,這時就要借助immutable。
**immutable把變化的節點加上原來的老節點,返回一個指向新地址的新樹(新對象)。類似深拷貝。**如之前:{a: 1, b: 2},之后:{a: 1, b: 3},這樣shouldComponentUpdate就可以繼續比較這兩個對象了。
//demo1: PureComponent的自動為我們添加的shouldComponentUpate函數 import React, { PureComponent } from "react"; class CounterButton extends PureComponent { constructor(props) { super(props); this.state = { count: 1 }; } render() { return ( <button className="btn btn-info" onClick={() => this.setState(state => ({ count: state.count + 1 }))} > Count: {this.state.count} </button> ); } } export default CounterButton;
// pure組件只是對類組件的升級,沒有解決掉復雜對象無法比較的問題,還是要引入immutable。 // demo2 import React from "react"; const { List } = require("immutable"); // let data = []; //創建不可變的對象 let data = List(["start"]); class CounterButton extends React.Component { constructor(props) { super(props); this.state = { count: data }; } render() { return ( <button color={this.props.color} onClick={() => { // data.push(Math.random()); this.setState(state => ({ count: this.state.count.push(Math.random()) })); }} > {/* Count: {this.state.count.length} */} Count: {this.state.count.size} </button> ); } } export default CounterButton;
應用場景:
如果組件的內部狀態改變的很頻繁,並且不想染外部一起渲染,就使用pureComponent,它內部會自動計算並攔截狀態的變化,不用自己去維護shouldComponentUpdate函數了。
3、高階組件
用於對普通組件進行包裝,並擴展功能。
高階函數:
// demo1 // hoc為高階函數 function hello (){ console.log("?我是高階組件") } function hoc(fn){ // 關鍵是要返回一個函數,不能執行 return ()=>{ console.log("first"); fn(); console.log("end"); } } const hocresult = hoc(hello); hocresult(); // demo2 function welcome(username) { console.log('welcome ' + username); } function goodbey(username) { console.log('goodbey ' + username); } //高階函數 function wrapWithUsername(wrappedFunc) { let newFunc = () => { let username = localStorage.getItem('username'); wrappedFunc(username); }; return newFunc; } // eslint-disable-next-line no-func-assign 一般不能重新定義函數 welcome = wrapWithUsername(welcome); // eslint-disable-next-line no-func-assign goodbey = wrapWithUsername(goodbey); welcome(); goodbey();
高階組件:
用函數包裹,函數參數接受一個普通組件,並最終返回一個新組件,這個返回的新組件就叫做高階組件
/
/=========高階組件的實戰代碼demo1===== import {Component} from 'react' function HOCFactoryFactory(...params){ return function HOCFactory(WrappedComponent){ return class HOC extends Component{ render(){ return <WrappedComponent {...this.props} /> } } } } //使用方式1,注入 @HOCFactoryFactory({}) class WrappedComponent extends React.Component{} //使用方式2 HOCFactoryFactory({})(WrappedComponent) //=========高階組件的實戰代碼demo2===== //##高階組件之后的代碼 //注值 localStorage.username = "老袁" const wrapWithUsername = WrappedComponent => { class NewComponent extends Component { constructor() { super(); this.state = { username: "" }; } componentWillMount() { let username = localStorage.getItem("username"); this.setState({ username: username }); } render() // 這里重新包裝了傳入的普通組件,並交給新生成的高階組件去渲染 return <WrappedComponent username={this.state.username} />; } } return NewComponent; }; class Welcome extends Component { render() { return <div className="text-warning">welcome {this.props.username}</div>; } } // 把Welcome升級成高階組件 Welcome = wrapWithUsername(Welcome); class Goodbye extends Component { render() { return <div className="text-info">goodbye {this.props.username}</div>; } } //升級高階組件 Goodbye = wrapWithUsername(Goodbye); class Greeting extends Component { render() { return ( <> <Welcome /> <Goodbye /> </> ); } } export default Greeting;
應用場景:redux
4、插槽(Portals組件)
Portals 提供了一個頂級的方法,使得我們有能力把一個子組件渲染到父組件 DOM 層級以外的 DOM 節點上。
使用:ReactDOM.createPortal()方法
import React from 'react' import ReactDOM from 'react-dom' import "./component.css" //組件插槽 const portalElm = document.createElement('div'); portalElm.className="txtcenter" document.body.appendChild(portalElm) class App extends React.Component { state = { show: true, } handleClick = () => { this.setState({ show: !this.state.show, }) } render() { return ( <div> <button className="btn btn-primary" onClick={this.handleClick}>動態展現Portal組件</button> {this.state.show ? ( <div>{ReactDOM.createPortal(<span>Portal組件</span>, portalElm)}</div> ) : null} </div> ) } } export default App
應用場景:彈窗
5、處理異步數據或組件(suspense)
react支持用suspense去處理異步數據,不需要async/await。 //新增了render 新的返回類型:fragments 和 strings import React, { Suspense, lazy } from "react"; import "./suspense.css"; // import { useFetch } from "react-hooks-fetch"; // console.log("異步加載數據", useFetch); //動態加載組件 const LazyComp = lazy(() => import("./lazy")); function fetchApi() { const promise = new Promise(resolve => { setTimeout(() => { resolve("Data resolved"); }, 3000); }); return promise; } //創建Fetcher var cached = {}; const createFetcher = promiseTask => { let ref = cached; return () => { const task = promiseTask(); task.then(res => { ref = res; }); console.log("?--ref",ref); console.log("?--cached",cached); if (ref === cached) { throw task; } //得到結果輸出 console.log("?",ref); return ref; }; }; const requestData = createFetcher(fetchApi); function SuspenseComp() { // const {error,data} = useFetch("a.php"); // console.log("數據?",data) // if (error) return <span>出錯了/(ㄒoㄒ)/~~</span>; // if (!data) return null; // return <span>RemoteData:{data.title}</span>; const data = requestData(); return <p className="text-warning">{data}</p>; } export default () => ( <Suspense fallback={<div className="text-danger">loading<i></i></div>}> <SuspenseComp /> <LazyComp /> </Suspense> );
6、memo組件
React.memo() 是高階函數能將函數組件轉換成類似於React.PureComponent組件。
//React.memo() 是高階函數能將函數組件轉換成類似於React.PureComponent組件 import React, { memo, Component } from "react"; function Child({ seconds }) { console.log("I am rendering"); return <div>Memo組件 seconds->{seconds} </div>; } function areEqual(prevProps, nextProps) { if (prevProps.seconds === nextProps.seconds) { return true; } else { return false; } } // const RocketComponent = props => <div>my rocket component. {props.fuel}!</div>; // 創建一個只在prop改變時發生渲染的版本 // const MemoizedRocketComponent = memo(RocketComponent); // const memocom = () => { // return memo(Child, areEqual); // }; const DemoComponent = memo(Child, areEqual); class Greeting extends Component { render() { return <DemoComponent seconds="20" />; } } export default Greeting; // function Child({seconds}){ // console.log('I am rendering'); // return ( // <div>I am update every {seconds} seconds</div> // ) // }; // export default React.memo(Child)
7、context API
Context 主要是解決props向多層嵌套的子組件傳遞的問題,原理是定義了一個全局對象。
子節點要用Consumer包裹
//Context 主要是解決props向多層嵌套的子組件傳遞的問題,原理是定義了一個全局對象 import React from "react"; import PropTypes from "prop-types"; const { Provider, Consumer } = React.createContext("default"); class Parent extends React.Component { state = { yideng: "普通字符串?", newContext: "京程一燈" }; // getChildContext() { // return { value: this.state.newContext, yideng: this.state.yideng }; // } render() { // <React.Fragment> == <> return ( <> <div> <label className="text-warning">父節點=> newContext:</label> <input type="text" value={this.state.newContext} onChange={e => this.setState({ newContext: e.target.value })} /> </div> <div> <label className="text-info">父節點=>yideng:</label> <input type="text" value={this.state.yideng} onChange={e => this.setState({ yideng: e.target.value })} /> </div> {/* {this.props.children} */} <Provider value={{ newContext: this.state.newContext, yideng: "普通字符串?" }} > {this.props.children} </Provider> </> ); } } function Child(props, context) { return ( <Consumer> {value => ( <p className="text-warning">子節點=> newContext: {value.newContext}</p> )} </Consumer> ); } class Child2 extends React.Component { static contextTypes = { yideng: PropTypes.string }; render() { // return <p>字符串a: {this.context.yideng}</p>; return ( <Consumer> {value => <p className="text-info">子節點=> yideng: {value.yideng}</p>} </Consumer> ); } } Child.contextTypes = { value: PropTypes.string }; // Parent.childContextTypes = { // value: PropTypes.string, // yideng: PropTypes.string // }; export default () => ( <Parent> <Child /> <Child2 /> </Parent> );
8、ref
react采用了新的ref方式,使用React.createRef()
forwardRef,省去了ref復雜程度。
// demo1 import React from 'react' const TargetComponent = React.forwardRef((props, ref) => ( <input type="text" ref={ref} /> )) export default class Comp extends React.Component { constructor() { super() this.ref = React.createRef() } componentDidMount() { this.ref.current.value = '轉發ref成功?' } render() { return <TargetComponent ref={this.ref} /> } } // demo2 import React from 'react' export default class RefDemo extends React.Component { constructor() { super() this.objRef = React.createRef(); } componentDidMount() { setTimeout(() => { this.refs.stringRef.textContent = 'string ref got' this.methodRef.textContent = 'method ref got' this.objRef.current.textContent = 'obj ref got' }, 30) } render() { return ( <> <p className="text-success" ref="stringRef">span1</p> <p ref={ele => (this.methodRef = ele)}>span3</p> <p ref={this.objRef}>span3</p> </>)}}
8、error組件
增加了componentDidCatch生命周期,父組件捕捉錯誤
import React, { Component } from "react"; class ErrorBoundary extends Component { constructor(props) { super(props); this.state = { hasError: false }; } // 捕捉錯誤和錯誤上報程序庫一起使用 componentDidCatch(err, info) { this.setState({ hasError: true }); } render() { if (this.state.hasError) { return <div>Something went wrong!</div>; } return this.props.children; } } class Profile extends Component { constructor(props) { super(props); this.state = { }; } render() { return <span>用戶名:{this.state.user.push(1)}</span> } } class Greeting extends Component { render() { return ( <ErrorBoundary> <Profile/> </ErrorBoundary> ); } } export default Greeting;
9.1 shouldComponentUpdate若返回false不會重新渲染渲染
9.2 可以使用this.setState()的生命周期:
componentWillmount()
componentWillReceiveProps()
componentDidMount()
componentDidUpdate()
9.3 react16廢棄的生命周期:
componentWillMount
componentWillReceiveProps
componentWillUpdate
新增生命周期:
getSnapshotBeforeUpdate
getDerivedStateFromProps代替componentWillReceiveProps
getDerivedStateFromProps 會在調用 render 方法之前調用,並且在初始掛載及后續更新時都會被調用。它應返回一個對象來更新 state,如果返回 null 則不更新任何內容。
getSnapshotBeforeUpdate() 在最近一次渲染輸出(提交到 DOM 節點)之前調用。它使得組件能在發生更改之前從 DOM 中捕獲一些信息(例如,滾動位置)。此生命周期的任何返回值將作為參數傳遞給 componentDidUpdate()。
用途:因為 “render” 階段生命周期(如 render)和 “commit” 階段生命周期(如 getSnapshotBeforeUpdate 和 componentDidUpdate)之間可能存在延遲。
10、hooks
hooks的誕生是為了解決react開發中遇到的問題
1、this的指向問題。
2、生命周期。
3、原本的函數組件是空的,給函數組件擴展功能。
/** * 1.只能在函數組件中使用hooks * 2.函數組件業務變更無需修改成class組件 * 3.告別了繁雜的this和難以記憶的生命周期 * 4.合並的生命周期componentDidMount、componentDidUpdate、和 componentWillUnmount * 5.包裝自己的hooks 是基於純命令式的api * 6.更好的完成狀態之間的共享 解決原來class組件內部封裝問題。也解決了高階組件和函數組件的嵌套過深 * 7.useReducer集成redux * 8.useEffect接受臟操作等到react更新了DOM之后,它再依次執行我們定義的副作用函數。這里就是一個io且是異步的 */ /** * 以上我們學習過的方法均提供了ref * useState 返回有狀態值,以及更新這個狀態值的函數 * useEffect 接受包含命令式,可能有副作用代碼的函數。 * useContext 接受上下文對象(從React.createContext返回的值)並返回當前上下文值, * useReducer useState的替代方案。接受類型為(state,action) => newState的reducer,並返回與dispatch方法配對的當前狀態。 * useCallback 返回一個回憶的memoized版本,該版本僅在其中一個輸入發生更改時才會更改。純函數的輸入輸出確定性 * useMemo 純的一個記憶函數 * useRef 返回一個可變的ref對象,其.current屬性被初始化為傳遞的參數 * useImperativeMethods 自定義使用ref時公開給父組件的實例值 * useMutationEffect 更新兄弟組件之前,它在React執行其DOM改變的同一階段同步觸發 * useLayoutEffect DOM改變后同步觸發。使用它來從DOM讀取布局並同步重新渲染 */ import React, { useState, useEffect } from "react"; const useCount = (initialCount = 0) => { const [count, setCount] = useState(initialCount); return [count, () => setCount(count + 1), () => setCount(count - 1)]; }; export default () => { const [count, increment, decrement] = useCount(1); //首次渲染完成 // componentDidMount() { // document.title = `You clicked ${this.state.count} times`; // } //更新渲染完成 // componentDidUpdate() { // document.title = `You clicked ${this.state.count} times`; // } //組件卸載階段 == return function useEffect每次組件變更均執行 // componentWillUnmount(){ // } useEffect(() => { console.log("component update"); document.title = `標題-${count} times`; return () => { console.log("unbind"); }; }, [count]); return ( <> <input type="button" value="增加count" onClick={increment} /> <span>當前count: {count}</span> <input type="button" value="減少count" onClick={decrement} /> </> ); };
11、受控組件和非受控組件
在大多數情況下,我們推薦使用 受控組件 來處理表單數據。在一個受控組件中,表單數據是由 React 組件來管理的。另一種替代方案是使用非受控組件,這時表單數據將交由 DOM 節點來處理。
受控組件:
在 HTML 中,表單元素(如<input>、 <textarea> 和 <select>)之類的表單元素通常自己維護 state,並根據用戶輸入進行更新。輸入的表單數據保存在組件的state屬性中,並且只能通過使用 setState()來更新。使 React 的 state 成為“唯一數據源”。渲染表單的 React 組件還控制着用戶輸入過程中表單發生的操作。被 React 以這種方式控制取值的表單輸入元素就叫做“受控組件”。
class NameForm extends React.Component { constructor(props) { super(props); this.state = {value: ''}; this.handleChange = this.handleChange.bind(this); this.handleSubmit = this.handleSubmit.bind(this); } handleChange(event) { this.setState({value: event.target.value}); } handleSubmit(event) { alert('提交的名字: ' + this.state.value); event.preventDefault(); } render() { return ( <form onSubmit={this.handleSubmit}> <label> 名字: <input type="text" value={this.state.value} onChange={this.handleChange} /> </label> <input type="submit" value="提交" /> </form> ); } }
非受控組件:
動態數據交給dom節點處理,借助ref讀取dom數據。
兩者的選取:
若需要表單即時驗證,選擇受控組件,不需要即時驗證,提交時驗證,則可以選擇非受控組件。