模拟 React 03 组件更新、ref、key、删除节点

组件更新

示例

function Heart(props) {
  return <div>{props.title}&hearts;</div>
}

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() {
    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)

setTimeout(() => {
  // 相同组件
  // TinyReact.render(<Alert name="李四" age={50} />, root)
  
  // 不同组件
  TinyReact.render(<Heart title="Hello React" />, root)
}, 2000)

判断是否是同一个组件

  • diff() 方法中判断要更新的 Virtual DOM 是否是组件
    • 如果不是(已经实现),则直接创建创建新的节点替换旧的节点
  • 如果是组件,要判断新旧 Virtual DOM 是否是同一个组件
    • 如果不是,则不需要更新操作,直接使用 mountElement() 方法将组件返回的 Virtual DOM 生成真实 DOM 显示到页面中,并删除旧的 DOM。
  • 如果是同一个组件,则执行更新组件操作
    • 其实就是将最新的 props 传递到组件中
    • 再调用组件的 render() 方法获取组件返回的最新的 Virtual DOM 对象
    • 将其传递给 diff() 方法,找出差异,从而将差异更新到真实 DOM 对象中。
  • 在更新组件的过程中,还要在不同阶段调用其不同的组件生命周期函数。

新增一个 diffComponnent() 方法进行判断对比,可以对比旧组件的实例对象的构造函数与新 Virtual DOM 对象的 type 属性存储的构造函数是否相同,判断是否是同一个组件。:

// src/TinyReact/diffComponent.js
/**
 * @param {*} virtualDOM 组件本身的 virtualDOM 对象:通过它可以获取组件最新的 props
 * @param {*} oldComponent 要更新的组件的实例对象:通过它可以调用组件的生命周期函数,可以更新组件的 props 属性,可以获取组件返回的最新的 Virtual DOM 对象
 * @param {*} oldDOM 要更新的 DOM 对象:在更新组件时,需要在已有 DOM 对象身上进行修改,实现DOM最小化操作,可以获取旧的 Virtual DOM 对象,如果是不同组件,则需要通过它删除旧的 DOM
 * @param {*} container 父级容器:如果要更新的组件和旧组件不是同一个组件,要直接将组件返回的 Virtual DOM 显示到页面中,此时需要父级容器
 */
export default function diffComponent(virtualDOM, oldComponent, oldDOM, container) {
  if (isSameComponent(virtualDOM, oldComponent)) {
    // 同一个组件:执行组件更新操作
  } else {
    // 不是同一个组件
  }
}

// 判断是否是同一个组件
function isSameComponent(virtualDOM, oldComponent) {
  return oldComponent && oldComponent.constructor === virtualDOM.type
}

// src/TinyReact/diff.js
import createDOMElement from './createDOMElement'
import mountElement from './mountElement'
import updateNodeElement from './updateNodeElement'
import updateTextNode from './updateTextNode'
import isFunction from './isFunction'
import unmountNode from './unmountNode'
import diffComponent from './diffComponent'
export default function diff(virtualDOM, container, oldDOM) {
  const oldVirtualDOM = oldDOM && oldDOM._virtualDOM
  const oldComponent = oldVirtualDOM && oldVirtualDOM.component
  // 判断 oldDOM 是否存在
  if (!oldDOM) { /*...*/
  } else if (
    // 对比的两个节点类型不相同
    virtualDOM.type !== oldVirtualDOM.type &&
    // 并且节点的类型不是组件,因为组件要单独处理
    !isFunction(virtualDOM)
  ) {/*...*/
  } else if (isFunction(virtualDOM)) {
    // 组件
    diffComponent(virtualDOM, oldComponent, oldDOM, container)
  } else if (virtualDOM.type === oldVirtualDOM.type) {/*...*/
  }
}

之前在mountComponent模块下的 buildClassComponent()方法中,将组件实例对象存储在了 Virtual DOM 对象上,所以可以直接获取 oldCompoenent

不同组件

如果是不同的组件,则直接执行两个操作:

  • 挂载新的 DOM:mountElement() 方法已实现
  • 删除旧的 DOM

将真实DOM挂载到页面的操作最终是在 mountNativeElement()方法中实现的。

