React15完整功能实现(mount&&update&&patch)

528 阅读10分钟

本文仅作为作者自己的技术沉淀,非面向读者,因此注释并非之前文章一般详细。不过本文对于React15版本功能实现足够完整,包括挂载阶段,更新阶段(包括diff)以及向真实dom打补丁阶段。本文参阅视频文末会有链接,有意者可参阅。

1,React15实现原理

1.1,挂载阶段

1,将挂载虚拟dom元素根据其类型(文本类型节点,原生标签类型节点,组件类型节点)生成对应的组件类实例(文本组件类实例,通用组件类实例,合成组件类实例)

  • 这些组件类实例拥有很多属性方法,包括当前节点(虚拟dom)的挂载方法,更新方法,当前节点属性等等

2,调用组件类实例的 获取标记方法(getMarkup)生成对应节点标记(标记 markup:html字符串)返回

  • 对于文件组件类实例:则直接创建对应文本节点的标记返回

  • 对于通用(虚拟dom类型为原生标签)组件类实例:根据节点(节点:虚拟dom)类型创建对应DOM类型节点标记,同时遍历节点的props,将props属性添加至节点标记中,最后返回当前节点标记,下面是对props的处理过程

    • 对于事件(in props):采用事件委托方式添加到document上

    • 对于样式(in props):根据样式key-value创建对应样式标记添加到节点标记中

    • 对于className(in props):创建class标记添加到节点标记中

    • 对于children(in props):遍历所有子节点,递归创建并获取所有子节点标记,添加到当前节点标记中

    • 对于其他属性(in props):创建key=value形式标记添加至当前节点标记中

  • 对于合成(虚拟dom类型为函数组件||类组件)组件类实例:

    • 创建类组件实例,调用实例render方法(传入props),其返回值即最终需要渲染的虚拟dom元素(render元素)

      • 如果是函数组件,则直接传入props,执行函数组件,其返回值即最终需要渲染的虚拟dom
    • 根据render元素对应类型创建对应render元素的组件类实例(render元素组件类实例)

    • 如果当前类组件实例存在componentWillMount方法,则执行

    • 调用render元素组件类实例的获取标记方法获取render元素标记

    • 如果组件类实例存在componentDidMount方法,则添加 执行该方法 的订阅,该订阅将在所有组件挂载到真实dom后触发,即组件挂载完毕,执行所有组件的componentDidMount方法(如果存在)

    • 返回render元素标记

3,获取根节点(根节点:根虚拟dom)的标记(根节点中子节点标记已经递归获取添加到根节点中),添加到真实dom中,并发布所有关于执行componentDidMount方法的订阅。

React.render(<App/>, document.getElementById('root'));
// <App/>:根节点
// document.getElementById('root'):根节点标记所添加至的真实dom

1.2,更新阶段

如果新老根节点类型不同,则直接创建新节点替换老节点,如果新老根节点类型相同,则继续比较新老根节点(以根节点为类组件调用setState触发更新说明)

1,更新老节点(节点:虚拟dom)中state,props,并将新state,props传入当前组件实例(在挂载阶段创建)的shouldComponentUpdate方法判断是否继续更新,true则继续更新,否则不更新

2,更新则调用类组件实例render方法,获取当前类组件新render元素(新render元素:本次render返回的虚拟dom)

3,对比类组件旧render元素(旧render元素:上一次render返回的虚拟dom),判断新旧render元素类型是否相同,不同则直接创建新render元素对应dom节点替换原节点

4,相同则将新render元素交给旧render元素的组件类实例的更新方法,对旧节点进行更新

5,更新旧节点(旧节点:旧render元素)所对应的真实dom的props

  • 遍历旧节点的props:

    • 对于存在于旧节点,但不存在于新节点中的旧节点上的props属性直接从旧节点对应的真实dom中删除

    • 取消旧节点对应的真实dom中所有事件委托

  • 遍历新节点的props:

    • 对于事件(in 新节点的props):采用事件委托方式添加到document上

    • 对于样式(in 新节点的props):对比新旧节点样式,更新旧节点对应真实dom中的样式

    • 对于className(in 新节点的props):将新节点的className属性添加或覆盖到旧节点对应的真实dom上的class属性(如果真实dom已存在该属性,那么就会直接覆盖原属性)

    • 对于其他属性(in 新节点的props):直接添加或覆盖到旧节点对应的真实dom中

    • 对于children(in 新节点的props):单独进行更新

