class组件
基本使用
首先,先确认一个点,React
类组件中setState
渲染组件是异步还是同步?答案既可以同步又可以异步! 乍一看,woca,还有这种操作? 我们知道,setState
是react
的一步操作,每次调用setState
都会触发更新,异步操作是为了提高性能,并且将多个状态合并一起更新,减少render
调用。先看下面代码:
class Foo extends React.Component {
state = {
num: 0,
}
componentDidMount() {
for (let i = 0; i < 5; i++) {
this.setState({ num: this.state.num + 1 });
console.log(this.state.num);
}
}
render() {
return (
<div>Foo:{this.state.num}</div>
)
}
}
复制代码
效果: 从打印结果来看,循环5次的结果组件状态中的num
始终是0,直到最后一次num
更新为1,所以能清楚的知道setState
更新的过程是异步的。异步更新完了,那同步又是什么鬼。不急,接着往下看:
我们在componentDidMount
中代码修改为:
componentDidMount() {
setTimeout(() => {
for (let i = 0; i < 5; i++) {
console.log(this.state.num);
this.setState({ num: this.state.num + 1 });
}
})
}
复制代码
效果: 加了setTimeout
过后,我们发现setState
竟然变成了同步更新,render
里面也被执行,也就意味着如果你多次 setState
,会导致多次更新,这样导致了我们dom
也会被更新,这是毫无意义并且浪费性能的。
为什么会有这种情况(这里不做详细探究),只大概说一下原因: 因为在react
有一套自定义的事件系统和生命周期流程控制,我们只需要知道只要代码进入了 react
的调度流程,那就是异步的。只要你没有进入 react
的调度流程,那就是同步的。什么东西不会进入 react
的调度流程? setTimeout
、setInterval
、在 DOM
上绑定原生事件等。这些操作方式会跳出react这个体系,这些都不会走 React
的调度流程,所以会直接更新this.state
,在这种情况下调用 setState
就是同步的。 否则就是异步的。
上面列举了setState
同步和异步更新的两种情况,异步这种情况是React的优化手段,,但是显然它也会在导致一些不符合直觉的问题(就如上面第一个异步的例子),所以针对这种情况,React给出了一种解决方案:setState
接收的参数还可以是一个函数,在这个函数中可以拿先前的状态,并通过这个函数的返回值得到下一个状态。我们来修改上面的代码:
componentDidMount() {
for (let i = 0; i < 5; i++) {
console.log( prevState.num );
this.setState(prevState => {
return {
num: prevState.num + 1
}
});
}
}
复制代码
这有点像reduce方法,每次循环都能拿到上次执行完的结果(这里是setState合并后的结果);
渲染结果:
现在就是我们想要的结果了。
react
是如何处理异步的呢,又是怎么把上一次合并后结果的放在setState的回调函数里面的。接下来我们来模拟实现一下。
模拟实现
合并setState
// stateOrStateFn可以是一个方法
setState(stateOrStateFn) {
Object.assign(this.state, typeof stateOrStateFn==='function'? stateOrStateFn(this.prevState):stateOrStateFn);// stateOrStateFn可能是方法,也可能state对象
renderComponent(this);// 更新组件
}
复制代码
这种实现,每次执行setState
都会renderComponent
(显然不符合其更新优化机制),所以我们要合并setState。
任务队列taskEnque
要合并state
,我们需要开启一个任务队列taskEnque
来保存每次setState
过后的值,等所有setState
操作完成后合并state
并且renderComponent
更新dom。
我们知道js
可以用数组Array
来实现队列(shift
和unshift
)
const taskEnque = [];
/**
* @des 保存当前的state状态
* @params stateOrStateFn传递的state,可以是一个方法
* @params component为当前的组件
*/
const enqueueSetState = (stateOrStateFn, component) => {
taskEnque.push({
stateOrStateFn,
component
});
}
复制代码
然后修改最初的setState方法,不让其直接更新state和渲染组件,而是添加在队列中。
setState(stateOrStateFn) {
enqueueSetState(stateOrStateFn, this);
//renderComponent(this);// 更新组件
}
复制代码
保存过后接下来的事就是清空队列并且合并state
了。
清空队列合并state
const emptyQueue = () => {
let item;
while (item = taskEnque.shift()) {
const { stateOrStateFn, component } = item;
// 第一次prevState为空
if (!component.prevState) {
component.prevState = Object.assign({}, component.state);
}
// 如果stateOrStateFn是一个方法,则执行这个方法
if (typeof stateOrStateFn === 'function') {
// prevState用来保存上一次合并的结果
Object.assign(component.state, stateOrStateFn(component.prevState, component.props));
} else {
Object.assign(component.state, stateOrStateFn);
}
// 更新prevState
component.prevState = component.state;
}
}
复制代码
这里我们只完成了state
的更新,组件并没有渲染。重点来了,仔细想一下,我们组件应该什么时候更新,组件怎么更新。组件的更新是不能和清空队列同时进行的,如果同时进行,那就成了同步了。所以组件更新只能等taskEnque
被清空也就是state
都合并完后才执行更新操作。
所以我们必须要用一个新的值来保存,考虑到存在多个组件情况,我们需要用数组来保存。
const components = [];
const enqueueSetState = (stateOrStateFn, component) => {
// 添加不重复的组件
if (!components.includes(component)) {
components.push(component);
}
taskEnque.push({
stateOrStateFn,
component
});
}
复制代码
因为保存的组件在state
更新过后只执行一次,所以在enqueueSetState
时需要对components
去重。
现在需要更新的组件就保存好了,然后放在emptyQueue
等state执行完后执行。
const emptyQueue = () => {
let item, conpoment;
// 更新状态
while (item = taskEnque.shift()) {
//...
}
/** 更新组件 */
while (conpoment = components.shift()) {
// forceUpdate更新当前组件(模拟渲染过程)
conpoment.forceUpdate();
}
}
复制代码
延迟执行
现在还有一个重要的事情,就是emptyQueue
什么时候执行,我们需要合并一段时间内所有的setState
,也就是在一段时间后才执行emptyQueue
方法来清空队列,关键是这个“一段时间“怎么决定。
我们利用js的事件队列机制Eventloop。这里循环机制不(想)多(偷)讲(懒),可自行百度。也就是emptyQueue放在下一个任务队里执行,例如:setTimeout
、Promise
中then
方法。这里利用Promise中then微任务来处理。
const defer = (fn) => Promise.resolve().then(fn);
const enqueueSetState = (stateOrStateFn, component) => {
if (taskEnque.length === 0) {
// 放在下一个任务队列
defer(emptyQueue);
}
// 添加不重复的组件
if (!components.includes(component)) {
components.push(component);
}
taskEnque.push({
stateOrStateFn,
component
});
}
复制代码
到这里,我们模拟实现setState就已经完成了,忙活半天,是时候看看效果了。
class Foo extends React.Component {
state = {
num: 0,
}
componentDidMount() {
for (let i = 0; i < 5; i++) {
this.setState(prevState => {
return {
num: prevState.num + 1
}
});
}
}
// 模拟setState,通过this.forceUpdate更新组件
setState(stateOrStateFn) {
enqueueSetState(stateOrStateFn, this);
}
render() {
this.testCount = Math.random();
console.log('forceUpdate')
return (
<div>
<div>Foo:{this.state.num}</div>
</div>
)
}
}
复制代码
同样,用另一种方式来调用。
componentDidMount() {
for (let i = 0; i < 5; i++) {
console.log(this.state.num);
this.setState({ num: this.state.num + 1 });
}
复制代码
看效果,和react一样。
function组件
讲完了class组件中的setState原理和实现,现在来看看React >=16.8.0
中hooks里面useState是如何运作的。
基本使用
useState更新渲染组件过程是和class组件中setState更新机制一样的,即能同步也能异步(这里就不举例了)。
const App = () => {
const [num, setNum] = useState(0);
const [count, setCount] = useState(0);
const inputClick = (type) => {
return ()=>{
type==='num'?setNum(1+num):setCount(count+1);
}
}
return (<>
<div>hooks</div>
<button onClick={inputClick('num')}>addNum </button>
<span>{num}</span>
<div style={{height:'10px'}}></div>
<button onClick={inputClick('count')}>addCount </button>
<span>{count}</span>
</>)
}
function renderComponent() {
ReactDOM.render(
<App />,
document.getElementById('hooks-root')
);
}
renderComponent();
复制代码
按照 React 16.8.0
版本之前的机制,如果某个组件是函数组件,则这个 function 就相当于 Class 组件中的 render()
,不能拥有自己的状态(故又称其为无状态组件,stateless components),所以数据(输入)必须是来自父组件的 props
。而在 >=16.8.0
中,函数组件支持通过使用 Hooks 来为其引入自身状态
的能力。如上图:通过useState来为组件注入状态,并且每次更新组件不会重新初始化已有状态。要更新自己的状态只能通过useState返回的第二个参数来进行更改。
模拟实现
React.useState()
里都做了些什么:
- 将初始值赋给一个变量我们称之为
state
- 返回这个变量
state
以及改变这个state
的回调函数我们称之为setState
- 当
setState()
被调用时,state
被其传入的新值重新赋值,并且更新根视图
这里可以实现一个基础版了:
function useState(initialValue) {
let state = initialValue;
const setState = (newState) => {
state = newState;
// 更新组件
renderComponent();
};
return [state, setState];
}
复制代码
4.当组件更新时,初始状态是不会改变的,所以需要把状态放在方法外面去。
let state;
function useState(initialValue) {
state = state === undefined ? initialValue : state;
const setState = (newState) => {
sstate = newState;
renderComponent();
};
return [state, setState];
复制代码
5.现在看起来应该大功告成了,不过还是差点,想一下,一个函数式组件是不是可以有多个状态,每个状态之间都是独立的,也就是useState可以使用多次。怎么才能保证每次的状态互不影响呢?可以用数组来保存状态
let state = [],
index = 0;
function useState(initialValue) {
let currentIndex = index;
state[currentIndex] = state[currentIndex] === undefined ? initialValue : state[currentIndex];
const setState = (newState) => {
state[currentIndex] = newState;
renderComponent();
index = 0;
}
index += 1;
return [state[currentIndex], setState];
}
复制代码
6.和类组件setState
类似,useState中的setState
也支持函数来接收上一次的state。
let state = [],
index = 0;
const defer = (fn) => Promise.resolve().then(fn);
function useState(initialValue) {
// 保存当前的索引;
let currentIndex = index;
if (typeof initialValue === "function") {
initialValue = initialValue();
}
// render时候更新state
state[currentIndex] = state[currentIndex] === undefined ? initialValue : state[currentIndex];
const setState = newValue => {
if (typeof newValue === "function") {
// 函数式更新
newValue = newValue(state[currentIndex]);
}
state[currentIndex] = newValue;
if (index!==0) {
defer(renderComponent);
}
index = 0;
};
index += 1;
return [state[currentIndex], setState];
}
复制代码
注意 虽然我们通过这种形式实现了useState
,这要求我们保证 useState()
的调用顺序,所以我们不能在循环、条件或嵌套函数中调用 useState()
,因为这些情况下不能保证useState
的执行顺序,setState时不能精确的设置state状态,这在React官网还也给出了专门的解释。
React真正的实现并不是这样,上面的索引index,React实际上是用链表实现。
最后
至此,本文已结束,如有写的不对或者错误的地方,欢迎指正!
演示代码地址已经整理到了github.com/javascript-… 有需要的同学可以自行clone
;