所以要将删除旧的DOM操作添加到里面,这就需要将旧的 DOM 传递到这个方法中。

通过 mountElement[ -> mountComponent] -> mountNativeElement 的调用过程执行挂载,所以需要扩展这几个方法,让它们接收 oldDOM,并在 mountNativeElement 中执行删除旧DOM操作:

// src/TinyReact/mountElement.js
/*...*/
export default function mountElement(virtualDOM, container, oldDOM) {
  // Component VS NativeElement
  if (isFunction(virtualDOM)) {
    // Component
    mountComponent(virtualDOM, container, oldDOM)
  } else {
    // NativeElement
    mountNativeElement(virtualDOM, container, oldDOM)
  }
}

// src/TinyReact/mountComponent.js
/*...*/
export default function mountComponent(virtualDOM, container, oldDOM) {
	/*...*/

  // 判断渲染的组件是否直接返回了另一个组件
  if (isFunction(nextVirtualDOM)) {
    mountComponent(nextVirtualDOM, container, oldDOM)
  } else {
    mountNativeElement(nextVirtualDOM, container, oldDOM)
  }
}

/*...*/

// src/TinyReact/mountElement.js
/*...*/
export default function mountNativeElement(virtualDOM, container, oldDOM) {
  const newElement = createDOMElement(virtualDOM)

  // 将转换之后的 DOM 对象放置到页面中
  container.appendChild(newElement)

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

	/*...*/
}

diffComponent中调用 mountElement,并传递 oldDOM

// src/TinyReact/diffComponent.js
import mountElement from "./mountElement"

export default function diffComponent(virtualDOM, oldComponent, oldDOM, container) {
  if (isSameComponent(virtualDOM, oldComponent)) {
    // 同一个组件:执行组件更新操作
  } else {
    // 不是同一个组件
    mountElement(virtualDOM, container, oldDOM)
  }
}

// 判断是否是同一个组件
function isSameComponent(virtualDOM, oldComponent) {
  return oldComponent && oldComponent.constructor === virtualDOM.type
}

相同组件

更新组件操作

  1. 将最新的 props 传递到组件中:通过调用组件实例的 updateProps 方法
  2. 再调用组件的 render() 方法获取组件返回的最新的 Virtual DOM 对象
    1. 此时要重新存储组件实例对象
  3. 将其传递给 diff() 方法,找出差异,从而将差异更新到真实 DOM 对象中。
// src/TinyReact/diffComponent.js
import mountElement from "./mountElement"
import updateComponent from "./updateComponent"
export default function diffComponent(virtualDOM, oldComponent, oldDOM, container) {
  if (isSameComponent(virtualDOM, oldComponent)) {
    // 同一个组件:执行组件更新操作
    updateComponent(virtualDOM, oldComponent, oldDOM, container)
  } else {
    // 不是同一个组件
    mountElement(virtualDOM, container, oldDOM)
  }
}

// 判断是否是同一个组件
function isSameComponent(virtualDOM, oldComponent) {
  return oldComponent && oldComponent.constructor === virtualDOM.type
}

import diff from "./diff"

// src/TinyReact/updateComponent.js
export default function updateComponent(virtualDOM, oldComponent, oldDOM, container) {
  // 组件更新
  // 1. 更新组件的 props
  oldComponent.updateProps(virtualDOM.props)

  // 2. 获取组件返回的最新的 VirtualDOM
  let nextVirtualDOM = oldComponent.render()
  // 重新存储组件实例
  nextVirtualDOM.component = nextVirtualDOM

  // 3. 进行比对
  diff(nextVirtualDOM, container, oldDOM)
}

调用组件的生命周期函数

在父类 Component 中定义生命周期函数,这样子类都可以继承。

如果子类要使用生命周期函数,重新定义覆盖即可。

// src/TinyReact/Component.js
import diff from './diff'
export default class Component {
  constructor(props) {
    this.props = props
  }
  setState(state) {
    this.state = Object.assign({}, this.state, state)
    // 获取最新的要渲染的 VirtualDOM 对象
    const virtualDOM = this.render()
    // 获取旧的 VirtualDOM 对象进行比对
    const oldDOM = this.getDOM()

    const container = oldDOM.parentNode
    diff(virtualDOM, oldDOM.parentNode, oldDOM)
  }
  setDOM(dom) {
    this._dom = dom
  }
  getDOM() {
    return this._dom
  }
  updateProps(props) {
    this.props = props
  }

