【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')
  );
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章