测不准

V1

2022/04/22阅读:35主题:红绯

react学习(七)子组件生命周期实现

react学习(七)子组件生命周期实现

上一节我们学习了 react 类组件的生命周期实现,其实就是基于 js 单线程机制,在操作的的节点插入自己的实现,唯一的方法。vue 也是同样的道理,但是 vue 做了数组合并处理。本节我们了解下含有子组件时的生命周期实现。

定义组件结构

父组件如下,我们主要关注打印结果

// src/index.js
class Counter extends React.Component {
  // 如果没有 shouldComponentUpdate 的 nextProps 属性默认会打印这里,影响到不大
  static defaultProps = {
    name: "aa",
  };
  constructor(props) {
    super(props);
    this.state = {
      number: 0,
    };
    console.log("init");
  }
  componentWillMount() {
    console.log("willMount");
  }
  componentDidMount() {
    console.log("didMount");
  }
  shouldComponentUpdate(nextProps, nextState) {
    console.log("shouldUpdate", nextProps, nextState);
    return nextState.number % 2 === 0; // 返回 boolean
  }
  componentWillUpdate() {
    console.log("willUpdate");
  }
  componentDidUpdate() {
    console.log("didUpdate");
  }
  handleClick = () => {
    this.setState({
      number: this.state.number + 1,
    });
  };
  render() {
    console.log("render");
    return (
      // 没有判断 <>
      <div>
        <div>{this.state.number}</div>
        <p>————————————————————-</p>
        {this.state.number === 4 ? null : (
          <Child count={this.state.number}></Child>
        )}
        <button onClick={this.handleClick}>+</button>
      </div>
    );
  }
}

子组件结构如下,观察状态改变后打印结果

// src/index.js
class Child extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 0,
    };
    console.log("child init");
  }
  componentWillMount() {
    console.log("child willMount");
  }
  componentDidMount() {
    console.log("child didMount");
  }
  shouldComponentUpdate(nextProps, nextState) {
    console.log("child shouldUpdate", nextProps, nextState);
    return nextState.count % 3 === 0; // 返回 boolean
  }
  componentWillUpdate() {
    console.log("child willUpdate");
  }
  componentDidUpdate() {
    console.log("child didUpdate");
  }
  componentWillReceiveProps(newProps) {
    console.log("child componentWillReceiveProps");
  }

  componentWillUnmount() {
    console.log("child will unmount");
  }
  render() {
    console.log("child render");
    return <div>{this.props.count}</div>;
  }
}

官方库的打印如下(mac 如果有好用的 gif 录制软件谢谢推荐):

实现

我们注意到子组件有 shouldComponentUpdatecomponentWillReceiveProps 两个不同的生命周期,componentWillUnmount 也在父组件中有判断是否显示子组件。所以我们从 setState 方法入手,找到 forceUpdate 方法内部的 compareTwoVdom 方法,跟进数据流动。

注意:因为我们本节涉及到子组件的挂载和删除,所以这里做一个简单的 diff 比较,只跟索引绑定一对一对比,dom 类型相同直接复用,否则删除。详细的 diff 算法会在后面的章节中书写。

// src/react-dom.js
我们上一节中对组件进行了完全替换,这样就不能很好的对比子组件也不好拿到新的属性,需要改写
export function compareTwoVdom(parentDOM, oldVdom, newVdom) {
  let newDOM = createDOM(newVdom);
  parentDOM.replaceChild(newDOM, oldDOM);
}

// 大家看的时候顺序看,广度的看,不要纠结一个方法看到底。捋清思路

// 简单的 diff 对比。[1, 2, 3]  [1,3] 从头到尾一一对比
function compareTwoVdom(parentDOM, oldVdom, newVdom) {
  // 没有新 老节点
  if (!oldVdom && !newVdom) return null
  // 老节点存在,没有新节点, 直接删除
  if (oldVdom && !newVdom) {
    unMountVdom(oldVdom) // 下面实现
  } else if (!oldVdom && newVdom) {
    // 没有老节点,有新节点,新增
    let newDOM = createDOM(newVdom) // 创建新 dom
    parentDOM.appendChild(newDOM) // 插入新dom (有点小问题看大家能否发现)
    // 上一行已经挂载到页面了,所以如果有 didmount 方法,直接执行
    if (newDOM.componentDidMount) {
      newDOM.componentDidMount()
    }
  } else if(oldVdom && newVdom && oldVdom.type !== newVdom.type) {
    // 新老节点 类型不同
    unMountVdom(oldVdom) // 直接卸载 老节点
    let newDOM = createDOM(newVdom);
    parentDOM.appendChild(newDOM); // bug
    if (newDOM.componentDidMount) {
      newDOM.componentDidMount();
    }
  } else {
    // 新的有 老节点也有,类型也一样,需要复用
    updateElement(oldVdom, newVdom)
  }
}

实现辅助方法

  • 卸载
