类组件状态的使用
类组件的数据来源有两个地方,父组件传过来的属性以及自己内部的状态。 属性和状态发生变化后组件都会更新。 state的更新可能是异步,出于性能考虑可能会把多个setState合并到一起使用。 连续两个this.setState({})在第二个state不能拿到最新的值。他们会被合并。 可以使用this.setState(pre=>({…pre}))传入函数的形式来返回一个新值,这样下一个setState就能拿到最新的值。 这些setState还是会异步执行。怎样同步执行呢?在react管不到的地方,比如setTimeout里面执行的状态更新就都是同步的。即setState之后在同个函数可以拿到最新的修改完的State。
- 凡是react可以管控的地方,就是setState批量异步执行。
- 而不能管控的地方就是非批量同步执行。 setTimeout,原生dom事件等等。
- 本质就是批量与非批量,与js的同步异步无关。setState第二个回调参数也是有批量与非批量之分。
react事件采用小驼峰,如onClick。传的是一个函数的引用地址,真实的函数定义。 而原生dom事件传的是函数名的字符串。 简单实现:
let isBatchingUpdate = false;
let queue = [];
let state = { number: 1 };
function setState(newState) {
if (isBatchingUpdate) {
queue.push(newState);
} else {
state = { ...state, ...newState };
}
}
const handleClick = () => {
isBatchingUpdate = true;
setState({ number: state.number + 1 });
console.log(state);
setState({ number: state.number + 1 });
console.log(state);
state = queue.reduce((newState, currentVal) => {
console.log('newState', newState);
console.log('currentVal', currentVal);
return { ...newState, ...currentVal };
}, state);
};
handleClick();
console.log(state);
如上,我们定义了一个变量控制是否批量更新。如果批量更新则先放入数组,否则立即更新。 在批量更新之后,会执行一个遍历操作。将最新的state与老的state喝冰。 这是打印结果,可以看到最后的state的number是2,他执行了两次为什么还是2?因为currentVal里面的state指向的是老的state。即使第一次更新了number变为2,但是第二次更新的时候state还是1,执行了加一操作后变成2,再与第一次执行的状态进行合并,u最后得到2。
组件状态的实现
现在我们要基于自己之前实现的源码继续进行学习。
- 首先,要在React.Component这个类动手脚。
在其原型上定义一个setState的方法。每个类组件的实例都有一个updater更新器。每次一setState就会触发更新器的addState方法。 所以重点看下这个更新器:先实现同步的更新 Updater这个类的属性有类的实例,以及将要更新的队列和一个回调函数队列。 addState方法 这个方法用于收集更新任务放入队列。执行emitUpdate方法。 这个方法暂时实现先调用updateComponent方法。 这个方法用于调用shouldUpdate方法,并将根据队列计算所得的新的state传入 这个方法会直接修改类实例的state。调用类的forceUpdate方法。 getState方法的实现,用于遍历队列中的更新计算最新的state然后返回。 看效果: 到此为止类的state就已经更新了。接着实现组件的更新,首先先暂时不使用diff算法。 我们需要在更新组件的时候获取新老vdom,并将其差异渲染在真实dom上。主要实现findDom,和compareTwoVdom两个方法。 在我们首次创建dom的时候,比如类组件本身就是一个react元素,然后调用render又返回一个react元素。那么就让类组件的vdom上的oldRenderVdom指向这个内层的返回的react元素,函数组件也是同样的道理。 到达内层组件的时候, 在我们之前创建dom的时候就有把真实dom挂载到vdom上。所以就可以通过类组件的vdom获取内层的vdom,在获取真实的dom。
findDom的实现
通过虚拟dom获取真实dom,如果类型是组件,就应该继续调用,传入内层的vdom。如果不是组件,就表示是内层的vdom了,直接返回dom即可。
compareTwoVdom的简易实现
直接更改。到此,类组件状态的同步更新就完成了。 效果: 我们刚才实现的时候同步的更新,也就是未批量的更新,现在实现异步(批量更新)
批量更新的实现
批量更新的实现需要依赖一个全局变量,updateQueue,他可以控制是否批量更新,并且存放着更新的队列,和一个遍历队列触发更新的函数。 如 然后在每次收集更新依赖的时候 如果开启了批量更新的开关,就将其存起来,等时机一到再一起更新。 目前我们已经定义了一个全局变量用来控制批量更新。我们还需要当更新发生的时候,就是onclick事件触发的时候开启批量更新的开关。
合成事件的完成
react通过将事件委托在document上,(17之后是委托在root组件) 在处理事件的时候需要修改一下,实现addEvent函数,该函数接受三个参数,当前的dom,事件名,事件函数。 这个函数做的事情也很简单,主要是给dom上加了一个store属性,用来存放该dom的事件。 然后将该事件委托给了document。当事件触发的时候触发dispatchEvent函数。 dispatchEvent函数,通过event,拿到target和type。然后在这里开启批量更新,因为事件已经发生了,对应的事件函数也会触发, 这个函数就会被调用,因为开启了批量更新,所以被收集到updateQueue中。 然后模范了下事件冒泡,比如a的伏组件是b,a发生了点击事件后,通过a.parentNode拿到b,如果b有点击事件也会触发。最后结束的时候直接调用全局变量updateQueue执行batchUpdate函数,遍历执行里面的每一个updater的updateComponent方法。达到批量更新的目的 结果: 可以看到,setTimeout之外的更新,两个state此时都是numebr为0,表示了这时候已经是批量更新了。然后接着调用setTimeout内的函数,
- 因为setTimeout是宏任务,会等同步任务执行完后再执行,所以当执行setTimeout里面的函数的时候,事件处理已经过去了,而批量更新的开关也已经关闭,所以只能是非批量(同步更新),所以可以看到两个打印出来的值是实时的。
- 这也是在开发中有时候遇到两个setState的时候,会有一个触发过后,另一个拿到的state的值还是老的的原因,因为他们是批量更新的,执行的时候拿到的state还是老的。解决办法有通过setTimeout(()=>{},0)让一个跳出批量更新,这样另一个拿到的state就永远是最新的了。
|