React学习第六天---Virtual DOM 及 Diff 算法(类组件的更新)(五)

877 阅读9分钟

这是我参与更文挑战的第18天,活动详情查看: 更文挑战

今天我们要学习的内容是类组件的更新:

  • 实现setState防范实现类类组件的更新
  • 组件更新为不是同一个组件的情况
  • 更新组件和旧组件是同一个组件的情况

使用setState方法实现类组件更新

1) 先声明有state状态的一个类组件

  1. 首先在src/index.js设置一个类组件Alert,设置类组件state有一个Title属性,该属性当按钮点击的时候会执行handleClick函数,将调用setState({title: "changed Title"})方法改变title属性。 我们怎么实现setState可以改变state的值并且更新组件DOM呢?
class Alert extends TinyReact.Component {
  constructor(props) {
    super(props)
    this.state = {
      title: "Default Title"
    }
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
    this.setState({title: "changed Title"})
  }

  render() {
    console.log(this.state)
    return (
      <div>
        {this.props.name}
        {this.props.age}
        <div>
          {this.state.title}
          <button onClick={this.handleClick}>
            改变Title
          </button>
        </div>
      </div>
    )
  }
}
TinyReact.render(<Alert name="张三" age={20} />, root);

2) 实现setState:

  • 既然是使用setState方法来实现组件的更新,所以我们应该继续编写Component类,完成setState更新DOM这个需求。所以在Component类里面添加一个方法setState,来完成state值的更新。
setState() {
    // 将新的state的值合并到state中,完成state更新操作
     this.state = Object.assign({}, this.state, state)
}
  • 接下来完成DOM随着state改变完成更新,需要做到DOM完成更新必须要找到当前更新DOM时生成的VirtualDOM,和已经渲染成的oldDOM,拿到这两个DOM进行比对完成更新。当state改变后,我们可以使用子类组件(Alert组件)的render方法,拿到新的VirtualDOM。
setState() {
 // 将新的state的值合并到state中,完成state更新操作
  this.state = Object.assign({}, this.state, state)
 // 获取最新的要渲染的 VirtualDOM对象
 let virtualDOM = this.render()
}
  • 怎么拿到类组件的oldDOM呢? 我们先找到mountNativeElement.js这文件,可以知道页面中旧的DOM对象是通过container.appendChild(newElement)添加到页面中的,也就是说这个newElement就是页面当中显示的这个旧的DOM,我们知道这个是oldDOM,但是怎么将这个oldDOM传递到类组件实例对象当中呢?

  • 在Component类当中定义一个方法,方法名为setDOM,通过setDOM传递一个参数dom可以将oldDOM对象存储在类组件的实例对象当中,也就是在mountNativeElement函数中调用Component的setDOM方法,将oldDOM传递类组件实例对象当中。然后再通过getDOM方法返回oldDOM实例对象。

  • 又有一个问题我们怎样在mountNativeElement我们怎样才能调用到setDOM方法,要调用setDOM方法,我们必须要获取类组件实例对象,然后通过这个类组件实例对象才能拿到setDOM方法。我们所说的组件实例对象指的不是Component这个类生成的实例,因为在我们代码实现中并没有new Component()实例化Component,实例化的是他的子类,我们获取Component的子类也可以获取setDOM方法。在mountComponent.js中有一个buildClassComponent函数,我们在这个函数里面就拿到了组件实例对象,这个方法会返回一个virtualDOM,这个返回的virtualDOM就是nextVirtualDOM,这个nextVirtualDOM刚好也会传递到mountNativeElement第一个参数中,我们可以把组件实例对象conponent挂载到nextVirtualDOM中就可以传到mountNativeElement方法中,在在里面就可以调用setDOM方法,将页面的oldDOM设置到Component组件中。

  • 代码实现

//mountComponent.js
function buildClassComponent(virtualDOM) {
 const component = new virtualDOM.type(virtualDOM.props || {});
 const nextVirtualDOM = component.render()
 nextVirtualDOM.component = component // 将组件实例对象挂载到类组件nextVirtualDOM中,传入mountNativeElement中
 return nextVirtualDOM
}

// mountNativeElement.js

export default function mountNativeElement(virtualDOM, container, oldDOM) {
 let newElement = createDOMElement(virtualDOM);

 ···
 
 // 获取类组件实例
 let component = virtualDOM.component
  // 获取类组件实例,有则调用其setDOM将对应的页面的oldDOM设置到Conponent中
  // 为什么需要判断,因为还有可能是函数组件,函数组件是不没有setDOM
 if (component) {
   component.setDOM(newElement);
 }
}