6,对比新老节点的children

  • 6.1,遍历新节点的children,根据新child的key(如果存在)或者新child的位置(即child位于children中的索引)去老节点中找对应key或者对应位置的老child,通过下面操作去收集一个 更新后的children组件类实例集合

    • 遍历之前创建一个 更新后children组件类实例集合,初始为空,用来保存新老child更新后的child组件类实例或者创建的新child组件类实例

    • 如果存在对应老child,则将新child交给老child的组件类实例的更新方法 去完成老child对应的真实dom的更新,并将完成更新的老child组件类实例添加到更新后children组件类实例集合中

    • 如果不存在对应老child,则直接创建新child的组件类实例,并将创建的新child的组件类实例添加到更新后children组件类实例集合中

    • 遍历完成后, 更新后children组件类实例集合 将 保存着 与新children元素对应的每一个最新(最新:可能是通过对旧child组件类实例的更新,也可能是直接创建的新child组件类实例)的child组件类实例

  • 此时对于部分老child所对应的真实dom更新已经完成,注意这部分老child的更新仅限于与新child存在相同key或者位置的老child。剩下的那一部分更新包括 移动,插入,删除:

    • 移动位置不对但已经更新后的老child对应的真实dom:比如旧child的位置索引为100,而新child的位置索引为1,且新旧child的key相同,那么此时会对旧child对应的真实dom即索引为100(同级下的位置索引)的真实dom进行更新,更新完毕之后,索引100的真实dom节点还需要移动到索引为1的位置上

    • 插入新child对应的真实dom:即对于新child,在老children中无相同key或相同位置的老child ,此时需要创建新child节点插入到真实dom中

    • 删除老child对应的真实dom:即对于老child,在新children中无相同key或者相同位置新child,此时需要删除老child对应的真实dom节点

  • 6.2,通过_mountIndex&&lastIndex算法收集节点的 移动 插入 删除操作

    • 收集之前声明几个概念:

      • lastIndex:最后一个确定位置的节点,初始值为0

      • _mountIndex:每个老child位于老children中的位置索引(或者说是每一个老的child对应的dom节点在真实dom中的位置索引(同级比较))

      • nextIndex:每个新child位于新children中的位置索引

    • 6.2.1,遍历新children找出需要移动,删除与插入的节点操作收集起来

      • 如果在老child中存在与当前 所遍历到的新child 相同key或者相同位置的老child,那么判断新老child是否相同(判断新老child类型,如果相同说明新child复用了老child,且老child对应的真实dom节点已经完成更新,但是位置可能不对)

        • 如果不同:

          • 如果存在老child,则老child对应的真实dom节点需要删除:删除真实dom节点位置索引即老child的_mountIndex,且更新lastIndex为老child的_mountIndex

          • 不管是否存在老child,都需要插入新child对应的真实dom:插入真实dom中的位置索引即新child在新children的位置索引(即新节点的nextIndex)

        • 如果相同,则判断老child的_mountIndex与lastIndex:

          • 如果_mountIndex<lastIndex,那么老child所对应的真实dom节点需要移动,从 老child._mountIndex位置 移动到 新child.nextIndex位置。

          • 如果_mountIndex>=lastIndex,则只更新lastIndex:lastIndex = Max(老child._mountIndex,lastIndex)

    • 6.2.2,遍历老children找出需要删除的节点操作收集起来:

      • 如果当前(遍历到)的老child在新children中不存在相同key或者相同位置的新child,那么该老child对应的真实dom节点需要删除,删除位置即老child._mountIndex

1.3,patch阶段

