React Hook 與Class的一些對比

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贮喧,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子剪芍,更是在濱河造成了極大的恐慌塞淹,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,482評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件罪裹,死亡現(xiàn)場離奇詭異,居然都是意外死亡运挫,警方通過查閱死者的電腦和手機状共,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谁帕,“玉大人峡继,你說我怎么就攤上這事⌒偻冢” “怎么了碾牌?”我有些...
    開封第一講書人閱讀 152,762評論 0 342
  • 文/不壞的土叔 我叫張陵康愤,是天一觀的道長。 經(jīng)常有香客問我舶吗,道長征冷,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,273評論 1 279
  • 正文 為了忘掉前任誓琼,我火速辦了婚禮检激,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘腹侣。我一直安慰自己叔收,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,289評論 5 373
  • 文/花漫 我一把揭開白布傲隶。 她就那樣靜靜地躺著饺律,像睡著了一般。 火紅的嫁衣襯著肌膚如雪跺株。 梳的紋絲不亂的頭發(fā)上复濒,一...
    開封第一講書人閱讀 49,046評論 1 285
  • 那天,我揣著相機與錄音帖鸦,去河邊找鬼芝薇。 笑死,一個胖子當(dāng)著我的面吹牛作儿,可吹牛的內(nèi)容都是我干的洛二。 我是一名探鬼主播,決...
    沈念sama閱讀 38,351評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼攻锰,長吁一口氣:“原來是場噩夢啊……” “哼晾嘶!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起娶吞,我...
    開封第一講書人閱讀 36,988評論 0 259
  • 序言:老撾萬榮一對情侶失蹤垒迂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后妒蛇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體机断,經(jīng)...
    沈念sama閱讀 43,476評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,948評論 2 324
  • 正文 我和宋清朗相戀三年绣夺,在試婚紗的時候發(fā)現(xiàn)自己被綠了吏奸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,064評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡陶耍,死狀恐怖奋蔚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤泊碑,帶...
    沈念sama閱讀 33,712評論 4 323
  • 正文 年R本政府宣布坤按,位于F島的核電站,受9級特大地震影響馒过,放射性物質(zhì)發(fā)生泄漏臭脓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,261評論 3 307
  • 文/蒙蒙 一沉桌、第九天 我趴在偏房一處隱蔽的房頂上張望谢鹊。 院中可真熱鬧,春花似錦留凭、人聲如沸佃扼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兼耀。三九已至,卻和暖如春求冷,著一層夾襖步出監(jiān)牢的瞬間瘤运,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評論 1 262
  • 我被黑心中介騙來泰國打工匠题, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拯坟,地道東北人。 一個月前我還...
    沈念sama閱讀 45,511評論 2 354
  • 正文 我出身青樓韭山,卻偏偏與公主長得像郁季,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子钱磅,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,802評論 2 345