1绷耍、值捕獲 造成數(shù)據(jù)不一致 異常
export default () => {
const [age, setAge] = useState(0);
const onClick = async () => {
setAge(age + 1)
let data = await request();
console.log(data);
}
const request = () => {
return new Promise(async (resolve, reject) => {
setTimeout(() => {
resolve({ age: age }) // ........@1
}, 1000);
})
}
return (
<div>
<div>{age}</div>
<button onClick={onClick}>
+
</button>
</div>
)
}
閉包內(nèi)部變量為值捕獲。
如例子忽刽,點擊按鈕天揖,設(shè)置age為1,調(diào)用request方法缔恳,內(nèi)部@1處block生成宝剖,捕獲此時age的值為0(setAge為異步方法,此時age還沒有變化歉甚,依舊為0)万细,1s后方法返回age的值依舊是0,但此時age真正的值為1纸泄,造成數(shù)據(jù)不一致
函數(shù)組建每一次渲染時赖钞,內(nèi)部會從上到下依次執(zhí)行代碼,重新生成上下文環(huán)境聘裁,普通函數(shù)也就重新生成雪营,重新捕獲上下文變量。useEffect衡便、useMemo献起、useCallback等這些hook都自帶閉包洋访,當(dāng)依賴設(shè)置不當(dāng)時,函數(shù)組建重復(fù)渲染時不會更新閉包谴餐,導(dǎo)致內(nèi)部捕獲的上下文還是上一次的姻政,從而數(shù)據(jù)出錯。hook函數(shù)的依賴很重要岂嗓,例如useMemo汁展,當(dāng)依賴不變時,組建重新渲染其內(nèi)部返回的子組件使用緩存上一次的厌殉,不會刷新食绿,性能得意提升。
2公罕、怎么存儲值
class時器紧,對組件內(nèi)部的值可以使用this.xxx和this.state.xxx存儲得问,對不需要更新頁面的屬性采用前者
hook怎么存儲不需要更新頁面的屬性慕淡?
let xxx2 = undefined;// ....@2
export default () => {
let xxx = undefined; // ....@1
const xxx3 = useRef(0) // ....@3
const [info] = useState({});
const [age, setAge] = useState(0);
const onClick = async () => {
setAge(age + 1)
xxx = 10;
xxx2 = 20;
xxx3.current = 30;
info.xxx4 = 40;
}
console.log(xxx);
console.log(xxx2);
console.log(xxx3.current);
console.log(info.xxx4);
return (
<div>
<div>{age}</div>
<button onClick={onClick}>
+
</button>
</div>
)
}
如上@2,@3,@4可以滿足需求摩桶,但是@2屬于全局變量,不在組件內(nèi)部帽揪,不符合設(shè)計原則硝清,@3使用ref方式存儲不需要更新頁面的屬性,@4使用對象info转晰,使用直接賦值的方式將值保存在對象中芦拿,也不會刷新頁面,后兩種滿足需求
3查邢、useEffect會在組件初次渲染時調(diào)用一次蔗崎,如何忽略這次?
有種場景扰藕,如dva中一個action缓苛,使modal中數(shù)據(jù)改變,要求一旦發(fā)生數(shù)據(jù)改變則執(zhí)行某個方法邓深。
在class中可以在componentWillReceiveProps中判斷數(shù)據(jù)有沒發(fā)生改變未桥,那么在hook中嘞。
我采取的是useEffect芥备,在此方法中過濾首次執(zhí)行冬耿,代碼封裝后如下
export const useEffect_ignoreFirst = (effect, deps) => {
const initializedRef = useRef(false)
useEffect(() => {
if (initializedRef.current) {
let result = effect();
if (result) return result;
} else {
initializedRef.current = true;
}
}, deps)
}
方法使用跟useEffect完全一致,只需要更改名字為useEffect_ignoreFirst即可萌壳,該方法會將組建首次渲染回調(diào)的useEffect過濾亦镶,再次渲染時候回調(diào)正常進行
4日月、this.setState異步回調(diào)問題
在class中,有時候希望調(diào)用setState后馬上拿到更新后的罪行state值做些事情缤骨,此時可以
this.setState({
name:'jack'
}, () => {
console.log(this.state.name);
})
在回調(diào)用拿到state的最新值
在hook中useState沒有回調(diào)山孔,無法即時獲取最新的state值,目前想到兩種方式代替
一種是傳值荷憋,將最新的要更改的state值保存下來台颠,在通過值傳遞方式使用
const [name, setName] = useState('');
const onClick = async () => {
let newName = 'jack'
setName(newName);
request(newName)
}
const request = (name) => {
console.log(name);
}
這樣有個問題就是當(dāng)請求鏈條很長時,這個參數(shù)需要在很多方法之間傳遞不太方便勒庄,也顯得多余
還有個方法串前,就是使用useEffect,當(dāng)某個state一旦改變就觸發(fā)執(zhí)行所需方法
const [name, setName] = useState('');
const onClick = async () => {
setName('jack');
}
useEffect_ignoreFirst(() => {
request()
}, [name])
const request = () => {
console.log(name);
}
這里需要使用useEffect_ignoreFirst過濾掉首次渲染導(dǎo)致的useEffect回調(diào)
補充:還有種方式实蔽,
let [name, setName] = useState('');
const onClick = async () => {
name = 'jack'
setName(name);
request()
}
const request = () => {
console.log(name);
}
5荡碾、useEffect監(jiān)聽對象改變
默認(rèn)useEffect是采用的淺比較
const [info, setInfo] = useState({ age: 0 });
const onClick = () => {
setInfo({ age: 0 })
}
useEffect(
() => {
console.log(info);
},
[info]
);
只要調(diào)用了setInfo,盡管內(nèi)部的屬性完全沒有發(fā)生變化局装,但是因為淺比較是比較的對象地址坛吁,判斷為不相等,會導(dǎo)致頁面刷新铐尚,useEffect重新執(zhí)行拨脉。
對對象類型,大多情況需要比較的是那部屬性是否變化宣增,而不是地址是否變化玫膀,寫了如下自定義對象比較類型的hook
export const useEffect_customCompare = (effect, deps, isEqual = (o1, o2) => o1 === o2) => {
let indexRef = useRef(0);
let depsRef = useRef(deps);
if (!isEqual(deps, depsRef.current)) {
indexRef.current++;
}
depsRef.current = deps;
return useEffect(effect, [indexRef.current]);
}
通過自定義比較方法isEqual,判斷前后deps是否發(fā)生變化爹脾,從而執(zhí)行useEffect帖旨。使用
const [info, setInfo] = useState({ age: 0 });
const onClick = () => {
setInfo({ age: 1 })
}
useEffect_customCompare_ignoreFirst(
() => {
console.log(info);
},
[info],
(deps1, deps2) => deps1[0].age == deps2[0].age
);
上例中只比較info的age屬性是否發(fā)生了變化,而執(zhí)行useEffect灵妨。
useEffect自定義比較對象解阅,又忽略首次組建渲染導(dǎo)致的調(diào)用,結(jié)合上面的useEffect_ignoreFirst如下
export const useEffect_customCompare_ignoreFirst = (effect, deps, isEqual = (o1, o2) => o1 === o2) => {
let indexRef = useRef(0);
let depsRef = useRef(deps);
if (!isEqual(deps, depsRef.current)) {
indexRef.current++;
}
depsRef.current = deps;
return useEffect_ignoreFirst(effect, [indexRef.current]);
}
6、Hook的刷新控制
class中使用shouldComponentUpate方法對比props和state控制自身組件的刷新時機泌霍,優(yōu)化新能货抄。
hook中也有相對的memo,但是用法有區(qū)別
const Child = () => {
console.log('子組件刷新');
return (
<div>
<Button style={{ marginTop: 100 }}>
---
</Button>
</div>
)
}
const MemoChild = memo(Child)
export default () => {
const [info, setInfo] = useState({ age: 0, height: 0 });
const [name, setName] = useState('');
const onClick = () => {
setInfo({ ...info, height: info.height + 1 })
}
const callback = () => {
console.log(name)
}
console.log('父組件刷新');
return (
<div>
<Button onClick={onClick}>
+++
</Button>
{/* <Child /> */}
{/* <Child name={name} /> */}
{/* <MemoChild /> */}
{/* <MemoChild info={info} /> */}
{/* <MemoChild callback={callback} /> */}
{/* <MemoChild callback={useCallback(callback, [name])} /> */}
{/* <MemoChild info={useMemo(() => info, [info.height])} /> */}
</div>
)
}
上述對子組件Child的幾種寫法烹吵,當(dāng)父組件刷新時碉熄,子組件的刷新情況測試如下
1、父組建一旦屬性肋拔,則子組建也會同時刷新
2锈津、同1。父組建刷新凉蜂,函數(shù)內(nèi)部所有的state變量琼梆,函數(shù)方法都會重新生成性誉。在這里,刷新后name變量發(fā)生變化茎杂,Child組建發(fā)現(xiàn)跟上次傳入的不一致當(dāng)然會觸發(fā)更新错览。
3、子組件只會在初始化時刷新一次煌往,此后不再刷新
4倾哺、子組件最初初始化刷新一次,此后只有當(dāng)父組件info改變導(dǎo)致的父組件刷新才會跟著刷新刽脖,其他的name羞海,index導(dǎo)致的父組件刷新,子組件不再刷新
5曲管、父組件刷新却邓,內(nèi)部函數(shù)callback重新生成,地址變化所以子組件跟著刷新
6院水、傳入函數(shù)用useCallback緩存了腊徙,如果name不變則父組件刷新該函數(shù)也不會發(fā)生變化,所以子組件不會跟著刷新檬某。如果name發(fā)生變化導(dǎo)致的父組件刷新撬腾,子組件還是會跟著刷新的。
7橙喘、設(shè)置info給Child時时鸵,有時候Child只希望當(dāng)info中的某一個/幾個屬性發(fā)生變化時才刷新,這時可以使用useMemo厅瞎,在這里,只有當(dāng)info的height屬性發(fā)生變化導(dǎo)致的父組件刷新初坠,Child才會同步刷新
useCallback和useMemo都是依賴第二個參數(shù)的緩存方法和簸,若第二個參數(shù)不寫則沒有任何作用。當(dāng)?shù)诙€參數(shù)內(nèi)部值變更時才會返回新的內(nèi)容碟刺,否則總是返回前面緩存的那個不變锁保。對第六種情況,如果第二個參數(shù)寫成[]半沽,那么返回的callback永遠是最初的那個爽柒,里面捕獲的name值也是最初的那個,不管外面的name是否發(fā)生改變者填,callback回調(diào)時打印輸出的永遠是最初捕獲的name浩村,因為callback沒有重新生成。
以上都是控制Child的刷新占哟,針對自身的刷新規(guī)則是心墅,任意調(diào)用setState的地方酿矢,會對比前后值的差異(淺比較),一旦變化則刷新頁面怎燥。刷新組件意味著代碼從函數(shù)開始順序執(zhí)行到結(jié)尾瘫筐,所以在hook中函數(shù)外代碼不宜過多過于復(fù)雜
7、使用let而不是const有什么問題
在4中使用了let方式聲明state铐姚,則state可以直接賦值策肝,此時相當(dāng)于class時代的this.xxx=yyy這樣,不會觸發(fā)頁面刷新
let [index, setIndex] = useState(0);
const onClick = () => {
index = 10;
setIndex(0);
}
return (
<div>
<Button onClick={onClick}>
+++
</Button>
</div>
)
如上隐绵,組件內(nèi)的state屬性index存儲在另外一塊空間中(取名G)驳糯,直接修改index=10改變的僅僅是組件內(nèi)的臨時變量index的值,G內(nèi)部該index值還是原先的值0沒有改變氢橙,兩邊同一個變量index值不一樣容易出現(xiàn)隱藏bug酝枢,也不符合設(shè)計規(guī)范,不推薦使用悍手。而通過setIndex(10)時先是改變G空間中index的值為10帘睦,對比原先和現(xiàn)在的值不同,刷新組件坦康,函數(shù)棧銷毀重新構(gòu)建新的組件竣付,內(nèi)部代碼從上到依次執(zhí)行,會重新創(chuàng)建組件內(nèi)部臨時變量index滞欠,賦值為10古胆,這樣兩邊的index保持值一樣。
要想實現(xiàn)class中this.xxx效果筛璧,下面的方式可能稍微好點
const [info] = useState({});
const onClick = async () => {
info.age = 10
}
聲明state的時候只賦給第一個值逸绎,這樣一看就知道info屬性是只能拿來使用,而不能刷新頁面夭谤。
8棺牧、個人思考hook優(yōu)勢劣勢
優(yōu)勢
a、復(fù)用粒度更細微朗儒,從class級別到了hook級別颊乘。例如網(wǎng)絡(luò)監(jiān)聽功能,可以將相關(guān)代碼全部寫到一個自定義hook中醉锄,使用時只要調(diào)用該hook即可
b乏悄、class時期,setState后需要對比整個虛擬dom的狀態(tài)恳不,對一個復(fù)雜頁面檩小,幾十個狀態(tài)需要對比耗費性能。而hook階段只需要對比一個值即可妆够,性能更佳识啦。
劣勢
a负蚊、閉包很多,值捕獲現(xiàn)象嚴(yán)重颓哮,要尤其注意hook的依賴
b家妆、大量的內(nèi)聯(lián)函數(shù)、函數(shù)嵌套冕茅,垃圾回收壓力大伤极。函數(shù)式組件這套方式,每次渲染就像調(diào)用一個純函數(shù)一樣(不純的東西交給Hook)姨伤,調(diào)用后產(chǎn)生一個作用域哨坪,并開辟對應(yīng)的內(nèi)容空間存儲該作用域下的變量,函數(shù)返回結(jié)束后該作用域會被銷毀乍楚,該作用域下的變量在作用域銷毀后就沒用了当编,如果沒有被作用域外的東西引用,就需要在下一次GC的時候被回收徒溪。這相對于Class組件而言忿偷,額外的開銷會多出很多,因為Class組件這套臊泌,所有的東西都是承載在一個對象上的鲤桥,都是在這對象上做操作,每次更新組件渠概,這個對象茶凳、對象的屬性和方法都是不會被銷毀的,即不會出現(xiàn)頻繁的開辟和回收內(nèi)存空間播揪。
最后:hook原理
https://www.cnblogs.com/rock-roll/p/11002093.html
https://segmentfault.com/a/1190000019966124
https://github.com/brickspert/blog/issues/26
https://react.docschina.org/docs/hooks-faq.html