赞
踩
参考文章:React Hook的实现原理和最佳实践
首先,我们来看一个简单的useState()
的使用案例:
import './App.css'; import { useEffect, useState } from 'react' function App() { const [count, setCount] = useState(0) useEffect(() => { console.log(`update---${count}`) }, [count]) return ( <div> <button onClick={() => setCount(count + 1)}> {`当前点击次数:${count}`} </button> </div> ); } export default App;
分析:来看下面这行代码:
const [count, setCount] = useState(0)
可以发现:
useState()
函数,会返回一个变量(count
)以及一个函数(setCount
)。useState()
函数中可以传入一个参数,也就是该变量的初始值。那么根据上述发现的2点,我们来自定义一个函数(创建个react脚手架,在index.js
文件中修改):
import React from "react"; import ReactDOM from "react-dom"; function useState(initVal) { let val = initVal; function setVal(newVal) { val = newVal; // 修改变量后,调用render函数,重新渲染页面 render(); } return [val, setVal]; } function App() { const [count, setCount] = useState(0); return ( <div> <button onClick={() => { console.log(count); setCount(count + 1); }}> {`当前点击次数:${count}`} </button> </div> ) } // 初次渲染用 render(); function render() { ReactDOM.render(<App />, document.getElementById("root")); }
但是效果如下:
出现这种情况的原因分析:
setState
函数执行了。let val = initVal;
这行代码是在函数内部被声明的,也因此每次调用useState
函数的时候,都会重新声明val
变量,从而导致其状态无法被保存。因此我们对上述代码进行一个修改,将val
变量放到全局作用域中:
// 全局作用域
let val;
function useState(initVal) {
// 判断val是否存在 存在就使用
val = val|| initVal;
function setVal(newVal) {
val = newVal;
// 修改变量后,调用render函数,重新渲染页面
render();
}
return [val, setVal];
}
此时,代码修改后的页面效果才是正常的:
我们可以注意到,在使用useState
的时候,允许我们传入一个参数作为该状态变量的默认值。我们将这个参数先命名为initValue
,该参数只会在组件初次渲染的时候起生效,在后续渲染的时候则会被忽略。
同时,倘若这个初始值需要经过计算获得,那么这种情况我们大致分为这么2种方式:
useEffect()
函数中去调用一个伪代码getUserInfo()
,然后通过返回值去调用setState
函数赋值。state
的方式。useState
中不仅可以传入一个参数作为默认值,还可以传入一个函数,在函数中计算并返回初始的state
即可。例如(伪代码):
const [state, setState] = useState(() => {
const userName= getUserInfo();
return userName;
});
React中,通过Object.is
算法来比较状态变量的不同,其判别的标准如下(摘自官网):Object.is(A,B)
只要满足下列条件任意一条,就代表这两个值相同。
undefined
.null
。true
/false
。两个都 +0。
两个都 -0。
两个都 NaN。
或两者都不为零且两者均不NaN相同,且两者的值相同。
Object.is(25, 25); // true Object.is('foo', 'foo'); // true Object.is('foo', 'bar'); // false Object.is(null, null); // true Object.is(undefined, undefined); // true Object.is(window, window); // true Object.is([], []); // false var foo = { a: 1 }; var bar = { a: 1 }; Object.is(foo, foo); // true Object.is(foo, bar); // false // Case 2: Signed zero Object.is(0, -0); // false Object.is(+0, -0); // false Object.is(-0, -0); // true Object.is(0n, -0n); // true // Case 3: NaN Object.is(NaN, 0/0); // true Object.is(NaN, Number.NaN) // true
我们知道useEffect()
函数会在第一次渲染之前调用,并且有两个参数:
useEffect()
函数根据第二个参数中是否有变化,来判断是否执行第一个参数的函数。也因此,我们在开发过程中,对于只希望其执行一次的useEffect()
函数,我们往往写入第二个参数为一个空数组(否则可能引起无限渲染的BUG)。
实现1:useEffect
传入一个函数参数,里面调用即可。
import React from "react"; import ReactDOM from "react-dom"; let val; function useState(initVal) { val = val || initVal; function setVal(newVal) { val = newVal; render(); // 重新render页面 } return [val, setVal]; } // 自定义的useEffect function useEffect(fn) { fn(); } function App() { const [count, setCount] = useState(0); useEffect(() => { console.log(`自定义useEffect调用--count:${count}`); }); return ( <div> <button onClick={() => { console.log(count); setCount(count + 1); }}> {`当前点击次数:${count}`} </button> </div> ) } // 初次渲染用 render(); function render() { ReactDOM.render(<App />, document.getElementById("root")); }
但是页面效果如下,可以发现每点击一次按钮,就重新渲染一次,而每次渲染则调用一次useEffect()
函数。
那么再来看看有两个参数版本的自定义useEffect()
函数,这个函数需要做到:
useEffect()
函数。let watchArr;
function useEffect(fn, watch) {
const hasWactchChange = watchArr
? !watch.every((val, i) => val === watchArr[i])
: true;
if (hasWactchChange) {
fn();
watchArr = watch;
}
}
完整案例如下(index.js
文件):
import React from "react"; import ReactDOM from "react-dom"; let val; function useState(initVal) { val = val || initVal; function setVal(newVal) { val = newVal; render(); // 重新render页面 } return [val, setVal]; } // 自定义的useEffect let watchArr; function useEffect(fn, watch) { const hasWactchChange = watchArr ? !watch.every((val, i) => val === watchArr[i]) : true; if (hasWactchChange) { fn(); watchArr = watch; } } function App() { const [count, setCount] = useState(0); useEffect(() => { console.log(`自定义useEffect调用--count:${count}`); }, []); return ( <div> <button onClick={() => { setCount(count + 1); }}> {`当前点击次数:${count}`} </button> </div> ) } // 初次渲染用 render(); function render() { ReactDOM.render(<App />, document.getElementById("root")); }
页面效果如下:可以见到,useEffect()
函数就执行了一次,因为第二个参数中我们传入了一个空数组。
在上面,我们初步实现了useState和useEffect函数,并成功调用,但是上述案例都是在一个变量的情况下发生的,那么倘若有两个变量的情况下,依旧采用上述的自定义代码,会发生什么?
案例如下:
import React from "react"; import ReactDOM from "react-dom"; let val; function useState(initVal) { val = val || initVal; function setVal(newVal) { val = newVal; render(); // 重新render页面 } return [val, setVal]; } // 自定义的useEffect let watchArr; function useEffect(fn, watch) { const hasWactchChange = watchArr ? !watch.every((val, i) => val === watchArr[i]) : true; if (hasWactchChange) { fn(); watchArr = watch; } } function App() { const [count, setCount] = useState(0); const [data, setData] = useState(0); useEffect(() => { console.log(`自定义useEffect调用--count:${count}`); }, [count]); useEffect(() => { console.log(`自定义useEffect调用--data:${data}`); }, [data]); return ( <div> <button onClick={() => { setCount(count + 1); }}> {`按钮1:当前点击次数:${count}`} </button> <hr /> <button onClick={() => { setData(data + 1); }}> {`按钮2:当前点击次数:${data}`} </button> </div> ) } // 初次渲染用 render(); function render() { ReactDOM.render(<App />, document.getElementById("root")); }
页面效果如下:
原因分析:以useState
为例:
useState
方法的地方,都会共享一个全局变量val
。代码改进:
代码如下:
import React from "react"; import ReactDOM from "react-dom"; let memoizedState = []; let currentIndex = 0; function useState(initVal) { memoizedState[currentIndex] = memoizedState[currentIndex] || initVal; const cursor = currentIndex; function setVal(newVal) { memoizedState[cursor] = newVal; render(); } // 返回state 然后 currentIndex+1 return [memoizedState[currentIndex++], setVal]; } // 自定义的useEffect function useEffect(fn, watch) { const hasWatchChange = memoizedState[currentIndex] ? !watch.every((val, i) => val === memoizedState[currentIndex][i]) : true; if (hasWatchChange) { fn(); memoizedState[currentIndex] = watch; currentIndex++; // 累加 currentIndex } } function App() { const [count, setCount] = useState(0); const [data, setData] = useState(0); useEffect(() => { console.log(`自定义useEffect调用--count:${count}`); }, [count]); useEffect(() => { console.log(`自定义useEffect调用--data:${data}`); }, [data]); return ( <div> <button onClick={() => { setCount(count + 1); }}> {`按钮1:当前点击次数:${count}`} </button> <hr /> <button onClick={() => { setData(data + 1); }}> {`按钮2:当前点击次数:${data}`} </button> </div> ) } // 初次渲染用 render(); function render() { console.log(memoizedState); // 执行hook后 数组的变化 currentIndex = 0; // 重新render时需要设置为 0 ReactDOM.render(<App />, document.getElementById("root")); }
页面效果如下:
从上述代码中,我们可以发现每次调用render()
函数,都要将对应的全局下标重置为0,这个操作我刚开始看到就觉得匪夷所思,想了半天我才明白是为什么:
useEffect
函数的第二个参数是数组的原因,也是因为我们变量的存储也是以数组形式来存在。备注:
我们这里的代码是个简化版的,官方的
useState
和useEffect
函数肯定是要更完善的。希望大家引以区分。
这里在对上述案例中的输出做一个解释,我们以第一次输出为例:(此时点击按钮1)
结果分析:
那么此时的数组值也就对应了控制台中输出的内容:
我们也可以注意到代码中:
setXXX
函数的时候,都会从数组memoizedState
中对应的位置去取值,并重新赋值,从而获得一个全新的数组memoizedState
(之所以能够把索引位置对得上,是因为调用render
函数的时候把全局下包也重置为0了)useEffect()
函数,则通过第二个参数是否发生变化来决定其是否调用。在日常开发当中,我们往往会给一个按钮添加一个onChange
事件或者onClick
事件,那么就以onClick
事件为例:
onClick
事件,里面肯定是调用我们自定义的onClick
函数。onClick
函数。伪代码如下:
class Demo extends Component{
render() {
return
<div>
<Button onClick={ () => { console.log('Hello World!!'); }} />
</div>;
}
}
因此我们在类式组件开发过程中,往往会这么改写代码,来避免性能浪费问题:
class Demo extends Component{
constructor(){
super();
this.buttonClick = this.buttonClick.bind(this);
}
render() {
return
<div>
<Button onClick={ this.buttonClick } />
</div>;
}
}
那如果在函数式组件中开发,如何写呢?
回答:采用ReactHook中的useCallback()
函数:
function Demo(){
const buttonClick = useCallback(() => {
console.log('Hello World!!')
},[])
return(
<div>
<Button onClick={ buttonClick } />
</div>
)
}
作用:useCallback
函数会生成一个记忆函数,这样更新时就能保证这个函数不会发生渲染。 从而达到规避性能浪费的目标。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。