【React】React源碼梳理筆記(七)

前言

  • 還有些別的功能繼續完成。

實現ref

  • 前面在做虛擬dom時,裏面會單獨把ref提取出來,這次就用上了。
  • ref時初始化時用createRef造個,vdom上賦上 ,打印時就可以在current裏獲取。
class Counter extends React.Component{
  static defaultProps = {name:'yehuozhili'}
  constructor(props){
    super(props)
    this.state={number:0}
    this.wrapper=React.createRef()
  }
  handleClick=()=>{
    console.log(this.wrapper)
    this.setState((state)=>({number:state.number+1}))
  }
  render(){
    console.log('render')
    return (
      <div ref={this.wrapper}>
        <p>{this.state.number}</p>
        {
          this.state.number>3?null:<Child number={this.state.number}></Child>
        }
        <button onClick={this.handleClick}>+</button>
      </div>
    )
  }
}
  • 一共有2個地方可以賦ref,一個是類組件實例,一個是原生dom:
function createNativeDOM(element){
    let {type,props,ref} =element
    let dom = document.createElement(type)
    createNativeDOMChildren(dom,element.props.children)
    setProps(dom,props)
    if(ref){
        ref.current=dom
    }
    return dom 
}
function createClassComponetDOM(element){
    let {type,props,ref}=element
    let componentInstance =new  type(props)
    if(ref){
        ref.current=componentInstance
    }
    if(componentInstance.componentWillMount){
        componentInstance.componentWillMount()
    }
    if(type.getDerivedStateFromProps){
        let newState =type.getDerivedStateFromProps(props,componentInstance.state)
        if(newState){
            componentInstance.state={...componentInstance,...newState}
        }
    }
    let renderElement = componentInstance.render()
    componentInstance.renderElement=renderElement
    element.componentInstance=componentInstance
    let newDom =createDOM(renderElement)
    if(componentInstance.componentDidMount){
        componentInstance.componentDidMount()
    }
    return newDom
}
  • 加個判斷就可以了。然後createRef實際就是創建個對象
function createRef(){
    return {current :null}
}
  • 這樣就能取到真實dom了。

實現context

  • context應用有以下幾步:
  • 使用createContext創建context
let ThemeContext = React.createContext(null);
  • 使用context.provider包裹渲染組件,並傳遞value,製作生產者。
class FunctionPage extends Component {
    constructor(props) {
        super(props);
        this.state = { color: 'red' };
    }
    changeColor = (color) => {
        this.setState({ color });
    }
    render() {
        let contextVal = { changeColor: this.changeColor, color: this.state.color };
        return (
            <ThemeContext.Provider value={contextVal}>
                <div style={{ margin: '10px', border: `5px solid ${this.state.color}`, padding: '5px', width: '200px' }}>
                    page
                    <FunctionHeader />
                    <FunctionMain />
                </div>
            </ThemeContext.Provider>

        )
    }
}
  • 消費者通過context的consumer包裹,即可得到生產者提供參數
class FunctionHeader extends Component {
    render() {
        return (
            <ThemeContext.Consumer>
                {
                    (value) => (
                        <div style={{ border: `5px solid ${value.color}`, padding: '5px' }}>
                            header
                            <FunctionTitle />
                        </div>
                    )
                }
            </ThemeContext.Consumer>
        )
    }
}
  • 實現createContext:
function createContext(defaultValue){
    Provider.value=defaultValue//相當於做個閉包,把這個值放這
    function Provider(props){
        Provider.value=props.value
        return props.children
    }
    function Consumer(props){//消費者其實是個代理攔截
        return  props.children(Provider.value)
    }
    return {Provider,Consumer}
}
  • 這樣就完成了,實際上,被這個組件包裹的元素,也就是這個組件的props.children原本是要渲染的元素,相當於做了個代理,組件內的中間件。
  • 在類組件裏,如果掛了靜態屬性contextType,還可以通過this.context.xxx來取得context的屬性:
class Header extends Component {
  static contextType = ThemeContext;
  render() {
      return (
          <div style={{ border: `5px solid ${this.context.color}`, padding: '5px' }}>
              header
              <Title />
          </div>
      )
  }
}
  • 就是在創建類虛擬dom裏把靜態屬性裏面對象的那個值拿出來,賦給實例的context。
