页面更新与批处理
何为更新
- 那类组件来说,我们可以取出当前组件的虚拟dom,让该虚拟dom跟旧的虚拟dom进行比对,替换掉不同的地方。然后渲染到界面上去。从而完成更新。
- 我们在类组件中,操作state是通过setState()来操作的,该方法是Component类的方法,该方法修改实例中state的属性,又调用更新的方法。那我们可以简单地编写出如下代码
class Component {
static isReactComponent = true
constructor(props) {
this.props = props;
this.state = {};
this.updater = new Updater(this);
}
setState(partialState, callback) {
partialState() // 用户传入的方法
forceUpdate() // 页面更新
callback() // 用户传入的回调函数
}
forceUpdate() {
let newVdom = this.render();
updateClassComponent(this, newVdom);
}
}
//TODO 更新类组件
function updateClassComponent(classInstance, newVdom) {
let oldDOM = classInstance.dom;//取出这个类组件上次渲染出来的真实DOM
// 下面我们应该使用diff算法进行比对,但是为了简化操作,我们选择直接替换。
let newDOM = createDOM(newVdom);
oldDOM.parentNode.replaceChild(newDOM, oldDOM);
classInstance.dom = newDOM;
}
批处理
现象演示
请看以下例子
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { name: this.props.name, number: 0 };
}
handleClick = (syntheticEvent) => {
this.setState((lastState) => ({ number: lastState.number + 1 }), () => {
console.log('callback1', this.state.number);
});
console.log(this.state.number);
this.setState((lastState) => ({ number: lastState.number + 1 }), () => {
console.log('callback2', this.state.number);
});
console.log(this.state.number);
Promise.resolve().then(() => {
console.log(this.state.number);
this.setState((lastState) => ({ number: lastState.number + 1 }), () => {
console.log('callback3', this.state.number);
});
console.log(this.state.number);
this.setState((lastState) => ({ number: lastState.number + 1 }), () => {
console.log('callback4', this.state.number);
});
console.log(this.state.number);
}, 0);
}
render() {
return (
<div>
<p>{this.state.name}</p>
<p>{this.state.number}</p>
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
ReactDOM.render((
<Counter name="计数器" />
), document.getElementById('root'));
请问当我们点击+按钮时,控制台得到的结果是什么?
以下为结果
为什么会有这个结果呢?我们来分析一下
- 在React里,事件的更新可能是异步的,是批量的,不是同步的
- 调用state之后状态并没有立刻更新,而是先缓存起来了 等事件函数处理完成后,再进行批量更新,一次更新并重新渲染
- 因为jsx事件处理函数是react控制的,只要归react控制就是批量,只要不归react管了。就是非批量。也就是说在 Promise,setTimeout 中,更新逻辑是同步的。
调度队列
一个点击事件或者其他事件可能会调用多次setState方法,为了达到批处理,我们给Component类添加一个Updater任务队列。当我们在React中使用setState时,调用addState()将该函数插入到任务队列中。接着该方法通过updateQueue.isBatchingUpdate()判断当前是否为批处理。如果是,则直接放入到 updateQueue 更新队列中。否则,直接更新,并且当更新队列为空时,便会依次执行callbacks回调函数。
代码如下:
Component
class Component{
static isReactComponent = true
constructor(props){
this.props = props;
this.state = {};
this.updater = new Updater(this);
}
setState(partialState,callback){
this.updater.addState(partialState,callback);
}
forceUpdate(){
let newVdom = this.render();
updateClassComponent(this,newVdom);
}
}
//TODO 更新类组件
function updateClassComponent(classInstance,newVdom){
let oldDOM = classInstance.dom;//取出这个类组件上次渲染出来的真实DOM
let newDOM = createDOM(newVdom);
oldDOM.parentNode.replaceChild(newDOM,oldDOM);
classInstance.dom=newDOM;
}
Updater
class Updater{
constructor(classInstance){
this.classInstance = classInstance;//类组件的实例
this.pendingStates = [];//等待生效的状态,可能是一个对象,也可能是一个函数
this.callbacks = [];
}
addState(partialState,callback){
this.pendingStates.push(partialState);///等待更新的或者说等待生效的状态
if(typeof callback ==='function')
this.callbacks.push(callback);//状态更新后的回调
if(updateQueue.isBatchingUpdate){//如果当前的批量模式。先缓存updater
updateQueue.updaters.add(this);//本次setState调用结束
}else{
this.updateClassComponent();//直接更新组件
}
}
updateClassComponent(){
let {classInstance,pendingStates,callbacks} = this;
// 如果有等待更新的状态对象的话
if(pendingStates.length>0){
classInstance.state = this.getState();//计算新状态
classInstance.forceUpdate();
callbacks.forEach(callback=>callback());
callbacks.length=0;
}
}
getState(){
let {classInstance,pendingStates} = this;
let {state}=classInstance;
pendingStates.forEach((nextState)=>{
//如果pendingState是一个函数的话,传入老状态,返回新状态,再进行合并
if(typeof nextState === 'function'){
nextState=nextState(state);
}
state={...state,...nextState};
});
pendingStates.length=0;//清空数组
return state;
}
}
更新队列 updateQueue
let updateQueue = {
isBatchingUpdate:false,//当前是否处于批量更新模式,默认值是false
updaters:new Set(),
batchUpdate(){//批量更新
for(let updater of this.updaters){
updater.updateClassComponent();
}
this.isBatchingUpdate = false; // 更新完毕后,将设置为非批处理。
}
}
下图为整个流程图
总结
- 在React中,所有更新逻辑默认都是异步的,其原理就是将更新的逻辑统一放到组件的updater中,再将updater放入更新队列中,然后统一处理。简称批处理,这样可以大大减少对dom的重复渲染。
- 如何开启关闭批处理的关键,是更新队列 updateQueue.isBatchingUpdate 是否为true。由于我们每次批处理后都会将其改为false,于是在我们使用Promise或者setTimeout等其他异步函数时,更新逻辑是同步的。
十分感谢 张仁阳 老师的教导