React Hook 与Class的一些对比

1、值捕获 造成数据不一致 异常

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>
    )
}

闭包内部变量为值捕获。
如例子,点击按钮,设置age为1,调用request方法,内部@1处block生成,捕获此时age的值为0(setAge为异步方法,此时age还没有变化,依旧为0),1s后方法返回age的值依旧是0,但此时age真正的值为1,造成数据不一致

函数组建每一次渲染时,内部会从上到下依次执行代码,重新生成上下文环境,普通函数也就重新生成,重新捕获上下文变量。useEffect、useMemo、useCallback等这些hook都自带闭包,当依赖设置不当时,函数组建重复渲染时不会更新闭包,导致内部捕获的上下文还是上一次的,从而数据出错。hook函数的依赖很重要,例如useMemo,当依赖不变时,组建重新渲染其内部返回的子组件使用缓存上一次的,不会刷新,性能得意提升。

2、怎么存储值
class时,对组件内部的值可以使用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属于全局变量,不在组件内部,不符合设计原则,@3使用ref方式存储不需要更新页面的属性,@4使用对象info,使用直接赋值的方式将值保存在对象中,也不会刷新页面,后两种满足需求

3、useEffect会在组件初次渲染时调用一次,如何忽略这次?
有种场景,如dva中一个action,使modal中数据改变,要求一旦发生数据改变则执行某个方法。
在class中可以在componentWillReceiveProps中判断数据有没发生改变,那么在hook中嘞。
我采取的是useEffect,在此方法中过滤首次执行,代码封装后如下

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即可,该方法会将组建首次渲染回调的useEffect过滤,再次渲染时候回调正常进行

4、this.setState异步回调问题
在class中,有时候希望调用setState后马上拿到更新后的罪行state值做些事情,此时可以

this.setState({
            name:'jack'  
        }, () => {
            console.log(this.state.name);
        })

在回调用拿到state的最新值
在hook中useState没有回调,无法即时获取最新的state值,目前想到两种方式代替
一种是传值,将最新的要更改的state值保存下来,在通过值传递方式使用

 const [name, setName] = useState('');
 const onClick = async () => {
      let newName = 'jack'
      setName(newName);
      request(newName)
  }

 const request = (name) => {
     console.log(name);
 }

这样有个问题就是当请求链条很长时,这个参数需要在很多方法之间传递不太方便,也显得多余
还有个方法,就是使用useEffect,当某个state一旦改变就触发执行所需方法

  const [name, setName] = useState('');
    const onClick = async () => { 
        setName('jack');
    }
    
    useEffect_ignoreFirst(() => {
        request()
    }, [name])

    const request = () => {
        console.log(name);
    }

这里需要使用useEffect_ignoreFirst过滤掉首次渲染导致的useEffect回调
补充:还有种方式,

 let [name, setName] = useState('');
 const onClick = async () => {
      name = 'jack'
      setName(name);
      request()
  }

 const request = () => {
     console.log(name);
 }

5、useEffect监听对象改变
默认useEffect是采用的浅比较

  const [info, setInfo] = useState({ age: 0 });

    const onClick = () => {
        setInfo({ age: 0 })
    }

    useEffect(
        () => {
            console.log(info);
        },
        [info]
    );

只要调用了setInfo,尽管内部的属性完全没有发生变化,但是因为浅比较是比较的对象地址,判断为不相等,会导致页面刷新,useEffect重新执行。

对对象类型,大多情况需要比较的是那部属性是否变化,而不是地址是否变化,写了如下自定义对象比较类型的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是否发生变化,从而执行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属性是否发生了变化,而执行useEffect。