  // 生命周期函数
  componentWillMount() {}
  componentDidMount() {}
  componentWillReceiveProps(nextProps) {}
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps != this.props || nextState != this.state
  }
  componentWillUpdate(nextProps, nextState) {}
  componentDidUpdate(prevProps, prevState) {}
  componentWillUnmount() {}
}

componentWillMountcomponentWillReceivePropscomponentWillUpdate 这些方法即将过时,官方不建议在代码中使用。

调用顺序:

  • 从上到下(父组件优先调用):在 updateComponent 中执行 diff() 前调用
    • componentWillMount
    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
  • 从下到上(子组件优先调用):在 updateComponent 中执行 diff() 后调用
    • componentDidMount
    • componentDidUpdate
import diff from "./diff"

// src/TinyReact/updateComponent.js
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)

    // 组件更新
    // 1. 更新组件的 props
    oldComponent.updateProps(virtualDOM.props)

    // 2. 获取组件返回的最新的 VirtualDOM
    let nextVirtualDOM = oldComponent.render()
    // 重新存储组件实例
    nextVirtualDOM.component = nextVirtualDOM

    // 3. 进行比对
    diff(nextVirtualDOM, container, oldDOM)

    // 调用生命周期函数
    oldComponent.componentDidUpdate(prevProps)
  }
}

当前一些生命周期方法中仅传递了 props,未传入所需的 nextStateprevState

也没有调用挂载相关的生命周期函数 componentWillMountcomponentDidMount

ref 属性获取元素的 DOM 对象和组件实例对象

在 React 中可以为 React 元素添加 ref 属性,值是一个函数:

  • 如果是普通元素,通过 ref 属性获取到元素的 DOM 对象。
    • 函数接收的参数是当前元素对应的 DOM 对象
  • 如果是类组件,通过 ref 属性获取到组件的实例对象。
    • 函数接收的参数是当前组件的实例对象
  • 函数组件不能使用 ref,因为它没有实例。
class DemoRef extends TinyReact.Component {
  constructor() {
    super()
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
    console.log(this.input.value)
  }
  render() {
    return (
      <div>
        <input type="text" ref={input => (this.input=input)} />
        <button onClick={this.handleClick}>按钮</button>
      </div>
    )
  }
}

TinyReact.render(<DemoRef />, root)

实现思路:

  1. 如果是普通 DOM 元素
    1. 在创建节点时(createDOMElement)判断其 Virtual DOM 对象中是否有ref属性
    2. 如果有,就调用 ref 属性中所存储的方法,并且将创建出来的 DOM 对象作为参数传递给 ref 方法
  2. 如果是类组件
    1. mountComponent方法中,判断当前处理的如果是 class 组件
      1. 则通过类组件返回的 VirtualDOM 对象中获取组件实例对象
      2. 判断组件实例对象中的 props 属性中是否存在 ref 属性
      3. 如果存在就调用 ref方法,并将组件实例对象传递给 ref方法
// src/TinyReact/createDOMElement.js
import mountElement from './mountElement'
import updateNodeElement from './updateNodeElement'
export default function createDOMElement(virtualDOM) {
  let newElement = null

  if (virtualDOM.type === 'text') {
    // 文本节点
    newElement = document.createTextNode(virtualDOM.props.textContent)
  } else {
    // 元素节点
    newElement = document.createElement(virtualDOM.type)

    updateNodeElement(newElement, virtualDOM)
  }

  // 将元素对应的 virtual DOM 存储到元素的属性上
  newElement._virtualDOM = virtualDOM

  // 递归创建子节点
  virtualDOM.children.forEach(child => {
    mountElement(child, newElement)
  })

  if (virtualDOM.props && virtualDOM.props.ref) {
    virtualDOM.props.ref(newElement)
  }

  return newElement
}