更新阶段的diff操作已经收集了新旧dom树的差异,即移动节点操作,插入节点操作,删除节点操作,此时我们要根据这些差异更新真实dom树,假设这些差异操作存放在patchQueue中:

  • 1,将patchQueue中所有需要移动与删除的dom节点根据其fromIndex从真实dom中删除:

    • 1.1,差异操作记录了移动操作所要移动的真实dom节点的fromIndex与toIndex,即从当前层级的真实dom节点中的fromIndex位置移动到toIndex的位置

    • 1.2,差异操作记录了删除操作所要删除的真实dom节点的fromIndex,即从当前层级的真实dom节点中的fromIndex位置删除

    • 其实除了fromIndex,lastIndex位置,每一个差异操作还记录了其他属性,比如需要操作的真实dom节点,及其父节点,操作名称(删除,移动还是插入)等属性,方便我们去更新真实dom

  • 2,将patchQueue中所有需要移动的真实dom节点与插入的真实dom节点插入到当前层级的真实dom节点中的toIndex位置

  • 3,完成真实dom树更新

2,代码

使用create-react-app创建一个基础react应用,除此之外还安装了jquery(方便实现一些dom插入,事件委托等功能)

2.1,文件目录及package.json中的依赖一览

image.png

由于我的代码没有实现平台托管,如果你想获取我的代码,你可以如下操作:

  • 1,使用create-react-app创建一个react应用,并安装jquery,跑起来你的react

  • 2,删除public文件夹下除index.html之外所有文件(且index.html只保留id为root的div标签,其他引入或无效引入等直接删除)

  • 3,删除src文件夹下除index.js之外所有其他文件

  • 4,按照上图在src中创建对应js文件(文件内容直接拷贝1.2-1.7)

2.2,index.js

import React from './react'

class Todos extends React.Component {
  constructor(props) {
    super(props)
    this.state = { lists: [], text: '' }
  }
  onChange = event => {
    this.setState({ text: event.target.value })
  }
  handleClick = () => {
    let text = this.state.text
    this.setState({
      lists: [...this.state.lists, text], text: ''
    })
  }
  onDel = (index) => {
    this.setState({
      lists: [...this.state.lists.slice(0, index), ...this.state.lists.slice(index + 1)]
    })
  }
  render() {
    let lists = this.state.lists.map((item, index) => {
      return React.createElement('li', {}, item, React.createElement('button', {
        onClick: () => {
          this.onDel(index)
        }
      }, 'X'))
    })
    let input = React.createElement('input', { onKeyup: this.onChange, value: this.state.text })
    let button = React.createElement('button', { onClick: this.handleClick }, 'add')
    return React.createElement('div', {}, input, button,
      // React.createElement('ul', {}, ...lists)
      ...lists
    )
  }
}

let element = React.createElement(Todos, { name: '计数器' })
React.render(element, document.getElementById('root'));

2.3,react.js

import $ from 'jquery'
import { createUnit } from './unit'
import { createElement } from './element'
import { Component } from './component'
let React = {
    render,
    createElement,
    Component
}
function render(element, container) {
    let unit = createUnit(element)
    let markUp = unit.getMarkUp('0');
    $(container).html(markUp)
    $(document).trigger('mounted')
}

export default React

2.4,element.js

class Element {
    constructor(type, props) {
        this.type = type
        this.props = props
    }
}
function createElement(type, props, ...children) {
    props.children = children
    return new Element(type, props)
}

export {
    Element,
    createElement
}

2.5,component.js

class Component {
    constructor(props) {
        this.props = props
    }
    setState(partialState) {
        this._currentUnit.update(null, partialState)
    }
}
export {
    Component
}

2.6,type.js

export default {
    MOVE: 'MOVE',       // 移动
    INSERT: 'INSERT',   // 插入
    REMOVE: 'REMOVE'    // 删除
}

2.7,unit.js

import { Element, createElement } from './element'
import $ from 'jquery'
import types from './types'

let diffQueue = []// 差异队列
let updateDepth = 0 // 更新的层级(dom层级)
class Unit {
    constructor(element) {
        this._currentElement = element
    }
    getMarkUp() {
        throw new Error('此方法不可被调用')
    }
}

class TextUnit extends Unit {
    getMarkUp(reactid) {
        this._reactid = reactid
        return `<span data-reactid=${reactid}>${this._currentElement}</span>`
    }
    update(nextElement) {
        if (this._currentElement !== nextElement) {
            this._currentElement = nextElement
            $(`[data-reactid="${this._reactid}"]`).html(this._currentElement)
        }
    }
}

