React的組件化給前端開發帶來了史無前例的體驗,咱們能夠像玩樂高玩具同樣將一個組件堆積拼接起來,就組成了一個完整的UI界面,在加快了開發速度的同時又提升了代碼的可維護性。可是隨着業務功能複雜度提升,業務代碼不得不和生命週期函數糅合到一塊兒。這樣不少重複的業務邏輯代碼很難被抽離出來,爲了快速開發不得不Ctrl+C,若是業務代碼邏輯發生變化時,咱們又不得不一樣時修改多個地方,極大的影響開發效率和可維護性。爲了解決這個業務邏輯複用的問題,React官方也作了不少努力:javascript
React mixin 是經過React.createClass建立組件時使用的,如今主流都是經過ES6方式建立react組件,官方由於mixin很差追蹤變化以及影響性能,因此放棄了對其支持,同時也不推薦咱們使用。這裏就簡單介紹下mixin:html
mixin的原理其實就是將[mixin]裏面的方法合併到組件的prototype上前端
var logMixin = {
alertLog:function(){
alert('alert mixin...')
},
componentDidMount:function(){
console.log('mixin did mount')
}
}
var MixinComponentDemo = React.createClass({
mixins:[logMixin],
componentDidMount:function(){
document.body.addEventListener('click',()=>{
this.alertLog()
})
console.log('component did mount')
}
})
// 打印以下
// component did mount
// mixin did mount
// 點擊頁面
// alert mixin...
複製代碼
能夠看出來mixin
就是將logMixn
的方法合併到MixinComponentDemo
組件中,若是有重名的生命週期函數都會執行(render除外),若是有重名的函數會報錯。可是因爲mixin的問題比較多因此這裏就不展開講。點擊瞭解更多java
組件是 React 中代碼複用的基本單元。但你會發現某些模式並不適合傳統組件。react
例如:咱們有個計時器和日誌記錄組件ios
class LogTimeComponent extends React.Component{
constructor(props){
super(props);
this.state = {
index: 0
}
this.show = 0;
}
componentDidMount(){
this.timer = setInterval(()=>{
this.setState({
index: ++index
})
},1000)
console.log('組件渲染完成----')
}
componentDidUpdate(){
console.log(`我背更新了${++this.show}`)
}
componentWillUnmount(){
clearInterval(this.timer)
console.log('組件即將卸載----')
}
render(){
return(
<div> <span>{`我已經顯示了:${this.state.index}s`}</span> </div>
)
}
}
複製代碼
上面就簡單的實現了簡單的日誌和計時器組件。那麼問題來了假若有三個組件分別是LogComponent
(須要記錄日誌)、SetTimeComponent
(須要記錄時間)、LogTimeShowComponent
(日誌和時間都須要記錄);怎麼處理呢?把上面邏輯 Ctrl+C 而後 Ctrl+V 嗎?若是記錄日誌的文案改變須要每一個組件都修改麼?官方給咱們提供了高階組件(HOC)的解決方案:git
function logTimeHOC(WrappedComponent,options={time:true,log:true}){
return class extends React.Component{
constructor(props){
super(props);
this.state = {
index: 0
}
this.show = 0;
}
componentDidMount(){
options.time&&this.timer = setInterval(()=>{
this.setState({
index: ++index
})
},1000)
options.log&&console.log('組件渲染完成----')
}
componentDidUpdate(){
options.log&&console.log(`我背更新了${++this.show}`)
}
componentWillUnmount(){
this.timer&&clearInterval(this.timer)
options.log&&console.log('組件即將卸載----')
}
render(){
return(<WrappedComponent {...this.state} {...this.props}/>) } } } 複製代碼
logTimeHOC
就是一個函數,接受一個組件返回一個新的組件(其實高階組件就是一個函數)。咱們用這個高階組件來構建咱們上面的三個組件:github
LogComponent
:打印日誌組件redux
class InnerLogComponent extends React.Component{
render(){
return(
<div>我是打印日誌組件</div>
)
}
}
// 使用高階組件`logTimeHOC`包裹下
export default logTimeHOC(InnerLogComponent,{log:true})
複製代碼
SetTimeComponent
:計時組件axios
class InnerSetTimeComponent extends React.Component{
render(){
return(
<div> <div>我是計時組件</div> <span>{`我顯示了${this.props.index}s`}</span> </div>
)
}
}
// 使用高階組件`logTimeHOC`包裹下
export default logTimeHOC(InnerSetTimeComponent,{time:true})
複製代碼
LogTimeShowComponent
:計時+打印日誌組件
class InnerLogTimeShowComponent extends React.Component{
render(){
return(
<div> <div>我是日誌打印+計時組件</div> </div>
)
}
}
// 使用高階組件`logTimeHOC`包裹下
export default logTimeHOC(InnerLogTimeShowComponent)
複製代碼
這樣不只複用了業務邏輯提升了開發效率,同時還方便後期維護。固然上面的案例只是爲了舉例而寫的案例,實際場景咱們須要本身去合理抽取業務邏輯。高階組件雖然很好用可是也有一些自身的缺陷:
上面說了不少,無非就是告訴咱們已經有解決功能複用的方案了。爲啥還要React Hooks這個呢?上面例子能夠看出來,雖然解決了功能複用可是也帶來了其餘問題。由此官方給我帶來React Hooks
,它不只僅解決了功能複用的問題,還讓咱們以函數的方式建立組件,擺脫Class方式建立,從而沒必要在被this的工做方式困惑,沒必要在不一樣生命週期中處理業務。
import React,{ useState, useEffect } from 'react'
function useLogTime(data={log:true,time:true}){
const [count,setCount] = useState(0);
useEffect(()=>{
data.log && console.log('組件渲染完成----')
let timer = null;
if(data.time){
timer = setInterval(()=>{setCount(c=>c+1)},1000)
}
return ()=>{
data.log && console.log('組件即將卸載----')
data.time && clearInterval(timer)
}
},[])
return {count}
}
複製代碼
咱們經過React Hooks
的方式從新改寫了上面日誌時間記錄高階組件。若是不瞭解React Hooks
的基本用法建議先閱讀react hook文檔。若是想深刻了解setInterval在Hook中的表現能夠看這篇從新 Think in Hooks。
假設咱們已經掌握了React Hooks
,那麼我來重寫下上面的三個組件:
LogComponent
:打印日誌組件
export default function LogComponent(){
useLogTime({log:true})
return(
<div>我是打印日誌組件</div>
)
}
複製代碼
SetTimeComponent
:計時組件
export default function SetTimeComponent (){
const {count} = useLogTime({time:true})
return(
<div> <div>我是計時組件</div> <span>{`我顯示了${count}s`}</span> </div>
)
}
複製代碼
LogTimeShowComponent
:計時+打印日誌組件
export default function LogTimeShowComponent (){
const {count} = useLogTime()
return(
<div> <div>我是日誌打印+計時組件</div> <div>{`我顯示了${count}s`}</div> </div>
)
}
複製代碼
咱們用React Hooks
實現的這個三個組件和高階組件一比較是否是發現更加清爽,更加PF。將日誌打印和記錄時間功能抽象出一個useLogTime
自定義Hooks。若是其餘組件須要打印日誌或者記錄時間,只要直接調用useLogTime
這個自定義Hooks就能夠了。是否是有種封裝函數的感受。
若是讓咱們來實現一個React Hooks
咱們如何實現呢?好像毫無頭緒,能夠先看一個簡單的useState
:(這部份內容只是幫咱們更好的理解Hooks工做原理,想了解Hooks最佳實踐能夠直接查看React 生產應用)
function App(){
const [count,setCount] = useState(0);
useEffect(()=>{
console.log(`update--${count}`)
},[count])
return(
<div> <button onClick={()=>setCount(count+1)}> {`當前點擊次數:${count}`} </button> </div>
)
}
複製代碼
上面能夠看出來當調用useState
時;會返回一個變量和一個函數,其參數時返回變量的默認值。咱們先構建以下的useState函數:
function useState(initVal) {
let val = initVal;
function setVal(newVal) {
val = newVal;
render(); // 修改val後 從新渲染頁面
}
return [val, setVal];
}
複製代碼
咱們能夠在代碼中來使用useState
--查看demo;
不出意外當咱們點擊頁面上的按鈕時候,按鈕中數字並不會改變;看控制檯中每次點擊都會輸出0,說明useState是執行了。因爲val是在函數內部被聲明的,每次useState都會從新聲明val從而致使狀態沒法被保存,所以咱們須要將val放到全局做用域聲明。
let val; // 放到全局做用域
function useState(initVal) {
val = val|| initVal; // 判斷val是否存在 存在就使用
function setVal(newVal) {
val = newVal;
render(); // 修改val後 從新渲染頁面
}
return [val, setVal];
}
複製代碼
修改useState
後,點擊按鈕時按鈕就發生改變了--修改後Demo
useEffect
是一個函數,有兩個參數一個是函數,一個是可選參數-數組,根據第二個參數中是否有變化來判斷是否執行第一個參數的函數:
// 實現初版 不考慮第二個參數
function useEffect(fn){
fn();
}
複製代碼
ok!不考慮第二個參數很簡單,其實就是執行下函數--這裏查看Demo(控制檯中能看到useEffect執行了)。可是咱們須要根據第二個參數來判斷是否執行,而不是一直執行。因此咱們還須要有一個判斷邏輯去執行函數。
let watchArr; // 爲了記錄狀態變化 放到全局做用域
function useEffect(fn,watch){
// 判斷是否變化
const hasWatchChange = watchArr?
!watch.every((val,i)=>{ val===watchArr[i] }):true;
if( hasWatchChange ){
fn();
watchArr = watch;
}
}
複製代碼
完成好useEffect
咱們在去測試下 --測試demo
打開測試頁面咱們每次點擊按鈕,控制檯會打印當前更新的count;到目前爲止,咱們模擬實現了useState
和useEffect
能夠正常工做了。不知道你們是否還記得咱們經過全局變量來保證狀態的實時更新;若是組件中要屢次調用,就會發生變量衝突的問題,由於他們共享一個全局變量。如何解決這個問題呢?
useState
useEffect
的問題// 經過數組維護變量
let memoizedState = [];
let currentCursor = 0;
function useState(initVal) {
memoizedState[currentCursor] = memoizedState[currentCursor] || initVal;
function setVal(newVal) {
memoizedState[currentCursor] = newVal;
render();
}
// 返回state 而後 currentCursor+1
return [memoizedState[currentCursor++], setVal];
}
function useEffect(fn, watch) {
const hasWatchChange = memoizedState[currentCursor]
? !watch.every((val, i) => val === memoizedState[currentCursor][i])
: true;
if (hasWatchChange) {
fn();
memoizedState[currentCursor] = watch;
currentCursor++; // 累加 currentCursor
}
}
複製代碼
修改核心是將useState
,useEffect
按照調用的順序放入memoizedState中,每次更新時,按照順序進行取值和判斷邏輯--查看Demo
如上圖咱們根據調用hooks順序,將hooks依次存入數組memoizedState
中,每次存入時都是將當前的currentcursor
做爲數組的下標,將其傳入的值做爲數組的值,而後在累加currentcursor
,因此hooks的狀態值都被存入數組中memoizedState
。
上面狀態更新圖,咱們能夠看到執行setCount(count + 1)
或setData(data + 2)
時,先將舊數組memoizedState
中對應的值取出來從新復值,從而生成新數組memoizedState
。對因而否執行useEffect
經過判斷其第二個參數是否發生變化而決定的。
這裏咱們就知道了爲啥官方文檔介紹:不要在循環,條件或嵌套函數中調用 Hooks, 確保老是在你的 React 函數的最頂層調用他們。由於咱們是根據調用hooks的順序依次將值存入數組中,若是在判斷邏輯循環嵌套中,就有可能致使更新時不能獲取到對應的值,從而致使取值混亂。同時useEffect
第二個參數是數組,也是由於它就是以數組的形式存入的。
固然,react官方不會像咱們這麼粗暴的方式去實現的,想了解官方是如何實現能夠去這裏查看。
在說到React實際工做應用以前,但願你能對React Hooks有作過了解,知道如useState
、useEffect
、useContext
等基本Hooks的使用,以及如何自定義Hooks,若是不瞭解能夠點擊這裏瞭解關於Hook的知識點。
前端頁面免不了要和數據打交道,在Class組件中咱們一般都是在componentDidMount
生命週期中發起數據請求,然而咱們使用Hooks時該如何發送請求呢?
import React,{ useState,useEffect } from 'react';
export default function App() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const result = await axios(
"https://easy-mock.com/mock/5b514734fe14b078aee5b189/example/queryList"
);
setData(result.data); // 賦值獲取後的數據
};
fetchData();
});
return (
<div> {data ? ( <ul> <li>{`id:${data.id}`}</li> <li>{`title:${data.title}`}</li> </ul> ) : null} </div>
);
}
複製代碼
能夠查看Demo,咱們發現頁面報錯。根據咱們瞭解到的知識,若是 useEffect 第二個參數不傳入,致使每次data更新都會執行,這樣就陷入死循環循環了。咱們須要改造下
...
useEffect(() => {
...
},[]);
'''
複製代碼
咱們給第二個參數加上一個[]發現頁面就能夠顯示了,將這個Demo中註釋解除了。,咱們就能夠發現頁面正常顯示了。
咱們一個程序會有多個組件,不少組件都會有請求接口的邏輯,不能每一個須要用到這個邏輯的時候都從新寫或者Ctrl+C。因此咱們須要將這個邏輯抽離出來做爲一個公共的Hooks來調用,那麼咱們就要用到自定義Hooks。
// config => 指望格式
// {
// method: 'post',
// url: '/user/12345',
// data: {
// firstName: 'Fred',
// lastName: 'Flintstone'
// }
// }
function useFetchHook(config){
const [data,setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const result = await axios(config);
setData(result.data)
};
fetchData();
},[]);
return { data }
}
複製代碼
如今咱們就將請求接口的邏輯單獨抽出來了,若是那個組件須要使用能夠直接引入useFetchHook
這裏能夠查看Demo。
上面的useFetchHook
雖然能夠解決咱們請求接口的問題;若是咱們如今是一個分頁接口,每次傳入不一樣的page都會從新請求,因此咱們還須要修改下:
// watch => 指望格式是 []
function useFetchHook(config,watch){
const [data,setData] = useState(null);
useEffect(() => {
...
},
watch?[...watch]:[] // 判斷是否有須要監測的屬性
);
return { data }
}
複製代碼
點擊查看Demo,咱們如今點檢頁面上的按鈕發現頁面的數據戶一直髮生變化,控制檯也會打印,說明咱們更改page時都會從新請求接口,上面的問題就解決了。
上面的useFetchHook
雖然能夠解決大部分狀況,可是一個健全的 接口請求Hooks 還須要告知使用者接口請求狀態的成功、失敗。咱們繼續---
function useFetchHook(config,watch){
// status 標識當前接口請求狀態 0:請求中 1:請求成功 2:請求失敗
const [status,setStatus] = useState(0);
const [data,setData] = useState(null);
useEffect(() => {
try{
...
setStatus(1) // 成功
}catch(err){
setStatus(2) // 失敗
}
},
watch?[...watch]:[] // 判斷是否有須要監測的屬性
);
return { data, status }
}
複製代碼
點擊這裏能夠查看;咱們改造後發現頁面按鈕多了接口狀態,點擊時也會發生改變,爲了測試失敗狀態,咱們將 Chrome - network - Offine 改成 offine狀態,再次點擊發現狀態就變成2(失敗)。
尚未完呢!使用者知道了狀態後能夠作相應的 loading... 操做等等;可是對於接口的報錯咱們也能夠作一個埋點信息或者給一個友善的提示---至於後面怎麼寫我相信你們均可以發揮本身的想象。下面是useFetchHook
完整代碼:
function useFetchHook(config, watch) {
const [data, setData] = useState(null);
const [status, setStatus] = useState(0);
useEffect(
() => {
const fetchData = async () => {
try {
const result = await axios(config);
setData(result.data);
setStatus(1);
} catch (err) {
setStatus(2);
}
};
fetchData();
},
watch ? [watch] : []
);
return { data, status };
}
複製代碼
class App extends Component{
render() {
return
<div> <Button onClick={ () => { console.log('do something'); }} /> </div>; } } 複製代碼
上面App組件若是props發生改變時,就會從新渲染組件。若是這個修改並不涉及到Button組件,可是因爲每次render的時候都會產生新的onClick函數,react就認爲其發生了改變,從而產生了沒必要要的渲染而引發性能浪費。
class App extends Component{
constructor(){
super();
this.buttonClick = this.buttonClick.bind(this);
}
render() {
return
<div> <Button onClick={ this.buttonClick } /> </div>; } } 複製代碼
在類組件中咱們能夠直接將函數綁定到this對象上。在Hooks組件中怎麼解決呢?
function App(){
const buttonClick = useCallback(
() => { console.log('do something'),[]
)
return(
<div> <Button onClick={ buttonClick } /> </div> ) } 複製代碼
如上直接用useCallback
生成一個記憶函數,這樣更新時就不會發生渲染了。在react Hooks 中 還有一個useMemo
也能實現一樣的效果。
React是從上而下的單向數據流,父子組件之間信息傳遞能夠經過Props實現,兄弟組件的信息傳遞咱們能夠將Props提高到共同的父級實現信息傳遞,若是組件層級嵌套過深,對開發者來講是十分痛苦的。因此社區基於redux產生了react-redux工具,固然咱們這裏對react-redux作講解,而是提供一種新的解決方案。
// 建立Context
const AppContext = React.createContext();
const AppDispatch = (state, action) => {
switch (action.type) {
case "count.add":
return { ...state, count: state.count + 1 };
case "count.reduce":
return { ...state, count: state.count - 1 };
case "color":
return { ...state, color: colorArr[getRandom()] };
default:
return state;
}
};
// 建立Provider
const AppProvider = props => {
let [state, dispatch] = useReducer(AppDispatch, context);
return (
<AppContext.Provider value={{ state, dispatch }}> {props.children} </AppContext.Provider> ); }; // ... function Demo3() { // 使用 Context const { state, dispatch } = useContext(AppContext); return ( <div className="demo" style={{ backgroundColor: state.color }} onClick={() => { dispatch({ type: "count.add" }); dispatch({ type: "color" }); }} > <div className="font">{state.count}</div> </div> ); } // ... // 將 AppProvider 放到根組件 ReactDOM.render( <AppProvider> <App /> </AppProvider>, rootElement ); 複製代碼
在開始封裝經常使用Hooks以前插一個題外話,咱們在開發中時,不可能都是新項目;對於那些老項目(react已經升級到16.8.x)咱們應該如何去使用Hooks呢?其實很簡單咱們能夠開發一些經常使用的hooks,當咱們老項目有新的功能咱們徹底能夠用Hooks去開發,若是對老的組件進行修改時咱們就能夠考慮給老組件上Hooks;不建議一上來就進行大改。隨着咱們的經常使用Hooks組件庫的豐富,後期改起來也會很是快。
在使用Hooks時不免少不了一些經常使用的Hooks,若是能夠將這些經常使用的Hooks封裝起來豈不是美滋滋!
首先能夠建立以下目錄結構:
index.js文件
import useInterval from './useInterval'
// ...
export{
useInterval
// ...
}
複製代碼
lib中存放經常使用Hooks, 如實現一個useInterval:爲啥咱們須要一個useInterval的自定義Hooks呢?
在程序中直接使用 setInterval
function App(){
const [count,setCount] = useState(0);
useEffect(()=>{
console.log(count)
setInterval(()=>{
setCount(count+1)
})
})
return <p>{count}</p>
}
複製代碼
上面代碼直接運行咱們會發現頁面上的 count 越加越快,是因爲 count 每次發生改變都致使定時器觸發。因此須要每次在清除下定時器:
function App(){
const [count,setCount] = useState(0);
useEffect(()=>{
console.log(count)
const timer = setInterval(()=>{
setCount(count+1)
})
// 清除反作用
return ()=>{ clearInterval(timer) }
})
return <p>{count}</p>
}
複製代碼
改動代碼後頁面好像能夠正常顯示了,咱們打開控制檯能夠看到一直會打印 count ,這樣對於性能來將無疑是一種浪費,咱們只須要執行一次就能夠了,在改下代碼
function App(){
const [count,setCount] = useState(0);
useEffect(()=>{
console.log(count)
const timer = setInterval(()=>{
setCount(count+1)
})
return ()=>{ clearInterval(timer) }
},[]) // 添加第二個參數
return <p>{count}</p>
}
複製代碼
在看頁面,發現控制檯好像是隻打印一次了,可是頁面上的 count 以及不發生改變了,這不是咱們想要的,還須要改變下:
function App(){
const [count,setCount] = useState(0);
useEffect(()=>{
console.log(count)
const timer = setInterval(()=>{
setCount(count+1)
})
return ()=>{ clearInterval(timer) }
},[count]) // 添加 count 變量
return <p>{count}</p>
}
複製代碼
Ok!如今好像解決了上面的問題了,可是這個只是一個定時器累加的任務並且只涉及到一個變量,若是是定時執行其餘任務,同時有多個變量,那麼豈不是又要修改。因此爲了解決這個問題咱們迫切須要一個useInterval自定義鉤子。
function useInterval(callback,time=300){
const intervalFn = useRef(); // 1
useEffect(()=>{
intervalFn.current = callback; // 2
})
useEffect(()=>{
const timer = setInterval(()=>{
intervalFn.current()
},time)
return ()=>{ clearInterval(timer) }
},[time]) // 3
}
複製代碼
簡單介紹下useInterval鉤子: 1.經過useRef建立一個對象;2.將須要執行的定時任務儲存在這個對象上;3.將time做爲第二個參數是爲了當咱們動態改變定時任務時,能太重新執行定時器。
開發中使用useInterval
以下:
useInterval(() => {
// you code
}, 1000);
複製代碼
是否是很簡單有很方便,如今將useInterval
放到lib文件夾中,再在index.js文件中導出一下,其餘地方要用的時候直接import就能夠了。
開放思惟
問題:作一個useImgLazy的 hooks 函數。
爲提升網頁的性能咱們通常都會網頁上圖片資源作一些優化,懶加載就是一種方案,useImgLazy就是實現懶加載的 Hooks
// 判斷是否在視口裏面
function isInWindow(el){
const bound = el.getBoundingClientRect();
const clientHeight = window.innerHeight;
return bound.top <= clientHeight + 100;
}
// 加載圖片真實連接
function loadImg(el){
if(!el.src){
const source = el.getAttribute('data-sourceSrc');
el.src = source;
}
}
// 加載圖片
function checkImgs(className){
const imgs = document.querySelectorAll(`img.${className}`);
Array.from(imgs).forEach(el =>{
if (isInWindow(el)){
loadImg(el);
}
})
}
function useImgLazy(className){
useEffect(()=>{
window.addEventListener('scroll',()=>{
checkImgs(className)
});
checkImgs(className);
return ()=>{
window.removeEventListener('scroll')
}
},[])
}
複製代碼
上面代碼邏輯就是 經過getBoundingClientRect
獲取圖片元素的位置,從而判斷是否顯示圖片真實地址,用useEffect
模擬頁面加載成功(onload事件)同時監聽scroll
事件。
在須要使用圖片懶加載的項目中使用:
function App(){
// ...
useImgLazy('lazy-img')
// ...
return (
<div> // ... <img className='lazy-img' data-sourceSrc='真實圖片地址'/> </div> ) } 複製代碼
以上useImgLazy
代碼我是寫這篇文章時忽然誕生的一個想法,沒有驗證,若是哪位同窗驗證後有問題還請告知我在這裏是反饋問題,如生產上使用產生問題我一律不負責。
我相信你們看了這篇文章必定會蠢蠢欲動,建立一個自定義 Hooks 。點擊這裏大家使用過哪些自定義Hooks函數你能夠分享、學習其餘人是如何自定義有趣的Hooks。
這裏能夠分享Hooks的最佳實踐幫助咱們更快的使用React Hooks說說Hooks中的一些最佳實踐##