function createClassComponetDOM(element){
    let {type,props,ref}=element

    let componentInstance =new  type(props)
    if(type.contextType){
        componentInstance.context =type.contextType.Provider.value
    }
    if(ref){
        ref.current=componentInstance
    }
    if(componentInstance.componentWillMount){
        componentInstance.componentWillMount()
    }
    if(type.getDerivedStateFromProps){
        let newState =type.getDerivedStateFromProps(props,componentInstance.state)
        if(newState){
            componentInstance.state={...componentInstance,...newState}
        }
    }
    let renderElement = componentInstance.render()
    componentInstance.renderElement=renderElement
    element.componentInstance=componentInstance
    let newDom =createDOM(renderElement)
    if(componentInstance.componentDidMount){
        componentInstance.componentDidMount()
    }
    return newDom
}
  • 同時走更新時也要加上:
function updateClassComponent(oldelement,newelement){
    let componentInstance = oldelement.componentInstance//拿到實例
    let updater = componentInstance.updater//實例裏new的那個updater
    let nextProps = newelement.props  // 新的屬性
    if(oldelement.type.contextType){
        componentInstance.context=oldelement.type.contextType.Provider.value
    }
    if(componentInstance.componentWillReceiveProps){
        componentInstance.componentWillReceiveProps(nextProps)
    }
    if(newelement.type.getDerivedStateFromProps){
        let newState =newelement.type.getDerivedStateFromProps(nextProps,componentInstance.state)
        if(newState){
            componentInstance.state={...componentInstance,...newState}
        }
    }
    updater.emitUpdate(nextProps)//setstate會走這個判斷,這個同樣直接判斷.讓updater去更新類組件
}
  • 因爲實例上的context還是老狀態,更新了需要用虛擬dom上的新狀態把老狀態換掉。

將生命週期改爲批處理

  • 由於前面寫的生命週期並不是批處理更新,所以需要更改下。
  • 其中react-dom裏能解構一個方法,叫unstable_batchedUpdates。這個就相當於批量更新的開關。
  • 先實現這個方法,然後把生命週期裏的函數傳給它就行了:
function unstable_batchedUpdates(fn){
  updateQueue.ispending=true
  fn()
  updateQueue.ispending=false
  updateQueue.batchUpdate()
}
  • 然後改生命週期:
    if(componentInstance.componentDidMount){
        unstable_batchedUpdates(()=>componentInstance.componentDidMount())
    }
  • 其他生命週期函數也這麼處理就完事了。

修復空數組bug

  • 前面寫的還有2個bug,就是對於字符串和產生空數組會有點問題。需要改這幾個bug:
  • createElement判斷有點問題,另外如果空數組情況,就不傳children了
export function createElement(type, config, children) {
    let propName;
    const props = {};
  
    let key = null;
    let ref = null;
    if (config != null) {
        /////////挑出ref 和key 
      if (hasValidRef(config)) {
        ref = config.ref;
      }
      if (hasValidKey(config)) {
        key = '' + config.key;
      }
      // 剩餘屬性放入Props
      for (propName in config) {
        if (
          hasOwnProperty.call(config, propName) &&
          !RESERVED_PROPS.hasOwnProperty(propName)
        ) {
          props[propName] = config[propName];
        }
      }
    }
    //孩子長度等於參數總長減去開頭2個
    const childrenLength = arguments.length - 2;
    if (childrenLength === 1) {//一個孩子就給children / /只有字符串或者數字渲染都是一樣
        if(typeof children==='string'||typeof children==='number'){
          children ={$$typeof:REACT_TEXT_TYPE,key:null,content:children,type:REACT_TEXT_TYPE,ref:null,props:null}
        }
        if(!(children instanceof Array &&children.length===0)){
          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,content:children,type:REACT_TEXT_TYPE,ref:null,props:null}
            childArray[i] = arguments[i + 2];
        }
        props.children = childArray;//多個就變成數組
    }
    //如果存在標籤的defaultProps屬性(類的靜態屬性),並且這個屬性config裏沒傳,那麼也賦給props
    if (type && type.defaultProps) {
        const defaultProps = type.defaultProps;
        for (propName in defaultProps) {
        if (props[propName] === undefined) {
            props[propName] = defaultProps[propName];
        }
        }
    }
    return ReactElement(
        type,
        key,
        ref,
        ReactCurrentOwner.current,
        props,//除了保留屬性的其餘屬性 孩子在children上
    );
}
  • 做Map時的判斷,如果是undefined直接return個空Map:
