前言
本篇來了解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= 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' ) 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] ) )
}
}
}