function unMountVdom(vdom) {
  let {classInstance, props, ref} = vdom
  let currentDOM = findDOM(vdom)
  // 将要卸载方法  相信大家应该有感觉了,能理解了
  if (classInstance && classInstance.componentWillUnmount) {
    classInstance.componentWillUnmount()
  }
  
  // 引用类型 都要清空,会影响
  if(ref) {
    ref.current = null
  }
  
  // 递归卸载子, 而没有直接清空父组件innerhtml,子里面可能还有类组件,继续执行卸载方法
  if (props.children) {
    let children = Array.isArray(props.children) ? props.children : [props.children]
    children.forEach(unMountVdom)
  }
  // 从父组件中移除
  if(currentDOM) currenDOM.parentNode.removeChild(currentDOM)
}
  • 节点复用更新
function updateElement(oldVdom, newVdom) {
  if(oldVdom.type === REACT_TEXT) {
    // 文本节点 内容不同直接替换
    let currentDOM = newVdom.dom = findDOM(oldVdom)
    if (oldVdom.props !== newVdom.props) {
      currentDOM.textContent = newVdom.props
    }
  } else if (typeof oldVdoml.type === 'string') {
    // 原生标签 div。p h1
    let currentDOM = newVdom.dom = findDOM(oldVdom)
    // 更新属性, 我们上节课写过
    updateProps(currentDOM, oldVdom.props, newVdom.props)
    // 递归的对比子
    updateChildren(currentDOM, oldVdom.props.children, newVdom.props.children)
  } else if (typeof oldVdom.type === 'function') {
    // 函数组件或类组件
    if (oldVdom.type.isReactComponent) {
      updateCLassComponent(oldVdom, newVdom)
    } else {
      updateFunctionComponent(oldVdom, newVdom)
    }
  }
}
  • 更新子节点
function updateChildren(parentDOM, oldVChildren, newVChildren) {
  // 子可能是数组可能是对象
  oldVChildren = Array.isArray(oldVChildren) ? oldVCHIldren : [oldVChildren]
  newVChildren = Array.isArray(newVCHildren) ? newVCHIldren : [newVChildren]
  
  // 获取长度最大值
  let maxLen = Math.max(oldVChildren.length, newVChildren.length) 
  for(let i = 0;i<maxLen;i++){
    // 递归就是父已经实现了,获取到子,再重新执行一遍父执行过的方法
    compareTwoVdom(parentDOM, oldVChildren[i], newVChildren[i])
  }
}
  • 更新类组件
function updateClassComponent(oldVdom, newVdom) {
  // 实例可以复用
  const classInstance = newVdom.classInstance = oldVdom.classInstance
  if (classInstance.componentWillReceiveProps) {
    // 子组件更新,可以获取属性
    classInstance.componentWillReceiveProps()
  }
  // 触发类的更新,传入新的属性
  classInstace.updater.emiUpdate(newVdom.props)
}
  • 更新函数组件
function updateFunctionComponent(oldVdom, newVdom) {
  let currentDOM = findDOM(oldVom)
  if (!currentDOM) return null
  let {type, props} = newVdom
  // 函数组件的更新就在于重新执行一遍函数获取新的虚拟dom
  let newRenderVdom = type(props)
  newVdom.oldRenderVdom = newRenderVdom
}

我们修改下类组件更新出发的方法

// src/component.js
emitUpdate(nextProps) {
    // 我们对新属性做下存储,更新时获取
    this.nextProps = nextProps;
    ....
    
updateComponent() {
  const { classInstance, pendingStates, nextProps } = this;
  // 等待更新的状态有多个     有新属性 或者新状态就会更新
  if (nextProps || pendingStates.length) {
    // 获取新状态
    let newState = this.getState();
    shouldUpdate(classInstance, nextProps, newState); // 是否更新
  }
  ...
}


function shouldUpdate(classInstance, nextProps, newState) {
  ...
  if (
    classInstance.shouldComponentUpdate &&
    // newProps 后面在加
    !classInstance.shouldComponentUpdate(
      nextProps || classInstance.constructor.defaultProps, // 这里返回新的属性,没有的话返回构造函数的默认属性
      newState
    )
  ) {
    willUpdate = false;
  }
  if (willUpdate && classInstance.componentWillUpdate) {
    classInstance.componentWillUpdate();
  }

  // 实例属性重新赋值
  if (nextProps) {
    classInstance.props = nextProps;
  }
  ...
}

我们自己的代码实现如下:和原生的打印结果一样

大家可能会有疑问,我都有 vdom 了,为什么还有个 renderVdom 的概念呢?vdom 指的是我们引用的如 <Counter /> 的解析结果,renderVdom 是具体的 render 方法返回的虚拟 dom,两者是不同的

至此我们的子组件生命周期基本实现完了,整片文章代码居多,但是必要的地方都写了注视,相信看过之前文章的小伙伴也可以梳理清楚。如果真的有疑问,可以在面留言,我会进行解答的。写一小节我们写出真实的 diff 算法,加深大家对虚拟 dom 的认识,谢谢阅读!

分类:

前端

标签:

React.js

作者介绍

测不准
V1