// src/TinyReact/mountComponent.js
import isFunction from './isFunction'
import isFunctionComponent from './isFunctionComponent'
import mountNativeElement from './mountNativeElement'
export default function mountComponent(virtualDOM, container, oldDOM) {
  let nextVirtualDOM = null
  let component = null
  // 判断组件是类组件还是函数组件
  if (isFunctionComponent(virtualDOM)) {
    // 函数组件
    nextVirtualDOM = buildFunctionComponent(virtualDOM)
  } else {
    // 类组件
    nextVirtualDOM = buildClassComponent(virtualDOM)
    component = nextVirtualDOM.component
  }

  // 判断渲染的组件是否直接返回了另一个组件
  if (isFunction(nextVirtualDOM)) {
    mountComponent(nextVirtualDOM, container, oldDOM)
  } else {
    mountNativeElement(nextVirtualDOM, container, oldDOM)
  }

  if (component) {
    component.componentDidMount()
    if (component.props && component.props.ref) {
      component.props.ref(component)
    }
  }
}

function buildFunctionComponent(virtualDOM) {
  return virtualDOM.type(virtualDOM.props || {})
}

function buildClassComponent(virtualDOM) {
  const component = new virtualDOM.type(virtualDOM.props || {})
  const nextVirtualDOM = component.render()
  // 存储组件实例对象
  nextVirtualDOM.component = component
  return nextVirtualDOM
}

使用 key 属性进行节点对比

key 属性

在 React 中,渲染列表数据时会要求在列表元素上添加 key 属性,否则会发出警告。

  • key 属性就是数据的唯一标识,用于 React 识别哪些数据被修改或者删除了,从而达到 DOM 最小化操作的目的。
  • key 属性不需要全局唯一,但是在同一个父节点下的同类型节点之间必须唯一。
  • 也就是说仅在对比同一个父节点下类型相同的子节点时需要用到 key 属性。

key 属性的作用是减少DOM操作,提高 DOM 操作的性能。

例如之前删除节点的示例是按顺序依次对比更新每个节点,然后删除最后一个 li

在这里插入图片描述

如果使用 key 属性,经过对比,只需删除文本为 2li即可,而不需要更新其他 li 的文本:

在这里插入图片描述

节点对比

实现思路

  • 两个元素进行对比时,如果类型相同,并且为元素节点(文本节点不用设置 key),就循环旧的 DOM 对象的子元素,查看其身上是否有 key
  • 如果都没有,则使用索引的方式对比每个节点
  • 如果有,就将这个子元素的 DOM 对象存储在一个 JavaScript 对象中
  • 接着循环要渲染的 Virtual DOM 对象的子元素
  • 在循环的过程中获取这个子元素的 key 属性
  • 然后使用这个 key 属性去之前的 JavaScript 对象中查找 DOM 对象
  • 如果能够找到,就说明这个元素已经存在,不需要重新渲染
    • 通过与旧 DOM 对象下相同索引的子元素是否相同,判断位置是否发生了变化
    • 如果位置变化,则将当前元素移动到旧 DOM 对象下当前索引的位置(通过 insertBefore移动到被对比的旧的子元素前面)
    • 如果位置没有发生变化,则不需要渲染
  • 如果找不到这个元素,说明这个元素是新增的,需要渲染,通过调用 mountElement 直接渲染到页面中。

示例

class KeyDemo extends TinyReact.Component {
  constructor() {
    super()
    this.state = {
      persons: [
        { id: 1, name: '张三' },
        { id: 2, name: '李四' },
        { id: 3, name: '王五' },
        { id: 4, name: '赵六' }
      ]
    }
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
    const newState = JSON.parse(JSON.stringify(this.state))
    // 位置变化
    newState.persons.push(newState.persons.shift())
    // 添加元素
    // newState.persons.splice(1, 0, { id: 0, name: '李逵' })
    // 删除元素
    // newState.persons.pop()
    this.setState(newState)
  }
  render() {
    return (
      <div>
        <ul>
          {this.state.persons.map(person => (
            <li key={person.id}>{person.name}</li>
          ))}
        </ul>
        <button onClick={this.handleClick}>按钮</button>
      </div>
    )
  }
}

TinyReact.render(<KeyDemo />, root)

位置变化