// Component.js
export default class Component {
 constructor (props) {
   this.props = props
 }
 setState(state) {
   this.state = Object.assign({}, this.state, state)
   // 获取最新的要渲染的 VirtualDOM对象
   let virtualDOM = this.render()
   // 获取旧的 virtualDOM 对象 进行比对
   let oldDOM = this.getDOM()
   console.log(oldDOM)
 }
 setDOM(dom) {
   // 设置oldDOM(页面的VirtualDOM对象)
   this._dom = dom
 }
getDOM() {
   return this._dom
 }
  • 获取到oldDOM 和 当前state更新了的之后新的virtualDOM,接下来将他们进行比对完成更新DOM操作。很明显,调用diff方法即可,这里可以通过oldDOM的parentNode获取diff第二个参数contanier组件容器(组件挂载点)
setState(state) {
    this.state = Object.assign({}, this.state, state);
    // 获取最新的要渲染的 VirtualDOM对象
    let virtualDOM = this.render();
    // 获取旧的 virtualDOM 对象 进行比对
    let oldDOM = this.getDOM();
    // 获取容器
    let container = oldDOM.parentNode;
    // 实现对象组件的更新
    diff(virtualDOM, container, oldDOM);
  }

image.png 点击按钮

image.png

2. 组件更新

完成组件更新,应该在diff方法中判断要更新的VirtualDOM时候是组件。

如果是组件在判断要更新的组件和未更新前的组件是否是同一个组件,如果不是同一个组件就不需要做组件更新操作,直接代用mountElement方法将组件返回的VirtualDOM添加到页面中

如果是同一个组件,就执行更新组件操作,其实就是将最新的props传递到组件中,再调用组件的render方法获取组件返回的最新的VirtualDOM对象,再将VirtualDOM对象传递给diff方法,让diff方法找出差异,从而将差异更新到真实DOM对象中。

在更新组件的过程中还要在不同阶段调用其不同的组件生命周期函数。

接下来一步步实现我们上面的思路:

  • 在diff方法中判断要更新的VirtualDOM是否是组件,如果是组件,新增diffComponent方法进行处理,在这个方法里面完成组件更新.在diffComponent.js文件中声明一个方法isSameComponent判断是否是同一个组件,分两种情况去处理。

diffComponent接收四个参数:

  • virtualDOM: 组件本身的virtual对象,通过它可以获取到组件最新的props
  • oldComponent: 要更新的组件的实例对象,通过它可以代用组件的生命周期函数,可以更新组件的props属性,可以获取到组按键返回最新的VirtualDOM
  • oldDOM 要更新的DOM对象,在更新组件时 需要在已有DOM对象身上进行修改 实现DOM最小化操作,获取旧的VirtualDOM对象
  • container 如果要更新的组件和旧组件不是同一个组件,要直接将组建返回的VirtualDOM显示在页面中,此时需要container作为父级容器
//diff.js
else if (typeof virtualDOM.type === "function") {
    // 组件
    diffComponent(virtualDOM, oldComponent, oldDOM, container)
}
export default function diffComponent (
  virtualDOM,
  oldComponent,
  oldDOM,
  container
) {
  console.log("boolean", isSameComponent(virtualDOM, oldComponent));
  // 判断是否是同一个组件
  if(isSameComponent(virtualDOM, oldComponent)) {
    // 同一个组件,做组件更新操作
    console.log('同一个组件')
  } else {
    // 不是同一个组件
    console.log("不是同一个组件");
  }
}

function isSameComponent (virtualDOM, oldComponent) {
  return oldComponent && virtualDOM.type === oldComponent.constructor
}

不是同一个组件

