美文网首页
React Hook 与Class的一些对比

React Hook 与Class的一些对比

作者: 请叫我啊亮 | 来源:发表于2020-06-22 09:36 被阅读0次

    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

    相关文章

      网友评论

          本文标题:React Hook 与Class的一些对比

          本文链接:https://www.haomeiwen.com/subject/dicbxktx.html