// src/TinyReact/diff.js
import createDOMElement from './createDOMElement'
import mountElement from './mountElement'
import updateNodeElement from './updateNodeElement'
import updateTextNode from './updateTextNode'
import isFunction from './isFunction'
import unmountNode from './unmountNode'
import diffComponent from './diffComponent'
export default function diff(virtualDOM, container, oldDOM) {
  const oldVirtualDOM = oldDOM && oldDOM._virtualDOM
  const oldComponent = oldVirtualDOM && oldVirtualDOM.component
  // 判断 oldDOM 是否存在
  if (!oldDOM) {
    mountElement(virtualDOM, container)
  } else if (
    // 对比的两个节点类型不相同
    virtualDOM.type !== oldVirtualDOM.type &&
    // 并且节点的类型不是组件,因为组件要单独处理
    !isFunction(virtualDOM)
  ) {
    // 节点类型不相同
    const newElement = createDOMElement(virtualDOM)
    oldDOM.parentNode.replaceChild(newElement, oldDOM)
  } else if (isFunction(virtualDOM)) {
    // 组件
    diffComponent(virtualDOM, oldComponent, oldDOM, container)
  } else if (virtualDOM.type === oldVirtualDOM.type) {
    // 节点类型相同
    if (virtualDOM.type === 'text') {
      // 文本节点:更新内容
      updateTextNode(virtualDOM, oldVirtualDOM, oldDOM)
    } else {
      // 元素节点:更新元素属性
      updateNodeElement(oldDOM, virtualDOM, oldVirtualDOM)
    }

    // 1. 将拥有 key 属性的子元素放置在一个单独的对象中
    const keyedElements = {}
    for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) {
      const domElement = oldDOM.childNodes[i]
      if (domElement.nodeType === 1) {
        // 元素节点
        const key = domElement._virtualDOM.props.key
        if (key) {
          keyedElements[key] = domElement
        }
      }
    }

    const hasNokey = Object.keys(keyedElements).length === 0

    if (hasNokey) {
      // 对比子节点
      virtualDOM.children.forEach((child, i) => {
        diff(child, oldDOM, oldDOM.childNodes[i])
      })
    } else {
      // 2. 循环 virtualDOM 的子元素,获取子元素的 key 属性
      virtualDOM.children.forEach((child, i) => {
        const key = child.props.key
        if (key !== undefined) {
          const domElement = keyedElements[key]
          if (domElement) {
            // 3. 看看当前位置的元素是不是期望的元素
            if (oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) {
              oldDOM.insertBefore(domElement, oldDOM.childNodes[i])
            }
          }
        }
      })
    }

    // 删除节点
    // 获取旧节点
    const oldChildNodes = oldDOM.childNodes
    // 判断旧节点的数量
    if (oldChildNodes.length > virtualDOM.children.length) {
      // 有节点需要被删除
      for (let i = oldChildNodes.length - 1; i > virtualDOM.children.length; i--) {
        unmountNode(oldChildNodes[i])
      }
    }
  }
}

这里通过 domElement._virtualDOM.props.key 获取 key,而不是 domElement.getAttribute('key') 获取,是因为 React 并没有将 key 属性添加到真实的 DOM 元素上,这里与 React 保持一致。

可以通过 chrome 浏览器查看 Elements 元素:

  • 当未设置 key 属性时,点击按钮,4个 li 都闪烁(表示重新渲染)
  • 当设置 key 属性时,点击按钮,只有3个li由于位置发生变化,发生了闪烁(重新渲染)

新增节点

示例修改:

  handleClick() {
    const newState = JSON.parse(JSON.stringify(this.state))
    // 位置变化
    // newState.persons.push(newState.persons.shift())
    // 添加元素
    newState.persons.splice(1, 0, { id: 0, name: '李逵' })
    // 删除元素
    // newState.persons.pop()
    this.setState(newState)
  }

mountElement 最终通过 mountNativeElement 向页面挂载元素。

当前使用的是 container.appendChild(newElement),所以新增的节点总会插入到容器的尾部。

所以要修改这个挂载方式,使其可以指定插入节点的位置(旧节点的前面)。

insertBefore(newnode, existingnode)

  • newnode 要插入的节点对象
  • existingnode 可选,在其之前插入新节点,如果未指定则会在结尾插入 newnode
    • 如果与 newnode 相同,则会执行移动操作
