本文仅作为作者自己的技术沉淀,非面向读者,因此注释并非之前文章一般详细。不过本文对于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中的依赖一览
由于我的代码没有实现平台托管,如果你想获取我的代码,你可以如下操作:
-
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
}