前言
在带你从0~1手写React源码----实现函数组件及类组件的渲染和更新中,如果只有一个状态改变,整个页面都会进行更新,这明显性能较差,而在REACT源码中,事件的更新可能是异步的,是批量的,不是同步的。
class Couter extends React.Component{
constructor(props){
super(props);
this.state={name:this.props.name,number:0}
}
handleClick=()=>{
this.setState({number:this.state.number+1});
console.log(this.state.number); //0
this.setState({number:this.state.number+1});
console.log(this.state.number);//0
setTimeout(()=>{
console.log(this.state.number);//1
this.setState({number:this.state.number+1});
console.log(this.state.number);//2
this.setState({number:this.state.number+1});
console.log(this.state.number);//3
},1000)
}
render(){
return (
<div>
<p>{this.state.name}</p>
<p>{this.state.number}</p>
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
上面这段代码在React源码中运行输出的结果是0 0 1 2 3。这说明在执行该事件方法时,调用setState改变state状态并没有立即更新state,而是将state先缓存起来,所以在事件中,state是异步批量更新的。而setTimeout中的方法执行,state会立即更新,所有在事件中,state是同步更新的。
批量更新的实现原理
在React源码中我们需要定义一个更新器Updater,用于控制state状态的更新
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.isBatchUpdate) {
//如果是批量更新,先缓存updater
updateQueue.updaters.add(this) //本次setState调用结束
} else {
this.updateClassComponent() //直接更新组件
}
}
updateClassComponent() {
const { classInstance, pendingStates, callbacks } = this
if (pendingStates.length > 0) {
//如果有需要等待更新的状态对象的话
classInstance.state = this.getState() //计算新状态
classInstance.forceUpdate() //强行更新组件
callbacks.forEach((callback) => {
callback()
})
callbacks.length = 0 //清空数组
}
}
getState() {
const { classInstance, pendingStates, callbacks } = this
let { state } = classInstance
pendingStates.forEach((pendingState) => {
//如果pendingStat是函数,需要传递老状态,返回新状态然后进行合并
if (typeof pendingState === 'function') {
pendingState = pendingState.call(classInstance, state)
}
state = { ...state, ...pendingState }
})
pendingStates.length = 0 //清空数组
return state
}
}
这里我们通过定义一个队列对象updateQueue来判断是否state需要批量更新
const updateQueue={
isBatchUpdate:false,//是否批量更新
updaters:[],
batchUpdate(){ //批量更新
for(let updater of this.updater){
updater.updateClassComponent()
}
this.isBatchUpdate=false;
this.updaters.length=0;
}
}
在上述代码中,我们在队列对象中定义一个属性isBatchUpdate,来控制事件是否需要批量更新,如果需要批量更新,我们将该实例先缓存到队列对象updateQueue中,如果不需要批量更新,直接更新组件。直接更新组件,需要做的三件事情:
- 计算新的状态
getState,也就是合并新的状态。注意:如果setState传递的第一个参数是函数,需要将该函数执行,其函数返回结果就是新的状态。 - 执行
setState传递的第二个参数callback - 强制更新组件
forceUpdate。该方法挂载在Component上。
在带你从0~1手写React源码----实现函数组件及类组件的渲染和更新中,我们没有对setState的状态进行处理,下面是Component类添加了setState方法和forceUpdate方法的代码:
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)
/**
* let state=this.state;
this.state={...state,...partialState};
//state改变组件更新 得到新的虚拟DOM
let newVdom=this.render();
//挂载到真实DOM去
updateClassComponent(this,newVdom)
*/
}
//强行更新组件
forceUpdate() {
let newVdom = this.render()
updateClassComponent(this, newVdom)
}
render() {
throw new Error('此方法为抽象方法,需要子类实现')
}
}
function updateClassComponent(classInstance, newVdom) {
let oldDom = classInstance.dom //取出上次类组件渲染出来的真实DOM
let newDom = createDOM(newVdom)
oldDom.parentNode.replaceChild(newDom, oldDom)
classInstance.dom = newDom
}
合成事件的实现原理
合成事件的作用:
- 兼容处理,兼容浏览器
- 可以在写的事件处理函数之前和之后做一些事情(比如让状态进行批量更新)
在带你从0~1手写React源码----实现函数组件及类组件的渲染和更新中,我们对虚拟DOM的props的事件进行了处理,这里我们需要改下:
/**
* 使用虚拟dom属性更新刚刚创建出来的真实DOM属性
* @param {*} vdom 真实DOM
* @param {*} props 新属性对象
*/
function updateProps(vdom, props) {
for (let key in props) {
if (key === 'children') continue //单独处理
if (key === 'style') {
let styleObj = props[key]
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr]
}
} else if (key.startsWith('on')) { //这里对事件方法进行处理
dom[key.toLocaleLowerCase()] = props[key]
} else {
dom[key] = porps[key]
}
}
}
/**
* 使用虚拟dom属性更新刚刚创建出来的真实DOM属性
* @param {*} vdom 真实DOM
* @param {*} props 新属性对象
*/
function updateProps(dom,newProps,oldProps){
for(let key in newProps){
if(key==='children')continue;//单独处理
if(key==='style'){
let styleObj=newProps[key];
for(let attr in styleObj){
dom.style[attr]=styleObj[attr];
}
}else if(key.startsWith('on')){
// dom[key.toLocaleLowerCase()]=props[key]
addEvent(dom,key.toLocaleLowerCase(),newProps[key])
}else{
dom[key]=newProps[key]
}
}
}
addEvent方法:
/**
* 给真实DOM添加事件处理函数
*
*/
export const addEvent = function addEvent(dom, eventType, listener) {
let store = dom.store || (dom.store = {})
store[eventType] = listener
if (!document(eventType)) {
//事件委托
document(eventType) = dispatchEvent //
}
}
let syntheticEvent = {}
function dispatchEvent(event) {
const { target, type } = event
let eventType = `on${type}`
updateQueue.isBatchUpdate = true //把队列设置为批量更新模式
syntheticEvent = createSyntheticEvent(event)
//事件冒泡到target上
while (target) {
let { store } = target
let listener = store && store[eventType]
listener && listener.call(target, syntheticEvent)
target = target.parentNode
}
for (let key in syntheticEvent) {
//syntheticEvent始终只有一个
syntheticEvent[key] = null
}
updateQueue.batchUpdate() //批量更新一下
}
function createSyntheticEvent(navtiveEvent) {
for (let key in navtiveEvent) {
//把原生事件都合成到syntheticEvent对象中
syntheticEvent[key] = navtiveEvent[key]
}
return syntheticEvent
}
这里所有的事件对象都委托到document上。在dispatchEvent方法中,需要处理的事情:
-
把队列设置为批量更新模式。
pdateQueue.isBatchUpdate = true -
合成事件处理
syntheticEvent = createSyntheticEvent(event) function createSyntheticEvent(navtiveEvent) { for (let key in navtiveEvent) { //把原生事件都合成到syntheticEvent对象中 syntheticEvent[key] = navtiveEvent[key] } return syntheticEvent } -
执行事件
listenerwhile (target) { let { store } = target let listener = store && store[eventType] listener && listener.call(target, syntheticEvent) target = target.parentNode } -
执行事件后把
syntheticEvent事件清空for (let key in syntheticEvent) { //syntheticEvent始终只有一个 syntheticEvent[key] = null } -
批量更新缓存的状态
updateQueue.batchUpdate() //批量更新一下const updateQueue={ isBatchUpdate:false,//是否批量更新 updaters:[], batchUpdate(){ //批量更新 for(let updater of this.updater){ updater.updateClassComponent() } this.isBatchUpdate=false; this.updaters.length=0; } }