继上一章讲完根底搭建之后,本章将持续讲述解决组件及比照更新。
组件解决
在持续之前,须要明确一点,针对类组件和函数组件,babel 调用 createElement 时传入的 type 是一个函数,如果是函数组件,type 指向的就是那个函数,如果是类组件,type 指向的是类的构造函数。
函数组件
在入口文件中增加函数组件:
虚构 Dom 的渲染入口在 mountElement 函数中,此时增加判断,如果 type 是一个函数,阐明是组件,须要独自渲染。
mountElement.js:
import mountNativeElement from './mountNativeElement'
import mountComponent from './mountComponent'
export default function mountElement(
virtualDom,
container
) {
// 如果 type 是 function,须要依照组件进行渲染
if (typeof virtualDom.type === 'function') {mountComponent(virtualDom, container)
} else {
// 调用 mountNativeElement 办法将虚构 Dom 转换为实在 Dom
mountNativeElement(virtualDom, container)
}
}
在 mountComponent 中须要判断是函数组件还是类组件,判断根据是如果 type 是函数,并且在原型上没有 render 办法,此时是函数组件,否则是类组件。
isFunctionComponent.js
export default function isFunctionComponent(virtualDOM) {
const type = virtualDOM.type
return (type && typeof virtualDOM.type === 'function' && !(type.prototype && type.prototype.render)
)
}
mountComponent.js:
import isFunctionComponent from "./isFunctionComponent"
import mountNativeElement from "./mountNativeElement"
import bindFunctionalComponent from './bindFunctionalComponent'
export default function mountComponent(virtualDom, container) {
let nextVirtualDom = null
// 处理函数组件
if (isFunctionComponent(virtualDom)) {nextVirtualDom = bindFunctionalComponent(virtualDom)
} else {// 解决类组件}
// 调用函数,返回值可能是一般的虚构 Dom,也可能是另一个组件
if (typeof nextVirtualDom.type === 'function') {
// 持续调用本身
mountComponent(nextVirtualDom, container)
} else {
// 当作一般的虚构 Dom 节点解决
mountNativeElement(nextVirtualDom, container)
}
}
bindFunctionalComponent.js:
export default function bindFunctionalComponent(virtualDom) {
// type 指向的就是函数组件申明时的函数
// 调用函数并将 props 作为参数传递进去
return virtualDom && virtualDom.type(virtualDom.props || {})
}
此时函数组件解决实现,刷新页面,能够看到函数组件可能被失常渲染:
类组件
在 MyReact 对象上增加 Component 类,所有的类组件均继承自此类:
Component.js:
// 类组件父类
export default class Component {constructor(props) {this.props = props}
}
批改入口文件,增加类组件:
在函数组件的逻辑中,曾经判断,如果 type 是一个函数,并且原型上没有 render 办法,就是一个函数组件,否则是类组件。欠缺 mountComponent 中的 else 判断:
import isFunctionComponent from "./isFunctionComponent"
import mountNativeElement from "./mountNativeElement"
import bindFunctionalComponent from './bindFunctionalComponent'
import bindStatefulComponent from './bindStatefulComponent'
export default function mountComponent(virtualDom, container) {
let nextVirtualDom = null
// 处理函数组件
if (isFunctionComponent(virtualDom)) {nextVirtualDom = bindFunctionalComponent(virtualDom)
} else {
// 解决类组件
nextVirtualDom = bindStatefulComponent(virtualDom)
}
// 调用函数,返回值可能是一般的虚构 Dom,也可能是另一个组件
if (typeof nextVirtualDom.type === 'function') {
// 持续调用本身
mountComponent(nextVirtualDom, container)
} else {
// 当作一般的虚构 Dom 节点解决
mountNativeElement(nextVirtualDom, container)
}
}
bindStatefulComponent.js:type 属性中保留着类组件的构造函数,通过 new 创立实例,并调用 render 办法。
export default function bindStatefulComponent(virtualDom) {
// 创立实例
const component = new virtualDom.type(virtualDom.props || {})
// 调用 render 办法
const nextVirtualDom = component.render()
return nextVirtualDom
}
此时,类组件也曾经解决实现,当刷新页面,能够看到页面上可能失常显示类组件。
比照更新
当发生变化后,react 会比照更新前和更新后的虚构 Dom,并将须要更新的节点更新到页面上,此时,更新后的虚构 Dom 能够通过 render 办法第一个参数传递过来,那么更新前的虚构 Dom 怎么获取呢?
在 createDomElement 办法中,创立完 Dom 节点之后,能够将虚构 Dom 节点增加到 Dom 节点的某个属性上,当比照的时候,从旧的 Dom 节点上通过该属性就能够获取旧的虚构 Dom:
比照更新的过程咱们仍然从一般虚构 Dom 节点开始,批改入口文件:
在定时 2 秒后,将应用更新后的虚构 Dom 执行 render 办法,虚构 Dom 的变动次要产生在四个方面:
- 批改原有元素节点的类型,从 h2 批改为 h3。
- 批改一段文本内容。
- 批改元素节点属性值。
- 删除一段节点。
在之前的 diff 办法中,只判断了 root 下没有节点的状况,当初须要补充含有节点的逻辑:
import mountElement from './mountElement'
export default function diff(
// 虚构 dom
virtualDom,
// 容器
container,
// 旧节点
oldDom = container.firstChild
) {
// 如果旧的节点不存在,不须要比照,间接渲染并挂载到容器下
if(!oldDom) {mountElement(virtualDom, container)
}
// 执行比照更新
}
节点类型不同
如果更新前后节点类型不同,不须要持续比照,间接用新的虚构 Dom 渲染 Dom 节点,并替换原有的 Dom 节点。
diff.js
import mountElement from './mountElement'
import createDomElement from './createDomElement'
export default function diff(
// 虚构 dom
virtualDom,
// 容器
container,
// 旧节点
oldDom = container.firstChild
) {
// 获取旧的虚构节点
const oldVirtualDom = oldDom && oldDom._virtualDom
// 如果旧的节点不存在,不须要比照,间接渲染并挂载到容器下
if (!oldDom) {mountElement(virtualDom, container)
}
// 执行比照更新
else if (
virtualDom.type !== oldVirtualDom.type &&
typeof virtualDom.type !== 'function'
) {
// 如果新旧虚构 Dom 节点的类型不同,并且新的虚构 Dom 不是组件
const newDomElement = createDomElement(virtualDom)
oldDom.parentNode.replaceChild(newDomElement, oldDom)
}
}
节点类型雷同
当新旧虚构 Dom 节点的节点类型雷同的时候,须要判断新的虚构 Dom 节点是否是文本类型,如果是文本类型,比照文本内容是否雷同,如果不是文本类型,比照节点的属性是否发生变化:
diff.js
import mountElement from './mountElement'
import createDomElement from './createDomElement'
import updateTextNode from './updateTextNode'
import updateElementNode from './updateElementNode'
export default function diff(
// 虚构 dom
virtualDom,
// 容器
container,
// 旧节点
oldDom = container.firstChild
) {
// 获取旧的虚构节点
const oldVirtualDom = oldDom && oldDom._virtualDom
// 如果旧的节点不存在,不须要比照,间接渲染并挂载到容器下
if (!oldDom) {mountElement(virtualDom, container)
}
// 执行比照更新
else if (
virtualDom.type !== oldVirtualDom.type &&
typeof virtualDom.type !== 'function'
) {
// 如果新旧虚构 Dom 节点的类型不同,并且新的虚构 Dom 不是组件
const newDomElement = createDomElement(virtualDom)
oldDom.parentNode.replaceChild(newDomElement, oldDom)
} else if (oldVirtualDom && virtualDom.type === oldVirtualDom.type) {
// 节点类型雷同
if (virtualDom.type === 'text') {
// 文本节点
updateTextNode(virtualDom, oldVirtualDom, oldDom)
} else {
// 元素节点
updateElementNode(oldDom, virtualDom, oldVirtualDom)
}
}
}
updateTextNode.js
export default function updateTextNode(virtualDOM, oldVirtualDOM, oldDOM) {if (virtualDOM.props.textContent !== oldVirtualDOM.props.textContent) {
// 批改旧的文本节点文本内容
oldDOM.textContent = virtualDOM.props.textContent
}
// 将新的虚构节点增加到 dom 节点的_virtualDom 属性中
oldDOM._virtualDom = virtualDOM
}
批改原有的 updateElementNode.js
元素节点的属性变动能够分为两种:
- 变动前后均有该属性,属性值发生变化。
- 变动前有该属性,变动后没有该属性,属性被删除。
在 updateElementNode.js 能够通过两个循环实现上述两种状况判断,首先循环更新后的所有属性,判断和更新前的是否雷同,如果不同,则更新属性。而后循环更新前的属性,判断是否在更新后不存在该属性,如果不存在则删除该属性:
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)
}
}
})
}
比照子节点
如果新旧节点类型雷同,上文中只比照了同层节点,其上面的所有子节点须要遍历比照:
删除节点
当比照更新实现后,须要判断是否有节点被删除了,标记就是旧节点子节点的数量少于新节点子节点的数量,此时须要删除节点:
import mountElement from './mountElement'
import createDomElement from './createDomElement'
import updateTextNode from './updateTextNode'
import updateElementNode from './updateElementNode'
export default function diff(
// 虚构 dom
virtualDom,
// 容器
container,
// 旧节点
oldDom = container.firstChild
) {
// 获取旧的虚构节点
const oldVirtualDom = oldDom && oldDom._virtualDom
// 如果旧的节点不存在,不须要比照,间接渲染并挂载到容器下
if (!oldDom) {mountElement(virtualDom, container)
}
// 执行比照更新
else if (
virtualDom.type !== oldVirtualDom.type &&
typeof virtualDom.type !== 'function'
) {
// 如果新旧虚构 Dom 节点的类型不同,并且新的虚构 Dom 不是组件
const newDomElement = createDomElement(virtualDom)
oldDom.parentNode.replaceChild(newDomElement, oldDom)
} else if (oldVirtualDom && virtualDom.type === oldVirtualDom.type) {
// 节点类型雷同
if (virtualDom.type === 'text') {
// 文本节点
updateTextNode(virtualDom, oldVirtualDom, oldDom)
} else {
// 元素节点
updateElementNode(oldDom, virtualDom, oldVirtualDom)
}
// 递归遍历子节点
virtualDom.children.forEach((child, i) => {diff(child, oldDom, oldDom.childNodes[i])
})
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])
}
}
}
}
组件更新
当一般虚构 Dom 的更新实现后,须要思考组件更新,组件更新中比较复杂的是类组件状态更新,也就是调用 setState 办法批改组件状态。
类组件状态更新
批改入口文件,为类组件增加 setState 调用:
在 Component 类中增加 setState 办法:
// 类组件父类
export default class Component {constructor(props) {this.props = props}
setState(state) {
// 合并 state 对象
this.state = Object.assign({}, this.state, state)
}
}
此时,在 setState 变更 state 对象后,能够通过 this.render 获取最新的虚构 Dom:
setState(state) {
// 合并 state 对象
this.state = Object.assign({}, this.state, state)
// 获取最新的虚构 Dom
let virtualDom = this.render()}
那么旧的虚构 Dom 怎么获取?因为旧的虚构 Dom 寄存在实在 Dom 的_virtualDom 属性中,这个问题也就变成在 setState 办法中怎么获取选而后的 Dom 对象。
能够为 Component 增加 setDom 和 getDom 办法:
// 类组件父类
export default class Component {constructor(props) {this.props = props}
setState(state) {
// 合并 state 对象
this.state = Object.assign({}, this.state, state)
// 获取最新的虚构 Dom
let virtualDom = this.render()}
setDom(dom) {this._dom = dom}
getDom(dom) {return this._dom}
}
在渲染的时候,能够通过调用实例的 setDom 办法将 Dom 对象存储到实例的_dom 属性中。
首先,在类组件实例化之后,须要将实例存储到虚构 Dom 中:
bindStatefulComponent.js
export default function bindStatefulComponent(virtualDom) {
// 创立实例
const component = new virtualDom.type(virtualDom.props || {})
// 调用 render 办法
const nextVirtualDom = component.render()
// 将实例增加到虚构 Dom 对象中
nextVirtualDom.component = component
return nextVirtualDom
}
将 diff 办法调用的所有办法增加第三个参数 oldDom(略):
因为类组件 render 办法最终返回的是一般虚构 Dom,当调用 mountNativeElement 办法后,能够通过虚构 Dom 获取类组件实例,而后调用 setDom 将 oldDom 传递给组件实例:
import createDomElement from './createDomElement'
export default function mountNativeElement(virtualDom, container, oldDOM) {
// 调用 createDomElement 创立 Dom
const newElement = createDomElement(virtualDom)
// 将转换之后的 DOM 对象搁置在页面中
if (oldDOM) {container.insertBefore(newElement, oldDOM)
} else {container.appendChild(newElement)
}
// 获取类组件实例对象
let component = virtualDom.component
// 如果类组件实例对象存在
if (component) {
// 将 DOM 对象存储在类组件实例对象中
component.setDom(newElement)
}
}
此时在 setState 办法中就能够取得旧的虚构 Dom 对象,而后获取类组件父容器,再调用 diff 办法实现更新。
import diff from "./diff"
// 类组件父类
export default class Component {constructor(props) {this.props = props}
setState(state) {
// 合并 state 对象
this.state = Object.assign({}, this.state, state)
// 获取最新的虚构 Dom
let virtualDom = this.render()
// 获取旧的 virtualDom 对象 进行比对
let oldDom = this.getDom()
// 获取容器
let container = oldDom.parentNode
// 实现对象
diff(virtualDom, container, oldDom)
}
setDom(dom) {this._dom = dom}
getDom() {return this._dom}
}
此时类组件的状态更新曾经实现。
更新组件 props
在 diff 办法中增加是否是组件的判断,如果是组件须要独自解决组件的 props 更新。
diffComponent.js: 判断是否是同一个组件,如果不是,间接渲染新的组件,如果是,则更新组件。
import updateComponent from './updateComponent'
import mountElement from './mountElement'
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 && virtualDOM.type === oldComponent.constructor
}
updateComponent.js: 从新调用 render 办法获取新的虚构 Dom,而后调用 diff 比照更新。
import diff from "./diff"
export default function updateComponent(
virtualDOM,
oldComponent,
oldDOM,
container
) {
// 组件更新
oldComponent.updateProps(virtualDOM.props)
// 获取组件返回的最新的 virtualDOM
let nextVirtualDOM = oldComponent.render()
// 更新 component 组件实例对象
nextVirtualDOM.component = oldComponent
// 比对
diff(nextVirtualDOM, container, oldDOM)
}
updateProps: Component 类提供的用于更新 props 的办法:
updateProps(props) {this.props = props}
至此,组件更新操作曾经实现。
本章形容如果在自定义 react 框架中实现组件的渲染和 diff 比照更新,残余一部分如 ref 属性和 key 属性,将在下一章增加。