// src/TinyReact/diff.js
/*...*/
export default function diff(virtualDOM, container, oldDOM) {
  const oldVirtualDOM = oldDOM && oldDOM._virtualDOM
  const oldComponent = oldVirtualDOM && oldVirtualDOM.component
  // 判断 oldDOM 是否存在
  if (!oldDOM) {/*...*/
  } else if (
    // 对比的两个节点类型不相同
    virtualDOM.type !== oldVirtualDOM.type &&
    // 并且节点的类型不是组件,因为组件要单独处理
    !isFunction(virtualDOM)
  ) {/*...*/
  } else if (isFunction(virtualDOM)) {/*...*/
  } else if (virtualDOM.type === oldVirtualDOM.type) {
    // 节点类型相同
    /*...*/

    if (hasNokey) {
      // 对比子节点
      virtualDOM.children.forEach((child, i) => {
        diff(child, oldDOM, oldDOM.childNodes[i])
      })
    } else {
      // 2. 循环 virtualDOM 的子元素,获取子元素的 key 属性
      virtualDOM.children.forEach((child, i) => {
        const key = child.props.key
        if (key !== undefined) {
          const domElement = keyedElements[key]
          if (domElement) {
            // 3. 看看当前位置的元素是不是期望的元素
            if (oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) {
              oldDOM.insertBefore(domElement, oldDOM.childNodes[i])
            }
          } else {
            // 新增元素
            mountElement(child, oldDOM, oldDOM.childNodes[i])
          }
        }
      })
    }

    // 删除节点
    // 获取旧节点
    const oldChildNodes = oldDOM.childNodes
    // 判断旧节点的数量
    if (oldChildNodes.length > virtualDOM.children.length) {
      // 有节点需要被删除
      for (let i = oldChildNodes.length - 1; i > virtualDOM.children.length; i--) {
        unmountNode(oldChildNodes[i])
      }
    }
  }
}

// src/TinyReact/mountElement.js
import createDOMElement from './createDOMElement'
import unmountNode from './unmountNode'
export default function mountNativeElement(virtualDOM, container, oldDOM) {
  const newElement = createDOMElement(virtualDOM)

  // 将转换之后的 DOM 对象放置到页面中
  if (oldDOM) {
    container.insertBefore(newElement, oldDOM)
  } else {
    container.appendChild(newElement)
  }

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

  // 获取类组件的实例对象
  const component = virtualDOM.component

  // 判断是否是类组件返回的 VirtualDOM
  if (component) {
    component.setDOM(newElement)
  }
}

卸载节点

在对比节点的过程中,如果旧节点的数量多于要渲染的新节点的数量,就说明有节点被删除了。

同样先判断 keyedElements 对象中是否有元素。

  • 如果没有,就使用索引方式删除
  • 如果有,就使用 key属性对比的方式进行删除

实现思路

  • 循环旧节点,获取旧节点对应的 key 属性
  • 然后根据 key 属性在新节点中查找这个旧节点
    • 如果找到,就说明这个节点没有被删除
    • 如果没找到,说明节点被删除了,调用卸载节点的方法即可

示例修改

  handleClick() {
    const newState = JSON.parse(JSON.stringify(this.state))
    // 位置变化
    // newState.persons.push(newState.persons.shift())
    // 添加元素
    // newState.persons.splice(1, 0, { id: 0, name: '李逵' })
    // 删除元素
    newState.persons.pop()
    this.setState(newState)
  }

diff