  • 如果要更新的组件和旧组件不是同一个组件,要直接将组建返回的VirtualDOM显示在页面中,我们直接使用当前virtualDOM,和container两个参数调用mountELement方法完成virtualDOM组件的渲染
if(isSameComponent(virtualDOM, oldComponent)) {
   // 同一个组件,做组件更新操作
   console.log('同一个组件')
 } else {
   // 不是同一个组件
   console.log("不是同一个组件");
   mountElement(virtualDOM, container);
 }

但是这样会出现会出现一个问题,虽然新的virtualDOM完成渲染,但是oldDOM还在页面上,所以我们要扩展mountELement功能,将oldDOM传进去,将oldDOM进行删除。完成不同组件更新。

// diffComponent.js
else {
   // 不是同一个组件
   console.log("不是同一个组件");
   mountElement(virtualDOM, container, oldName);
 }
// mountElement.js
export default function mountElement(virtualDOM, container, oldDOM) {
 // Component VS NativeElement
 if(isFunction(virtualDOM)) {
   // Component
   mountComponent(virtualDOM, container, oldDOM); // 传递old在mountComponent调用mountNativeElement的时候进行删除
 } else {
   // NativeElement
   mountNativeElement(virtualDOM, container, oldDOM);// 传递old在mountNativeElement的时候进行删除
 }
 
}

export default function mountNativeElement(virtualDOM, container, oldDOM) {
 let newElement = createDOMElement(virtualDOM);

 // 判断旧的DOM对象是否存在,如果存在,删除
 if(oldDOM) {
   unmountNode(oldDOM)
 }

同一个组件

  • 同一个组件更新的方法,我们命名为updateComponent,同时需要四个参数virtualDOM,oldComponent,oldDOM,container。在类Component声明一个方法updateProps,用来更新组件实例的props。使用oldComponent这个组件实例调用updateProps完成类组件的props的更新,更新之后,将组件实例放到待渲染的nextVirtualDOM中,然后执行diff方法完成组件的更新。
export default function updateComponent(virtualDOM, oldComponent, oldDOM, container) {
  // 组件更新
  oldComponent.updateProps(virtualDOM.props)
  let nextVirtualDOM = oldComponent.render()
  nextVirtualDOM.component = oldComponent
  diff(nextVirtualDOM, container, oldDOM)
  // 更新props
}

  • 我们还需要做一些事情,在组件更新的时候我们还需要使用一些生命周期函数。我们将这些周期函数都放于Component类中,当子组件使用的时候,直接重写即可。在updateComponent中会优先调用子组件的生命周期函数,如果子组件没重写则调用Component中的生命周期方法。updateComponent使用组件实例对象oldComponent调用相应周期函数
  • 最开始执行周期函数componentWillReceiveProps
  • 然后执行shouldComponentUpdate查看组件属性是否更新,更新则往下执行
  • 将未更前前的props传递给componentWillUpdate周期函数
  • 最后完成比对之后调用componentDidUpdate方法
// Component.js
export default class Component {
  constructor(props) {
    this.props = props;
  }
  ···
  updateProps(props) {
    this.props = props;
  }
  // 生命周期函数
  componentWillMount() {}
  componentDidMount() {}
  componentWillReceiveProps(nextProps) {}
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps != this.props || nextState != this.state;
  }
  componentWillUpdate(nextProps, nextState) {}
  componentDidUpdate(prevProps, preState) {}
  componentWillUnmount() {}
}

// updateComponent.js
import diff from "./diff"

export default function updateComponent(virtualDOM, oldComponent, oldDOM, container) {
  oldComponent.componentWillReceiveProps(virtualDOM.props);
  if (oldComponent.shouldComponentUpdate(virtualDOM.props)) {
    // 未更前前的props
    let prevProps = oldComponent.props;
    oldComponent.componentWillUpdate(virtualDOM.props);
    // 组件更新
    oldComponent.updateProps(virtualDOM.props);
    // 获取组件返回的最新的 virtualDOM
    let nextVirtualDOM = oldComponent.render();
    // 更新 component 组件实例对象
    nextVirtualDOM.component = oldComponent;
    // 比对
    diff(nextVirtualDOM, container, oldDOM);
    oldComponent.componentDidUpdate(prevProps);
  }
}

image.png

今天主要是对类组件更新进行一个实现,最开始我们使用setState进行改变组件状态来改变组件的更新,先实现新的state值和组件旧的state进行合并,然后通过更新的值获取最新的virtualDOM,然后通过setDOM和getDOM方法,在mountNativeElement调用setDOM给Component存储oldDOM对象,在通过getDOM渠道oldDOM,最后调用diff完成组件更新。然后完成一个类组件的更新,分为两种情况,一种是不同组件的时候,只需要要直接将组建返回的VirtualDOM显示在页面中,调用mountElement即可,注意给一个问题这样只会把新的VirtualDOM渲染到页面中,但是还要删除oldDOM,所以将oldDOM传递给mountELement完成删除oldDOM操作。另外相同组件情况就是替换掉组件属性即可,所以在Component类中声明一个updateProps,在updateComponent函数中使用oldComponnet组件实例调用updateProps完成属性更新使用diff完成比对进行DOM更新,最后还要在更新组件中嵌入周期函数,在Component中声明好,子组件也可以调用。在updateComponent设定好循序,完成周期函数钩子。

这是今天全部内容,第7天我们结束Virtual DOM 及 Diff 算法:

  • 实现ref属性获取元素DOM对象获取组件实例对像
  • 使用Key属性进行节点对比
  • 删除节点

谢谢点赞讨论,敬请期待.....