function getChildrenElementsMap(oldChildrenElements){
    let oldChildrenElementsMap={}
    if(!oldChildrenElements){
        return{}
    }
    if(!oldChildrenElements.length){//如果沒有length說明是單個虛擬dom
        oldChildrenElements=[oldChildrenElements]
    }
    for(let i=0 ;i<oldChildrenElements.length;i++){
        let oldkey = oldChildrenElements[i]?.key ||i.toString()
        oldChildrenElementsMap[oldkey]=oldChildrenElements[i]
    }
    return oldChildrenElementsMap
}
  • 這樣就ok了

修復渲染時propsbug

  • 在render下如果新屬性沒有變動,但是組件渲染了,會導致render下新的屬性是undefined,所以要加個判斷就好了:
function shouldUpdate(componentInstance,nextProps,nextState){//nextState就是最新state
    let nextPropsT = nextProps?nextProps:componentInstance.props
    let scu=componentInstance.shouldComponentUpdate&&!componentInstance.shouldComponentUpdate(nextPropsT,nextState)
    componentInstance.props =nextPropsT
    componentInstance.state = nextState//讓其有新屬性
    if(scu){
        return false
    }    
    componentInstance.forceUpdate()
}

修復更新屬性不正確bug

  • 由於在diff時,老虛擬dom需要更新屬性,再進行復用,如果忘記更新屬性,可能導致每次渲染量都在原來基礎上增加。
  • 修改updateElement函數:
function updateElement(oldelement,newelement ){
    let currentDom =newelement.dom =oldelement.dom //在這裏的就可以複用dom了。
    if(oldelement.$$typeof===REACT_TEXT_TYPE&&newelement.$$typeof===REACT_TEXT_TYPE){//文本比較
        if(currentDom.textContent!==newelement.content)currentDom.textContent=newelement.content//修改文本
    }else if(oldelement.$$typeof===REACT_ELEMENT_TYPE){//元素類型
        updateDomProperties(currentDom,oldelement.props,newelement.props)
        updateChildrenElements(currentDom,oldelement.props.children,newelement.props.children)//比對子節點
        oldelement.props=newelement.props
    }else if(oldelement.$$typeof===FUNCTION_COMPONENT){//類型都是函數組件
        updateFunctionComponent(oldelement,newelement)
    }else if(oldelement.$$typeof===CLASS_COMPONENT){
        updateClassComponent(oldelement,newelement)
    }
}
  • 這樣就基本沒有bug了,可以拿下面案例試一下,能正確添加刪除就ok。
class Todos extends React.Component {
      constructor(props) {
          super(props);
          this.state = { list: [], text: '' };
      }
      add = () => {
        console.log(this.state)
          if (this.state.text && this.state.text.length > 0) {
            console.log({ list: [...this.state.list, this.state.text] })
              this.setState({ list: [...this.state.list, this.state.text] });
          }
      }
      onChange = (event) => {
          this.setState({ text: event.target.value });
      }
      onDel = (index) => {
          this.state.list.splice(index, 1);
          this.setState({ list: this.state.list });
      }
      render() {
        console.log(this.state.list)
          return(
            <div>
              <input onChange={this.onChange} value={this.state.text}></input><button onClick={this.add}>+++</button>
              <ul>
                {this.state.list.map((item,index)=>{
                  return (
                    <li key={index}>{item}<button onClick={this.onDel}>x</button></li>
                  )
                })}
              </ul>

            </div>
          )
      }
  }
  let element = React.createElement(Todos, {});
  ReactDOM.render(
      element,
      document.getElementById('root')
  );
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章