// src/TinyReact/diff.js
/*...*/
export default function diff(virtualDOM, container, oldDOM) {
  const oldVirtualDOM = oldDOM && oldDOM._virtualDOM
  const oldComponent = oldVirtualDOM && oldVirtualDOM.component
  // 判断 oldDOM 是否存在
  if (!oldDOM) {/*...*/
  } else if (
    // 对比的两个节点类型不相同
    virtualDOM.type !== oldVirtualDOM.type &&
    // 并且节点的类型不是组件,因为组件要单独处理
    !isFunction(virtualDOM)
  ) {/*...*/
  } else if (isFunction(virtualDOM)) {/*...*/
  } else if (virtualDOM.type === oldVirtualDOM.type) {
    // 节点类型相同
    /*...*/

    // 删除节点
    // 获取旧节点
    const oldChildNodes = oldDOM.childNodes
    // 判断旧节点的数量
    if (oldChildNodes.length > virtualDOM.children.length) {
      // 有节点需要被删除
      if (hasNokey) {
        for (let i = oldChildNodes.length - 1; i > virtualDOM.children.length; i--) {
          unmountNode(oldChildNodes[i])
        }
      } else {
        // 通过 key 属性删除节点
        for (let i = 0; i < oldChildNodes.length; i++) {
          const oldChild = oldChildNodes[i]
          const oldChildKey = oldChild._virtualDOM.props.key
          const found = virtualDOM.children.some(newChild => {
            return oldChildKey === newChild.props.key
          })
          if (!found) {
            unmountNode(oldChild)
          }
        }
      }
    }
  }
}

卸载节点需要考虑的几种情况

卸载节点并不是直接将节点删除就可以了,还要考虑以下几种情况:

  1. 如果要删除的节点是文本节点,可以直接删除
  2. 如果要删除的节点由组件生成,需要调用组件卸载生命周期函数 componentWillUnmount
  3. 如果要删除的节点中包含了其他组件生成的节点,需要调用其他组件的卸载生命周期函数
  4. 如果要删除的节点身上有 ref 属性,需要删除通过 ref 属性传递给组件的 DOM 节点对象
  5. 如果要删除的节点身上有事件,需要删除事件对应的事件处理函数

示例:

class DemoRef extends TinyReact.Component {
  constructor() {
    super()
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
    console.log(this.input.value)
    console.log(this.alert)
  }
  componentWillUnmount() {
    console.log('componentWillUnmount')
  }
  render() {
    return (
      <div>
        <input type="text" ref={input => (this.input = input)} />
        <button onClick={this.handleClick}>按钮</button>
        <Alert ref={alert => (this.alert = alert)} name="张三" age={20} />
      </div>
    )
  }
}

// TinyReact.render(<DemoRef />, root)

class KeyDemo extends TinyReact.Component {
  constructor() {
    super()
    this.state = {
      persons: [
        { id: 1, name: '张三' },
        { id: 2, name: '李四' },
        { id: 3, name: '王五' },
        { id: 4, name: '赵六' }
      ]
    }
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick() {
    const newState = JSON.parse(JSON.stringify(this.state))
    // 位置变化
    // newState.persons.push(newState.persons.shift())
    // 添加元素
    // newState.persons.splice(1, 0, { id: 0, name: '李逵' })
    // 删除元素
    newState.persons.pop()
    this.setState(newState)
  }
  render() {
    return (
      <div>
        <ul>
          {this.state.persons.map(person => (
            <li key={person.id}>
              {person.name}
              <DemoRef />
            </li>
          ))}
        </ul>
        <button onClick={this.handleClick}>按钮</button>
      </div>
    )
  }
}

TinyReact.render(<KeyDemo />, root)
// src/TinyReact/unmountNode.js
export default function unmountNode(node) {
  const virtualDOM = node._virtualDOM
  // 1. 文本节点可以直接删除
  if (virtualDOM.type === 'text') {
    // 直接删除
    node.remove()
    // 阻止程序向下执行
    return
  }

  // 2. 节点是否是由组件生成
  const component = virtualDOM.component
  // 如果 component 存在,就说明节点是由组件生成的
  if (component) {
    component.componentWillUnmount()
  }

  // 3. 节点身上是否有 ref 属性
  if (virtualDOM.props && virtualDOM.props.ref) {
    virtualDOM.props.ref(null)
  }

  // 4. 节点的属性中是否有事件属性
  Object.keys(virtualDOM.props).forEach(propName => {
    if (propName.startsWith('on')) {
      const eventName = propName.toLowerCase().slice(0, 2)
      const eventHandler = virtualDOM.props[propName]
      node.removeEventListener(eventName, eventHandler)
    }
  })

  // 5. 递归删除子节点
  if (node.childNodes.length > 0) {
    for (let i = 0; i < node.childNodes.length; i++) {
      unmountNode(node.childNodes[i])
      i--
    }
  }

  // 删除节点
  node.remove()
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值