共计 14240 个字符,预计需要花费 36 分钟才能阅读完成。
Virtual Dom 和 Diff 算法
React.creaeElement()
Babel
会对将 JSX 编译为 React API(React.creaeElement()
),React.creaeElement()
会返回一个 Virtual Dom,React
会将 Virtual Dom 转换为真是 Dom,显示到页面中。
jsx 转换为 Virtual Dom 构造,type
,props
,children
<div className="container">
<h3>Hello World</h3>
<p>React Demo </p>
</div>
转换后
{
type: "div",
props: {className: "container"},
children: [
{
type: "h3",
props: null,
children: [
{
type: "text",
props: {textContent: "Hello World"}
}
]
},
{
type: "p",
props: null,
children: [
{
type: "text",
props: {textContent: "React Demo"}
}
]
}
]
}
1, 创立 Virtual DOM
在 React 代码执行前,JSX 会被 Babel 转换为 React.createElement 办法的调用,在调用 createElement 办法时会传入元素的类型,元素的属性,以及元素的子元素,createElement 办法的返回值为构建好的 Virtual DOM 对象。依据返回的 virtualDom 对象,进行解决成须要的数据结构, 在这个过程中,须要解决 virtualDom 中的布尔值或者 null
{
type,
props: Object.assign({children: childElements}, props),
children: childElements
}
2, 渲染 Virtual DOM 对象为 DOM 对象
调用 render 办法能够将 Virtual DOM 对象更新为实在 DOM 对象。
在更新之前须要确定是否存在旧的 Virtual DOM,如果存在须要比对差别,如果不存在能够间接将 Virtual DOM 转换为 DOM 对象。
先只思考不存在旧的 Virtual DOM 的状况,先间接将 Virtual DOM 对象更新为实在 DOM 对象。
export default function diff(virtualDOM, container, oldDOM) {
// 判断 oldDOM 是否存在
if (!oldDOM) {
// 如果不存在 不须要比照 间接将 Virtual DOM 转换为实在 DOM
mountElement(virtualDOM, container)
}
}
mountElement
办法中须要判断是组件还是元素
一般元素间接挂载
export default function mountNativeElement(virtualDOM, container) {const newElement = createDOMElement(virtualDOM)
container.appendChild(newElement)
}
createDOMElement
办法中须要判断是一般文本节点还是元素节点, 并且判断是否有子元素,递归渲染
export default function createDOMElement(virtualDOM) {
let newElement = null
if (virtualDOM.type === "text") {
// 创立文本节点
newElement = document.createTextNode(virtualDOM.props.textContent)
} else {
// 创立元素节点
newElement = document.createElement(virtualDOM.type)
// 更新元素属性
updateElementNode(newElement, virtualDOM)
}
// 递归渲染子节点
virtualDOM.children.forEach(child => {
// 因为不确定子元素是 NativeElement 还是 Component 所以调用 mountElement 办法进行确定
mountElement(child, newElement)
})
return newElement
}
3, 为元素节点增加属性
元素渲染实现后,须要给元素增加属性, 元素属性也分为事件属性,input 标签类的 value 属性或者 checked 属性,还有 className 或者其余属性,在这个过程中,要刨除 children, 它存在于 virtualDom 数据结构中,但不是咱们须要的节点属性
export default function updateElementNode(element, virtualDOM) {
// 获取要解析的 VirtualDOM 对象中的属性对象
const newProps = virtualDOM.props
// 将属性对象中的属性名称放到一个数组中并循环数组
Object.keys(newProps).forEach(propName => {const newPropsValue = newProps[propName]
// 思考属性名称是否以 on 结尾 如果是就示意是个事件属性 onClick -> click
if (propName.slice(0, 2) === "on") {const eventName = propName.toLowerCase().slice(2)
element.addEventListener(eventName, newPropsValue)
// 如果属性名称是 value 或者 checked 须要通过 [] 的模式增加} else if (propName === "value" || propName === "checked") {element[propName] = newPropsValue
// 刨除 children 因为它是子元素 不是属性
} else if (propName !== "children") {
// className 属性独自解决 不间接在元素上增加 class 属性是因为 class 是 JavaScript 中的关键字
if (propName === "className") {element.setAttribute("class", newPropsValue)
} else {
// 一般属性
element.setAttribute(propName, newPropsValue)
}
}
})
}
4, 渲染组件
一般的 html 元素解决完之后须要解决渲染组件的状况,组件的 virtualDom 数据结构
// 组件的 Virtual DOM
{type: f function() {},
props: {}
children: []}
组件的 virtualDom 中 type 属性是function
export default function mountComponent(virtualDOM, container) {
// 寄存组件调用后返回的 Virtual DOM 的容器
let nextVirtualDOM = null
// 辨别函数型组件和类组件
if (isFunctionalComponent(virtualDOM)) {
// 函数组件 调用 buildFunctionalComponent 办法处理函数组件
nextVirtualDOM = buildFunctionalComponent(virtualDOM)
} else {// 类组件}
// 判断失去的 Virtual Dom 是否是组件
if (isFunction(nextVirtualDOM)) {
// 如果是组件 持续递归调用 mountComponent 解剖组件
mountComponent(nextVirtualDOM, container)
} else {
// 如果是 Navtive Element 就去渲染
mountNativeElement(nextVirtualDOM, container)
}
}
// Virtual DOM 是否为函数型组件
// 条件有两个: 1. Virtual DOM 的 type 属性值为函数 2. 函数的原型对象中不能有 render 办法
// 只有类组件的原型对象中有 render 办法
export function isFunctionalComponent(virtualDOM) {
const type = virtualDOM && virtualDOM.type
return (type && isFunction(virtualDOM) && !(type.prototype && type.prototype.render)
)
}
// 函数组件解决
function buildFunctionalComponent(virtualDOM) {
// 通过 Virtual DOM 中的 type 属性获取到组件函数并调用
// 调用组件函数时将 Virtual DOM 对象中的 props 属性传递给组件函数 这样在组件中就能够通过 props 属性获取数据了
// 组件返回要渲染的 Virtual DOM
return virtualDOM && virtualDOM.type(virtualDOM.props || {})
}
5,渲染类组件
类组件自身也是 Virtual DOM,能够通过 Virtual DOM 中的 type 属性值确定以后要渲染的组件是类组件还是函数组件。类组件有 render 办法
在确定以后要渲染的组件为类组件当前,须要实例化类组件失去类组件实例对象,通过类组件实例对象调用类组件中的 render 办法,获取组件要渲染的 Virtual DOM。
类组件须要继承 Component 父类,子类须要通过 super 办法将本身的 props 属性传递给 Component 父类,父类会将 props 属性挂载为父类属性,子类继承了父类,本人自身也就天然领有 props 属性了。当 props 产生更新后,父类能够依据更新后的 props 帮忙子类更新视图。
在 mountComponent
中解决类组件时
// 解决类组件
function buildStatefulComponent(virtualDOM) {
// 实例化类组件 失去类组件实例对象 并将 props 属性传递进类组件
const component = new virtualDOM.type(virtualDOM.props)
// 调用类组件中的 render 办法失去要渲染的 Virtual DOM
const nextVirtualDOM = component.render()
// 返回要渲染的 Virtual DOM
return nextVirtualDOM
}
6. Virtual DOM 比对
比对过程遵循同级比对,深度遍历优先准则
须要解决节点类型雷同的状况,如果是元素节点,就比照元素节点属性是否发生变化
在diff
办法中获取oldVirtualDom
// diff.js
// 获取未更新前的 Virtual DOM
const oldVirtualDOM = oldDOM && oldDOM._virtualDOM
oldVirtualDOM 是否存在,如果存在则持续判断要比照的 Virtual DOM 类型是否雷同,如果类型雷同判断节点类型是否是文本,如果是文本节点比照,就调用 updateTextNode 办法,变动间接替换 textContent
属性值,如果是元素节点比照就调用 setAttributeForElement 办法.
setAttributeForElement 办法用于设置 / 更新元素节点属性
思路是先别离获取更新后的和更新前的 Virtual DOM 中的 props 属性,循环新 Virtual DOM 中的 props 属性,通过比照看一下新 Virtual DOM 中的属性值是否产生了变动,如果发生变化 须要将变动的值更新到实在 DOM 对象中
再循环未更新前的 Virtual DOM 对象,通过比照看看新的 Virtual DOM 中是否有被删除的属性,如果存在删除的属性 须要将 DOM 对象中对应的属性也删除掉
export default function updateNodeElement(
newElement,
virtualDOM,
oldVirtualDOM = {}) {
// 获取节点对应的属性对象
const newProps = virtualDOM.props || {}
const oldProps = oldVirtualDOM.props || {}
Object.keys(newProps).forEach(propName => {
// 获取属性值
const newPropsValue = newProps[propName]
const oldPropsValue = oldProps[propName]
if (newPropsValue !== oldPropsValue) {
// 判断属性是否是否事件属性 onClick -> click
if (propName.slice(0, 2) === "on") {
// 事件名称
const eventName = propName.toLowerCase().slice(2)
// 为元素增加事件
newElement.addEventListener(eventName, newPropsValue)
// 删除原有的事件的事件处理函数
if (oldPropsValue) {newElement.removeEventListener(eventName, oldPropsValue)
}
} else if (propName === "value" || propName === "checked") {newElement[propName] = newPropsValue
} else if (propName !== "children") {if (propName === "className") {newElement.setAttribute("class", newPropsValue)
} else {newElement.setAttribute(propName, newPropsValue)
}
}
}
})
// 判断属性被删除的状况
Object.keys(oldProps).forEach(propName => {const newPropsValue = newProps[propName]
const oldPropsValue = oldProps[propName]
if (!newPropsValue) {
// 属性被删除了
if (propName.slice(0, 2) === "on") {const eventName = propName.toLowerCase().slice(2)
newElement.removeEventListener(eventName, oldPropsValue)
} else if (propName !== "children") {newElement.removeAttribute(propName)
}
}
})
}
比照实现当前还须要递归比照子元素
else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
// 递归比照 Virtual DOM 的子元素
virtualDOM.children.forEach((child, i) => {diff(child, oldDOM, oldDOM.childNodes[i])
})
}
当 Virtual DOM 类型不同时,就不须要持续比照了,间接应用新的 Virtual DOM 创立 DOM 对象,用新的 DOM 对象间接替换旧的 DOM 对象。以后这种状况要将组件刨除,组件要被独自解决。
比照完须要思考删除节点的状况,产生在节点更新当前并且产生在同一个父节点下的所有子节点身上。旧节点对象的数量多于新 VirtualDOM 节点的数量,就阐明有节点须要被删除。
// 获取就节点的数量
let oldChildNodes = oldDOM.childNodes
// 如果旧节点的数量多于要渲染的新节点的长度
if (oldChildNodes.length > virtualDOM.children.length) {
for (
let i = oldChildNodes.length - 1;
i > virtualDOM.children.length - 1;
i--
) {oldDOM.removeChild(oldChildNodes[i])
}
}
类组件的状态更新,须要应用 setState
办法,定义在父类 Component 中的,该办法的作用是更改子类的 state,产生一个全新的 state 对象。子类能够调用父类的 setState 办法更改状态值之后,当组件的 state 对象产生更改时,要调用 render 办法更新组件视图。
在更新组件之前,要应用更新的 Virtual DOM 对象和未更新的 Virtual DOM 进行比照找出更新的局部,达到 DOM 最小化操作的目标。
在 setState 办法中能够通过调用 this.render 办法获取更新后的 Virtual DOM,因为 setState 办法被子类调用,this 指向子类,所以此处调用的是子类的 render 办法。
页面中的 DOM 对象是通过 mountNativeElement 办法挂载到页面中的,所以咱们只须要在这个办法中调用 Component 类中的办法就能够将 DOM 对象保留在 Component 类中了。在子类调用 setState 办法的时候,在 setState 办法中再调用另一个获取 DOM 对象的办法就能够获取到之前保留的 DOM 对象了。这里存在一个问题,如何在 mountNativeElement
中通过类的实例对象调用 setDom 办法。mountNativeElement
办法接管最新的 Virtual DOM 对象,如果这个 Virtual DOM 对象是类组件产生的,在产生这个 Virtual DOM 对象时肯定会先失去这个类的实例对象,而后再调用实例对象上面的 render 办法进行获取。咱们能够在那个时候将类组件实例对象增加到 Virtual DOM 对象的属性中,而这个 Virtual DOM 对象最终会传递给 mountNativeElement
办法,这样咱们就能够在 mountNativeElement
办法中获取到组件的实例对象了,既然类组件的实例对象获取到了,咱们就能够调用 setDOM 办法了。
在 buildClassComponent
办法中为 Virtual DOM 对象增加 component 属性,值为类组件的实例对象。
// 保留 DOM 对象的办法
setDOM(dom) {this._dom = dom}
// 获取 DOM 对象的办法
getDOM() {
retur
setState(state) {
// setState 办法被子类调用 此处 this 指向子类
// 所以扭转的是子类的 state
this.state = Object.assign({}, this.state, state)
// 通过调用 render 办法获取最新的 Virtual DOM
let virtualDOM = this.render()}
function buildClassComponent(virtualDOM) {const component = new virtualDOM.type(virtualDOM.props)
const nextVirtualDOM = component.render()
nextVirtualDOM.component = component
return nextVirtualDOM
}
export default function mountNativeElement(virtualDOM, container) {
// 获取组件实例对象
const component = virtualDOM.component
// 如果组件实例对象存在
if (component) {
// 保留 DOM 对象
component.setDOM(newElement)
}
}
这里在 setState 办法中还须要调用 diff 办法,进行状态更新
setState(state) {this.state = Object.assign({}, this.state, state)
let virtualDOM = this.render()
let oldDOM = this.getDOM()
// 获取实在 DOM 对象父级容器对象
let container = oldDOM.parentNode
}
组件更新,如果更新的是组件,还须要判断是否是同一个组件,如果不是同一个组件就不须要做组件更新操作,间接调用 mountElement 办法将组件返回的 Virtual DOM 增加到页面中。如果是同一个组件,就执行更新组件操作,就是将最新的 props 传递到组件中,再调用组件的 render 办法获取组件返回的最新的 Virtual DOM 对象,再将 Virtual DOM 对象传递给 diff 办法,让 diff 办法找出差别,从而将差别更新到实在 DOM 对象中。
在更新组件的过程中还要在不同阶段调用其不同的组件生命周期函数。
在 diff 办法中判断要更新的 Virtual DOM 是否是组件,如果是组件又分为多种状况,新增 diffComponent 办法进行解决
else if (typeof virtualDOM.type === "function") {
// 要更新的是组件
// 1) 组件自身的 virtualDOM 对象 通过它能够获取到组件最新的 props
// 2) 要更新的组件的实例对象 通过它能够调用组件的生命周期函数 能够更新组件的 props 属性 能够获取到组件返回的最新的 Virtual DOM
// 3) 要更新的 DOM 象 在更新组件时 须要在已有 DOM 对象的身上进行批改 实现 DOM 最小化操作 获取旧的 Virtual DOM 对象
// 4) 如果要更新的组件和旧组件不是同一个组件 要间接将组件返回的 Virtual DOM 显示在页面中 此时须要 container 做为父级容器
diffComponent(virtualDOM, oldComponent, oldDOM, container)
}
在 diffComponent 办法中判断要更新的组件是未更新前的组件是否是同一个组件
function isSameComponent(virtualDOM, oldComponent) {return oldComponent && virtualDOM.type === oldComponent.constructor}
不是同一个组件的话,就不须要执行更新组件的操作,间接将组件内容显示在页面中,替换原有内容
// 不是同一个组件 间接将组件内容显示在页面中
// 这里为 mountElement 办法新增了一个参数 oldDOM
// 作用是在将 DOM 对象插入到页背后 将页面中已存在的 DOM 对象删除 否则无论是旧 DOM 对象还是新 DOM 对象都会显示在页面中
mountElement(virtualDOM, container, oldDOM)
在 mountNativeElement 办法中删除原有的旧 DOM 对象 unmount(oldDOM)
, 调用node.remove()
办法
如果是同一个组件的话,须要执行组件更新操作,须要调用组件生命周期函数
export default class Component {
// 生命周期函数
componentWillMount() {}
componentDidMount() {}
componentWillReceiveProps(nextProps) {}
shouldComponentUpdate(nextProps, nextState) {return nextProps != this.props || nextState != this.state}
componentWillUpdate(nextProps, nextState) {}
componentDidUpdate(prevProps, preState) {}
componentWillUnmount() {}
}
更新时的操作,updateComponent
export default function updateComponent(
virtualDOM,
oldComponent,
oldDOM,
container
) {
// 生命周期函数
oldComponent.componentWillReceiveProps(virtualDOM.props)
if (
// 调用 shouldComponentUpdate 生命周期函数判断是否要执行更新操作
oldComponent.shouldComponentUpdate(virtualDOM.props)
) {
// 拷贝 props
let prevProps = oldComponent.props
// 生命周期函数
oldComponent.componentWillUpdate(virtualDOM.props)
// 更新组件的 props 属性 updateProps 办法定义在 Component 类型
oldComponent.updateProps(virtualDOM.props)
// 因为组件的 props 曾经更新 所以调用 render 办法获取最新的 Virtual DOM
const nextVirtualDOM = oldComponent.render()
// 将组件实例对象挂载到 Virtual DOM 身上
nextVirtualDOM.component = oldComponent
// 调用 diff 办法更新视图
diff(nextVirtualDOM, container, oldDOM)
// 生命周期函数
oldComponent.componentDidUpdate(prevProps)
}
}
export default class Component {updateProps(props) {this.props = props}
}
6, 解决 ref 属性
ref 属性能够是元素的,也能够是组件的
元素节点时,在创立节点时判断其 Virtual DOM 对象中是否有 ref 属性,如果有就调用 ref 属性中所存储的办法并且将创立进去的 DOM 对象作为参数传递给 ref 办法,这样在渲染组件节点的时候就能够拿到元素对象并将元素对象存储为组件属性了。
// createDOMElement.js
if (virtualDOM.props && virtualDOM.props.ref) {virtualDOM.props.ref(newElement)
}
类组件的元素有 ref 属性时,判断以后解决的是类组件,就通过类组件返回的 Virtual DOM 对象中获取组件实例对象,判断组件实例对象中的 props 属性中是否存在 ref 属性,如果存在就调用 ref 办法并且将组件实例对象传递给 ref 办法。
let component = null
if (isFunctionalComponent(virtualDOM)) {}
else {
// 类组件
nextVirtualDOM = buildStatefulComponent(virtualDOM)
// 获取组件实例对象
component = nextVirtualDOM.component
}
// 如果组件实例对象存在的话
if (component) {
// 判断组件实例对象身上是否有 props 属性 props 属性中是否有 ref 属性
if (component.props && component.props.ref) {
// 调用 ref 办法并传递组件实例对象
component.props.ref(component)
}
}
与此同时,执行
// 如果组件实例对象存在的话
if (component) {component.componentDidMount()
}
7,key 属性
节点比照时,在两个元素进行比对时,如果类型雷同,就循环旧的 DOM 对象的子元素,查看其身上是否有 key 属性,如果有就将这个子元素的 DOM 对象存储在一个 JavaScript 对象中,接着循环要渲染的 Virtual DOM 对象的子元素,在循环过程中获取到这个子元素的 key 属性,而后应用这个 key 属性到 JavaScript 对象中查找 DOM 对象,如果可能找到就阐明这个元素是曾经存在的,是不须要从新渲染的。如果通过 key 属性找不到这个元素,就阐明这个元素是新增的是须要渲染的。
// diff.js
else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
// 将领有 key 属性的元素放入 keyedElements 对象中
let keyedElements = {}
for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) {let domElement = oldDOM.childNodes[i]
if (domElement.nodeType === 1) {let key = domElement.getAttribute("key")
if (key) {keyedElements[key] = domElement
}
}
}
}
// diff.js
// 看一看是否有找到了领有 key 属性的元素
let hasNoKey = Object.keys(keyedElements).length === 0
// 如果没有找到领有 key 属性的元素 就依照索引进行比拟
if (hasNoKey) {
// 递归比照 Virtual DOM 的子元素
virtualDOM.children.forEach((child, i) => {diff(child, oldDOM, oldDOM.childNodes[i])
})
} else {
// 应用 key 属性进行元素比拟
virtualDOM.children.forEach((child, i) => {
// 获取要进行比对的元素的 key 属性
let key = child.props.key
// 如果 key 属性存在
if (key) {
// 到已存在的 DOM 元素对象中查找对应的 DOM 元素
let domElement = keyedElements[key]
// 如果找到元素就阐明该元素曾经存在 不须要从新渲染
if (domElement) {
// 尽管 DOM 元素不须要从新渲染 然而不能确定元素的地位就肯定没有发生变化
// 所以还要查看一下元素的地位
// 看一下 oldDOM 对应的 (i) 子元素和 domElement 是否是同一个元素 如果不是就阐明元素地位产生了变动
if (oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) {
// 元素地位产生了变动
// 将 domElement 插入到以后元素地位的后面 oldDOM.childNodes[i] 就是以后地位
// domElement 就被放入了以后地位
oldDOM.insertBefore(domElement, oldDOM.childNodes[i])
}
} else {mountElement(child, oldDOM, oldDOM.childNodes[i])
}
}
})
}
// mountNativeElement.js
if (oldDOM) {container.insertBefore(newElement, oldDOM)
} else {
// 将转换之后的 DOM 对象搁置在页面中
container.appendChild(newElement)
}
节点卸载。在比对节点的过程中,如果旧节点的数量多于要渲染的新节点的数量就阐明有节点被删除了,持续判断 keyedElements 对象中是否有元素,如果没有就应用索引形式删除,如果有就要应用 key 属性比对的形式进行删除。
实现思路是循环旧节点,在循环旧节点的过程中获取旧节点对应的 key 属性,而后依据 key 属性在新节点中查找这个旧节点,如果找到就阐明这个节点没有被删除,如果没有找到,就阐明节点被删除了,调用卸载节点的办法卸载节点即可。
// 获取就节点的数量
let oldChildNodes = oldDOM.childNodes
// 如果旧节点的数量多于要渲染的新节点的长度
if (oldChildNodes.length > virtualDOM.children.length) {if (hasNoKey) {
for (
let i = oldChildNodes.length - 1;
i >= virtualDOM.children.length;
i--
) {oldDOM.removeChild(oldChildNodes[i])
}
} else {for (let i = 0; i < oldChildNodes.length; i++) {let oldChild = oldChildNodes[i]
let oldChildKey = oldChild._virtualDOM.props.key
let found = false
for (let n = 0; n < virtualDOM.children.length; n++) {if (oldChildKey === virtualDOM.children[n].props.key) {
found = true
break
}
}
if (!found) {unmount(oldChild)
i--
}
}
}
}
卸载节点并不仅蕴含将节点间接删除的状况,还有以下几种状况
- 如果要删除的节点是文本节点的话能够间接删除
- 如果要删除的节点由组件生成,须要调用组件卸载生命周期函数
- 如果要删除的节点中蕴含了其余组件生成的节点,须要调用其余组件的卸载生命周期函数
- 如果要删除的节点身上有 ref 属性,还须要删除通过 ref 属性传递给组件的 DOM 节点对象
- 如果要删除的节点身上有事件,须要删除事件对应的事件处理函数
export default function unmount(dom) {
// 获取节点对应的 virtualDOM 对象
const virtualDOM = dom._virtualDOM
// 如果要删除的节点时文本
if (virtualDOM.type === "text") {
// 间接删除节点
dom.remove()
// 阻止程序向下运行
return
}
// 查看节点是否由组件生成
let component = virtualDOM.component
// 如果由组件生成
if (component) {
// 调用组件卸载生命周期函数
component.componentWillUnmount()}
// 如果节点具备 ref 属性 通过再次调用 ref 办法 将传递给组件的 DOM 对象删除
if (virtualDOM.props && virtualDOM.props.ref) {virtualDOM.props.ref(null)
}
// 事件处理
Object.keys(virtualDOM.props).forEach(propName => {if (propName.slice(0, 2) === "on") {const eventName = propName.toLowerCase().slice(2)
const eventHandler = virtualDOM.props[propName]
dom.removeEventListener(eventName, eventHandler)
}
})
// 递归删除子节点
if (dom.childNodes.length > 0) {for (let i = 0; i < dom.childNodes.length; i++) {unmount(dom.childNodes[i])
i--
}
}
dom.remove()}