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