class NativeUnit extends Unit {
    getMarkUp(reactid) {
        this._reactid = reactid
        const { type, props } = this._currentElement
        let tagStart = `<${type} data-reactid="${this._reactid}"`
        let childString = ''
        let tagEnd = `</${type}>`
        this._renderedChildrenUnits = []

        for (let propName in props) {
            if (/^on[A-Z]/.test(propName)) {
                let eventName = propName.slice(2).toLowerCase()
                $(document).delegate(`[data-reactid="${this._reactid}"]`, `${eventName}.${this._reactid}`, props[propName])
            }
            else if (propName === 'style') {
                let styleObj = props[propName]
                let styles = Object.entries(styleObj).map(([attr, value]) => {
                    return `${attr.replace(/[A-Z]/g, m => `-${m.toLowerCase()}`)} : ${value}`
                }).join(';')
                tagStart += `style="${styles}"`
            }
            else if (propName === 'className') {
                tagStart += `${propName}="${props[propName]}"`
            }
            else if (propName === 'children') {
                const children = props[propName]
                children.forEach((child, index) => {
                    const childUnit = createUnit(child)
                    childUnit._mountIndex = index // 每一个childUnit都有个_mountIndex,指向自己在父节点中的位置索引
                    this._renderedChildrenUnits.push(childUnit)
                    const childMarkUp = childUnit.getMarkUp(`${this._reactid}.${index}`)
                    childString += childMarkUp
                })
            } else {
                tagStart += ` ${propName}=${props[propName]} `
            }
        }
        return tagStart + '>' + childString + tagEnd
    }
    update(nextElement) {
        let oldProps = this._currentElement.props
        let newProps = nextElement.props
        // 更新props
        this.updateDOMProperties(oldProps, newProps)
        // 更新children (比较新老children,找出差异,进行修改dom)
        this.updateDOMChildren(nextElement.props.children)

    }
    updateDOMChildren(newChildrenElements) {
        updateDepth++
        this.diff(diffQueue, newChildrenElements)
        updateDepth--
        // updateDepth 深度优先遍历完成 则恢复为0,此时可以打补丁
        if (updateDepth === 0) {
            this.patch(diffQueue)
            diffQueue = []
        }
    }
    patch(diffQueue) {
        let deleteChildren = []
        let deleteMap = {}
        for (let i = 0; i < diffQueue.length; i++) {
            let difference = diffQueue[i]
            if (difference.type === types.MOVE || difference.type === types.REMOVE) {
                let fromIndex = difference.fromIndex
                let oldChild = $(difference.parentNode.children().get(fromIndex))
                if (!deleteMap[difference.parentId]) {
                    deleteMap[difference.parentId] = {}
                }
                deleteMap[difference.parentId][fromIndex] = oldChild
                deleteChildren.push(oldChild)
            }
        }
        $.each(deleteChildren, (idx, item) => $(item).remove())
        for (let i = 0; i < diffQueue.length; i++) {
            let difference = diffQueue[i]
            switch (difference.type) {
                case types.INSERT:
                    this.insertChildAt(difference.parentNode, difference.toIndex, $(difference.markUp))
                    break;
                case types.MOVE:
                    this.insertChildAt(difference.parentNode, difference.toIndex, deleteMap[difference.parentId][difference.fromIndex])
                    break;
                default: break;
            }
        }
    }
    insertChildAt(parentNode, index, newNode) {
        let oldChild = parentNode.children().get(index)
        oldChild ? newNode.insertBefore(oldChild) : newNode.appendTo(parentNode)
    }
    diff(diffQueue, newChildrenElements) {
        // 生成老的key-childrenUnit映射
        let oldChildrenUnitMap = this.getOldChildrenMap(this._renderedChildrenUnits)
        // 生成一个更新后的newChildrenUnits数组(将新children元素根据key交给对应老childrenUnit完成更新,如果不存在对应老childrenUnit,则直接创建新的childrenUnit)
        let { newChildrenUnitMap, newChildrenUnits } = this.getNewChildren(oldChildrenUnitMap, newChildrenElements)
        let lastIndex = 0 // 上一个已经确定位置的索引
        // 遍历新节点(newChildUnit),根据_mountIndex,lastIndex找出插入,移动的节点
        for (let i = 0; i < newChildrenUnits.length; i++) {
            let newUnit = newChildrenUnits[i]
            let newKey = newChildrenUnits[i]?._currentElement?.props?.key ?? i.toString()
            let oldChildUnit = oldChildrenUnitMap[newKey]
            if (oldChildUnit === newUnit) { // 如果新老一致说明复用了老节点
                if (oldChildUnit._mountIndex < lastIndex) {
                    diffQueue.push({
                        parentId: this._reactid,
                        parentNode: $(`[data-reactid="${this._reactid}"]`),
                        type: types.MOVE,
                        fromIndex: oldChildUnit._mountIndex,
                        toIndex: i
                    })
                }
                lastIndex = Math.max(oldChildUnit._mountIndex, lastIndex)
            } else {
                if (oldChildUnit) {
                    diffQueue.push({
                        parentId: this._reactid,
                        parentNode: $(`[data-reactid="${this._reactid}"]`),
                        type: types.REMOVE,
                        fromIndex: oldChildUnit._mountIndex,
                    })
                    this._renderedChildrenUnits = this._renderedChildrenUnits.filter(item => item !== oldChildUnit)
                    $(document).undelegate(`.${oldChildUnit._reactid}`)
                }
                diffQueue.push({
                    parentId: this._reactid,
                    parentNode: $(`[data-reactid="${this._reactid}"]`),
                    type: types.INSERT,
                    toIndex: i,
                    markUp: newUnit.getMarkUp(`${this._reactid}.${i}`)
                })

            }
            newUnit._mountIndex = i
        }
        // 遍历老节点(oldChildUnit)找出需要删除的老节点
        for (let oldKey in oldChildrenUnitMap) {
            let oldChild = oldChildrenUnitMap[oldKey]
            if (!newChildrenUnitMap.hasOwnProperty(oldKey)) {
                diffQueue.push({
                    parentId: this._reactid,
                    parentNode: $(`[data-reactid="${this._reactid}"]`),
                    type: types.REMOVE,
                    fromIndex: oldChild._mountIndex,
                })
                this._renderedChildrenUnits = this._renderedChildrenUnits.filter(item => item !== oldChild)
                $(document).undelegate(`.${oldChild._reactid}`)
            }
        }
    }
    getNewChildren(oldChildrenUnitMap, newChildrenElements) {
        let newChildrenUnits = []
        let newChildrenUnitMap = {}
        newChildrenElements.forEach((newElement, index) => {
            let newKey = newElement?.props?.key ?? index.toString()
            let oldUnit = oldChildrenUnitMap[newKey]
            let oldElement = oldUnit?._currentElement
            if (shouldDeepCompare(oldElement, newElement)) {
                oldUnit.update(newElement)
                newChildrenUnits.push(oldUnit)
                newChildrenUnitMap[newKey] = oldUnit
            } else {
                let nextUnit = createUnit(newElement)
                newChildrenUnits.push(nextUnit)
                newChildrenUnitMap[newKey] = nextUnit
                this._renderedChildrenUnits[index] = nextUnit
            }
        })
        return { newChildrenUnitMap, newChildrenUnits }
    }
    getOldChildrenMap(childrenUnit = []) {
        let map = {}
        for (let i = 0; i < childrenUnit.length; i++) {
            let key = childrenUnit[i]?._currentElement?.props?.key || i.toString()
            map[key] = childrenUnit[i]
        }
        return map
    }
    updateDOMProperties(oldProps, newProps) {
        let propName
        // 遍历旧props
        for (propName in oldProps) {
            // 如果旧props中属性不存在新props中,直接移除dom中的旧props属性
            if (!newProps.hasOwnProperty(propName)) {
                $(`[data-reactid="${this._reactid}"]`).removeAttr(propName)
            }
            // 取消旧props属性中的事件委托
            if (/^on[A-Z]/.test(propName)) {
                $(document).undelegate(`.${this._reactid}`)
            }
        }
        // 遍历新props
        for (propName in newProps) {
            // children暂不处理
            if (propName === 'children') {
                continue
            }
            // 向真实dom中添加新props事件
            else if (/^on[A-Z]/.test(propName)) {
                let eventName = propName.slice(2).toLowerCase()
                $(document).delegate(`[data-reactid="${this._reactid}"]`, `${eventName}.${this._reactid}`, newProps[propName])
            }
            // 向真实dom中添加class属性
            else if (propName === 'className') {
                $(`[data-reactid="${this._reactid}"]`).attr('class', newProps[propName])
            }
            // 向真实dom中添加样式属性
            else if (propName === 'style') {
                let styleObj = newProps[propName]
                Object.entries(styleObj).map(([attr, value]) => {
                    $(`[data-reactid="${this._reactid}"]`).css(attr, value)
                })
            }
            // 向真实dom中华添加其他属性
            else {
                $(`[data-reactid="${this._reactid}"]`).prop(propName, newProps[propName])
            }
        }
    }
}
class CompositeUnit extends Unit {
    // 这里负责处理组件更新操作
    update(nextElement, partialState) {
        // this._currentElement更新为新传入的虚拟Dom元素(如果有的话)
        this._currentElement = nextElement || this._currentElement
        // 更新当前组件state(组件实例是不变的,所以直接更新组件实例state即可)(Object.assign会修改原值,所以不必this._componentInstance.state = nextState去赋值)
        let nextState = Object.assign(this._componentInstance.state, partialState)
        // 更新当前组件props,即新传入的虚拟dom元素的props
        let nextProps = this._currentElement.props
        // 如果组件方法shouldComponentUpdate(传入nextProps,nextState)返回false 则不更新
        if (this._componentInstance.shouldComponentUpdate && !this._componentInstance.shouldComponentUpdate(nextProps, nextState)) {
            return
        }
        // 拿到先前render元素的unit
        let preRenderedUnitInstance = this._renderedUnitInstance
        // 获取先前render元素的虚拟dom
        let preRenderedElement = preRenderedUnitInstance._currentElement
        // 获取当前最新render元素的虚拟dom
        let nextRenderedElement = this._componentInstance.render()
        // 如果新旧两个元素类型相同,则继续比较,如果不同,直接干掉老的元素,新建新的元素
        if (shouldDeepCompare(preRenderedElement, nextRenderedElement)) {
            // 如果可以深比较,则将更新操作交给先前render元素unit(preRenderedUnitInstance.update)去进行处理
            preRenderedUnitInstance.update(nextRenderedElement)
            this._componentInstance.componentDidUpdate && this._componentInstance.componentDidUpdate()
        } else {
            this._renderedUnitInstance = createUnit(nextRenderedElement)
            let nextMarkUp = this._renderedUnitInstance.getMarkUp(this._reactid)
            $(`[data-reactid="${this._reactid}"]`).replaceWith(nextMarkUp)
        }
    }
    getMarkUp(reactid) {
        this._reactid = reactid
        let { type: component, props } = this._currentElement
        let componentInstance = this._componentInstance = new component(props)
        // 让组件的currentUnit等于当前unit
        componentInstance._currentUnit = this
        // 如果有组件将要渲染函数让它执行
        componentInstance.componentWillMount && componentInstance.componentWillMount()
        // 调用组件render方法获得要渲染的元素
        let renderedElement = componentInstance.render();
        // 拿到render元素的unit
        let renderedUnitInstance = this._renderedUnitInstance = createUnit(renderedElement)
        // 通过renderedUnit获取其html标记
        let renderedMarkUp = renderedUnitInstance.getMarkUp(this._reactid)
        // 如果组件实例有componentDidMount方法,则添加该方法订阅,在组件挂载到真实dom上之后触发该订阅
        $(document).on('mounted', () => componentInstance.componentDidMount && componentInstance.componentDidMount())
        return renderedMarkUp
    }
}

function createUnit(element) {
    if (typeof element === 'string' || typeof element === 'number') {
        return new TextUnit(element)
    }
    if (element instanceof Element && typeof element.type === 'string') {
        return new NativeUnit(element)
    }
    if (element instanceof Element && typeof element.type === 'function') {
        return new CompositeUnit(element)
    }
}
// 判断两个元素的类型是否一样
function shouldDeepCompare(oldElement, newElement) {
    if (oldElement !== null && newElement !== null) {
        let oldType = typeof oldElement
        let newType = typeof newElement
        if ((oldType === 'string' || oldType === 'number') && newType === 'string' || newType === 'number') {
            return true
        }
        if (oldElement instanceof Element && newElement instanceof Element) {
            return oldElement.type === newElement.type
        }
    }
    return false
}

export {
    createUnit
}

感谢参考: