前言
本篇来了解setState之谜。 react最常见的面试题就是setState到底是同步还是异步?看完这篇就知道了。
渲染类组件和函数组件
接上篇,上篇只是基本把事件给做了,下面需要渲染类组件和函数组件。 现在一共有3种形式的元素: 单独的react元素 类组件 函数组件 而单独的react元素前面已经是可以渲染出来了。在babel转译前表现形式就是<div>xxx</div>
这样。 为了便于理解,这里使用转译后的形式。
class ClassComponent extends React. Component {
render ( ) {
return React. createElement ( 'div' , { id: 'counter' } , 'hello' )
}
}
function FunctionCounter ( ) {
return React. createElement ( 'div' , { id: 'counter' } , 'hello' )
}
let element1 = React. createElement ( 'div' , { id: 'counter' } , 'hello' )
let element2 = React. createElement ( ClassComponent, { id: 'counter' } , 'hello' )
let element3 = React. createElement ( FunctionCounter, { id: 'counter' } , 'hello' )
下面渲染element2和element3。 类组件函数在第一篇 已经写了,我还画了个图,其中有个setState会调用updater的方法。它有2个方法,一个是setState,一个是forceUpdate。都是调用的updater上的方法。 但虚拟dom上创建就有点不一样了。没用fiber前还是得在ReactElement里判断。先做出几种类型:
export const REACT_ELEMENT_TYPE = Symbol. for ( 'react.element' )
export const REACT_TEXT_TYPE = Symbol. for ( 'TEXT' ) ;
export const FUNCTION_COMPONENT = Symbol. for ( 'FUNCTION_COMPONENT' )
export const CLASS_COMPONENT = Symbol. for ( 'CLASS_COMPONENT' )
前面ReactElement里是全都加的是React_ELEMENT_TYPE类型。这次做个判断。
const ReactElement = function ( type, key, ref, owner, props) {
let $$typeof
if ( typeof type=== 'function' && type. prototype. isReactComponent) {
$$typeof = CLASS_COMPONENT
} else if ( typeof type=== 'function' ) {
$$typeof = FUNCTION_COMPONENT
} else {
$$typeof = REACT_ELEMENT_TYPE
}
const element = {
$$typeof ,
type: type,
key: key,
ref: ref,
_owner: owner,
props: props,
} ;
return element;
} ;
function createFunctionDOM ( element) {
let { type, props} = element
let renderElement = type ( props)
let newDom = createDOM ( renderElement)
return newDom
}
function createClassComponetDOM ( element) {
let { type, props} = element
let componentInstance = new type ( props)
let renderElement = componentInstance. render ( )
let newDom = createDOM ( renderElement)
return newDom
}
export function createDOM ( element) {
let { $$typeof } = element
let dom = null
if ( ! $$typeof ) {
dom = document. createTextNode ( element)
} else if ( $$typeof === REACT_ELEMENT_TYPE ) {
dom = createNativeDOM ( element)
} else if ( $$typeof === FUNCTION_COMPONENT ) {
dom = createFunctionDOM ( element)
} else if ( $$typeof === CLASS_COMPONENT ) {
dom = createClassComponetDOM ( element)
}
return dom
}
可以看见函数组件直接取返回值,拿返回值调createDom,类组件new出一个实例,然后调用render拿返回值,再传给createDom。 这样就完成了渲染函数组件和类组件。
实现setState
一般setState说的是类组件那个,函数组件那个是用hooks另外说。 看一下原版使用:
import React from 'react' ;
import ReactDOM from 'react-dom' ;
class Counter extends React. Component {
constructor ( props) {
super ( props)
this . state= { number: 0 }
}
handleClick = ( ) => {
this . setState ( { number: this . state. number+ 1 } )
console. log ( this . state. number)
this . setState ( { number: this . state. number+ 1 } )
console. log ( this . state. number)
setTimeout ( ( ) => {
this . setState ( { number: this . state. number+ 1 } )
console. log ( this . state. number)
this . setState ( { number: this . state. number+ 1 } )
console. log ( this . state. number)
} ) ;
}
render ( ) {
return < button onClick= { this . handleClick} > + < / button>
}
}
ReactDOM. render (
< Counter> < / Counter> ,
document. getElementById ( 'root' )
) ;
这样点击一下按钮会打印0023。其实主要是react里面有个批量更新的玩意。会在事件流程里开启批量更新,然后在事件对象完成后关闭批量更新。现在来实现下。 在组件中调用setState实际上就是调继承的component的prototype的setstate方法。前面照源码抄来的是这样:
Component. prototype. setState = function ( partialState, callback) {
this . updater. enqueueSetState ( this , partialState, callback, 'setState' ) ;
} ;
Component. prototype. forceUpdate = function ( callback) {
this . updater. enqueueForceUpdate ( this , callback, 'forceUpdate' ) ;
} ;
所以这个调用的是this.updater,但是源码里Component的updater是传来的,所以先改成自己做的。同时将方法也改简略点。
export function Component ( props, context) {
this . props = props;
this . context = context;
this . refs = emptyObject;
this . updater = new Updater ( this )
}
Component. prototype. isReactComponent = { } ;
Component. prototype. setState = function ( partialState) {
this . updater. enqueueSetState ( partialState) ;
} ;
Component. prototype. forceUpdate = function ( ) {
console. log ( 'forceupdate' )
} ;
这里就把updater改成new出来,然后把实例传进去。一个实例即对应一个updater。 下面是updater,以及一个全局的updateQueue。
export let updateQueue= {
updaters: [ ] ,
ispending: false ,
add ( updater) {
this . updaters. push ( updater)
} ,
batchUpdate ( ) {
let { updaters} = this
this . ispending = true
let updater = updaters. pop ( )
while ( updater) {
updater. updeteComponent ( ) ;
updater = updaters. pop ( )
}
this . ispending= false
}
}
function isFunction ( obj) {
return typeof obj === 'function'
}
class Updater {
constructor ( componentInstance) {
this . componentInstance = componentInstance
this . penddingState = [ ]
this . nextProps= null
}
enqueueSetState ( partialState) {
this . penddingState. push ( partialState)
this . emitUpdate ( )
}
emitUpdate ( nextProps) {
this . nextProps= nextProps
if ( nextProps|| ! updateQueue. ispending) {
this . updeteComponent ( )
} else {
updateQueue. add ( this )
}
}
updeteComponent ( ) {
let { componentInstance, penddingState, nextProps} = this
if ( nextProps|| penddingState. length> 0 ) {
shouldUpdate ( componentInstance, nextProps, this . getState ( ) )
}
}
getState ( ) {
let { componentInstance, penddingState} = this
let { state} = componentInstance
if ( penddingState. length> 0 ) {
penddingState. forEach ( nextState => {
if ( isFunction ( nextState) ) {
state= { ... state, ... nextState. call ( componentInstance, state) }
} else {
state= { ... state, ... nextState}
}
} ) ;
}
penddingState. length= 0
return state
}
}
function shouldUpdate ( componentInstance, nextProps, nextState) {
componentInstance. props = nextProps
componentInstance. state = nextState
if ( componentInstance. shouldComponentUpdate&&
! componentInstance. shouldComponentUpdate ( nextProps, nextState) ) {
return false
}
componentInstance. forceUpdate ( )
}
简单说是这样,有个全局的一个对象里面有个队列,以及一个代表这个对象状态的标志ispending。它有个add方法就是往队列里加Updater,有个批量更新方法就是把队列里Updater拿出来执行Updater的立即更新方法。 而Updater,它有个队列,这个队列是存新状态的,当有新状态,第一件事就是存到Updater这个队列里。然后再进行一个判断,是放到queue里进行批量更新还是直接进行更新? 放到queue里的就会等待某地方调用queue的batchUpdate方法进行批量更新。而直接进行更新就直接自己进行调用更新。 在更新方法里,通过getState拿到最新的状态,传递给shouldUpdate配合其生命周期控制渲染。如果需要渲染,就走forceUpdate这个方法。这时,真正的操作dom才会来。 为了后面方便进行domdiff(react在fiber前是domdiff,fiber没有domdiff),需要前面创建虚拟dom稍微修改一下,让字符串也包裹成一个虚拟dom。这样便于方便比较。同时将真实dom也挂载到虚拟dom上。(这段准备操作很像vue的domdiff)。
if ( childrenLength === 1 ) {
if ( typeof children === 'string' || 'number' ) children = { $$typeof : REACT_TEXT_TYPE , key: null , type: children, ref: null , props: null }
props. children = children;
} else if ( childrenLength > 1 ) {
const childArray = Array ( childrenLength) ;
for ( let i = 0 ; i < childrenLength; i++ ) {
if ( typeof arguments[ i + 2 ] === 'string' ) arguments[ i + 2 ] = { $$typeof : REACT_TEXT_TYPE , key: null , type: children, ref: null , props: null }
childArray[ i] = arguments[ i + 2 ] ;
}
props. children = childArray;
}
字符串在遍历时候就可以发现,直接包裹成文本类型的虚拟dom。 然后修改创建真实dom那:
function createClassComponetDOM ( element) {
let { type, props} = element
let componentInstance = new type ( props)
let renderElement = componentInstance. render ( )
componentInstance. renderElement= renderElement
let newDom = createDOM ( renderElement)
return newDom
}
export function createDOM ( element) {
let { $$typeof } = element
let dom = null
if ( $$typeof === REACT_TEXT_TYPE ) {
dom = document. createTextNode ( element. type)
} else if ( $$typeof === REACT_ELEMENT_TYPE ) {
dom = createNativeDOM ( element)
} else if ( $$typeof === FUNCTION_COMPONENT ) {
dom = createFunctionDOM ( element)
} else if ( $$typeof === CLASS_COMPONENT ) {
dom = createClassComponetDOM ( element)
}
element. dom = dom
return dom
}
另外在事件发生时,我们需要开启批量更新,结束时关闭批量更新并调用queue的批量更新:
function dispatchEvent ( event) {
let { type, target} = event
let eventType = 'on' + type
syntheticEvent = getSyntheticEvent ( event)
updateQueue. ispending= true
while ( target) {
let { eventStore} = target
let listener = eventStore&& eventStore[ eventType]
if ( listener) {
listener. call ( target, syntheticEvent)
}
target= target. parentNode
}
for ( let key in syntheticEvent) {
if ( key!== 'persist' ) syntheticEvent[ key] = null
}
updateQueue. ispending= false
updateQueue. batchUpdate ( )
}
这样就完成了,可以打印试一下,跟原版一模一样,都是0023。 所以说,在点击按钮时,其实是开启了批量更新模式,因为事件对象先进dispatchEvent函数,然后再运行用户的setState方法,这样用户的状态会放进updater队列并存储到queue里,等待批量更新完成后再将其关闭,这个过程是个while循环,如果说同步还是异步?这里有2种情况,一种批量更新情况,应该算是同步,因为整个流程是一个同步过程,但是你后面console.log取不到。相当于这样的代码:
function a ( ) {
}
console. log ( a. yname)
a. yname= 'yehuozhili'
这代码是同步还是异步?肯定同步啊,但是console.log放前面去了而已。 另一种情况是非批量更新情况,这种情况更是同步的情况。相当于这样的代码:
function a ( ) {
}
a. yname= 'yehuozhili'
console. log ( a. yname)
最后把渲染逻辑写一下,剩下的下篇说。 可以先在button上加个id等于this.state.number来观察渲染情况。 前面componentInstance.forceUpdate就调用了渲染,完成这个逻辑:
Component. prototype. forceUpdate = function ( ) {
let { renderElement} = this
if ( this . componentWillUpdate) {
this . componenentWillUpdate ( )
}
let newRenderElement = this . render ( )
let currentElement = compareTwoElement ( renderElement, newRenderElement)
this . renderElement = currentElement
if ( this . componentDidUpdate) {
this . componentDidUpdate ( )
}
} ;
function compareTwoElement ( oldelement, newelement) {
let currentDom = oldelement. dom
let currentElement = oldelement
if ( newelement=== null ) {
currentDom. parentNode. removeChild ( currentDom)
currentDom= null
currentElement= null
} else if ( oldelement. type!== newelement. type) {
let newDom = createDOM ( newelement)
currentDom. parentNode. replaceChild ( newDom, currentDom)
currentElement= newelement
} else {
let newDom = createDOM ( newelement)
currentDom. parentNode. replaceChild ( newDom, currentDom)
currentElement= newelement
}
return currentElement
}
其中通过组件实例拿到实例上挂载的虚拟Dom,进入compare函数去比较新老虚拟dom。而虚拟dom上的dom属性正好挂载了真实Dom,所以也可以操作dom。 这个新的虚拟dom,其实是执行了实例render的结果。所以更新会走一次render。 最后那个else,先这么写,下次再写domdiff。 其实这个有点对应VUE的patch,不过patch是边比对边patch。
function patch ( oldVnode, newVnode) {
if ( newVnode. type!== oldVnode. type) {
return oldVnode. domElement. parentNode. replaceChild ( creatRealDom ( newVnode) , oldVnode. domElement)
}
if ( newVnode. text!== undefined) {
return oldVnode. domElement. textContent= newVnode. text
}
let domElement = newVnode. domElement = oldVnode. domElement
updateAttr ( newVnode, oldVnode. props)
let oldChildren = oldVnode. children
let newChildren = newVnode. children
if ( oldChildren. length> 0 && newChildren. length> 0 ) {
updateChildren ( domElement, newChildren, oldChildren)
} else if ( oldChildren. length> 0 ) {
domElement. innerHTML= ''
} else if ( newChildren. length> 0 ) {
for ( let i= 0 ; i< newChildren. length; i++ ) {
domElement. appendChild ( creatRealDom ( newChildren[ i] ) )
}
}
}