useEffect自定义比较对象,又忽略首次组建渲染导致的调用,结合上面的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控制自身组件的刷新时机,优化新能。
hook中也有相对的memo,但是用法有区别

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的几种写法,当父组件刷新时,子组件的刷新情况测试如下
1、父组建一旦属性,则子组建也会同时刷新
2、同1。父组建刷新,函数内部所有的state变量,函数方法都会重新生成。在这里,刷新后name变量发生变化,Child组建发现跟上次传入的不一致当然会触发更新。
3、子组件只会在初始化时刷新一次,此后不再刷新
4、子组件最初初始化刷新一次,此后只有当父组件info改变导致的父组件刷新才会跟着刷新,其他的name,index导致的父组件刷新,子组件不再刷新
5、父组件刷新,内部函数callback重新生成,地址变化所以子组件跟着刷新
6、传入函数用useCallback缓存了,如果name不变则父组件刷新该函数也不会发生变化,所以子组件不会跟着刷新。如果name发生变化导致的父组件刷新,子组件还是会跟着刷新的。
7、设置info给Child时,有时候Child只希望当info中的某一个/几个属性发生变化时才刷新,这时可以使用useMemo,在这里,只有当info的height属性发生变化导致的父组件刷新,Child才会同步刷新

useCallback和useMemo都是依赖第二个参数的缓存方法,若第二个参数不写则没有任何作用。当第二个参数内部值变更时才会返回新的内容,否则总是返回前面缓存的那个不变。对第六种情况,如果第二个参数写成[],那么返回的callback永远是最初的那个,里面捕获的name值也是最初的那个,不管外面的name是否发生改变,callback回调时打印输出的永远是最初捕获的name,因为callback没有重新生成。

以上都是控制Child的刷新,针对自身的刷新规则是,任意调用setState的地方,会对比前后值的差异(浅比较),一旦变化则刷新页面。刷新组件意味着代码从函数开始顺序执行到结尾,所以在hook中函数外代码不宜过多过于复杂

7、使用let而不是const有什么问题
在4中使用了let方式声明state,则state可以直接赋值,此时相当于class时代的this.xxx=yyy这样,不会触发页面刷新

 let [index, setIndex] = useState(0);

    const onClick = () => {
        index = 10;
        setIndex(0);
    }

    return (
        <div>
            <Button onClick={onClick}>
                +++
            </Button>
        </div>
    )

如上,组件内的state属性index存储在另外一块空间中(取名G),直接修改index=10改变的仅仅是组件内的临时变量index的值,G内部该index值还是原先的值0没有改变,两边同一个变量index值不一样容易出现隐藏bug,也不符合设计规范,不推荐使用。而通过setIndex(10)时先是改变G空间中index的值为10,对比原先和现在的值不同,刷新组件,函数栈销毁重新构建新的组件,内部代码从上到依次执行,会重新创建组件内部临时变量index,赋值为10,这样两边的index保持值一样。

要想实现class中this.xxx效果,下面的方式可能稍微好点

   const [info] = useState({});
    const onClick = async () => {
        info.age = 10
    }

声明state的时候只赋给第一个值,这样一看就知道info属性是只能拿来使用,而不能刷新页面。

8、个人思考hook优势劣势
优势
a、复用粒度更细微,从class级别到了hook级别。例如网络监听功能,可以将相关代码全部写到一个自定义hook中,使用时只要调用该hook即可
b、class时期,setState后需要对比整个虚拟dom的状态,对一个复杂页面,几十个状态需要对比耗费性能。而hook阶段只需要对比一个值即可,性能更佳。
劣势
a、闭包很多,值捕获现象严重,要尤其注意hook的依赖
b、大量的内联函数、函数嵌套,垃圾回收压力大。函数式组件这套方式,每次渲染就像调用一个纯函数一样(不纯的东西交给Hook),调用后产生一个作用域,并开辟对应的内容空间存储该作用域下的变量,函数返回结束后该作用域会被销毁,该作用域下的变量在作用域销毁后就没用了,如果没有被作用域外的东西引用,就需要在下一次GC的时候被回收。这相对于Class组件而言,额外的开销会多出很多,因为Class组件这套,所有的东西都是承载在一个对象上的,都是在这对象上做操作,每次更新组件,这个对象、对象的属性和方法都是不会被销毁的,即不会出现频繁的开辟和回收内存空间。

最后: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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350