【JS】如何构建自定义React基础虚拟Dom框架(二)

继上一章讲完基础搭建之后,本章将继续讲述处理组件及对比更新。

组件处理

在继续之前,需要明确一点,针对类组件和函数组件,babel调用createElement时传入的type是一个函数,如果是函数组件,type指向的就是那个函数,如果是类组件,type指向的是类的构造函数。

函数组件

在入口文件中添加函数组件:

【JS】如何构建自定义React基础虚拟Dom框架(二)

虚拟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 || {})

}

此时函数组件处理完成,刷新页面,可以看到函数组件能够被正常渲染:

【JS】如何构建自定义React基础虚拟Dom框架(二)

类组件

在MyReact对象上添加Component类,所有的类组件均继承自此类:

Component.js:

// 类组件父类

export default class Component {

constructor(props) {

this.props = props

}

}

修改入口文件,添加类组件:

【JS】如何构建自定义React基础虚拟Dom框架(二)

在函数组件的逻辑中,已经判断,如果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

}

此时,类组件也已经处理完成,当刷新页面,可以看到页面上能够正常显示类组件。

【JS】如何构建自定义React基础虚拟Dom框架(二)

对比更新

当发生变化后,react会对比更新前和更新后的虚拟Dom,并将需要更新的节点更新到页面上,此时,更新后的虚拟Dom可以通过render方法第一个参数传递过去,那么更新前的虚拟Dom怎么获取呢?

在createDomElement方法中,创建完Dom节点之后,可以将虚拟Dom节点添加到Dom节点的某个属性上,当对比的时候,从旧的Dom节点上通过该属性就可以获取旧的虚拟Dom:

【JS】如何构建自定义React基础虚拟Dom框架(二)

对比更新的过程我们依然从普通虚拟Dom节点开始,修改入口文件:

【JS】如何构建自定义React基础虚拟Dom框架(二)

在定时2秒后,将使用更新后的虚拟Dom执行render方法,虚拟Dom的变化主要发生在四个方面:

  1. 修改原有元素节点的类型,从h2修改为h3。
  2. 修改一段文本内容。
  3. 修改元素节点属性值。
  4. 删除一段节点。

在之前的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

元素节点的属性变化可以分为两种:

  1. 变化前后均有该属性,属性值发生变化。
  2. 变化前有该属性,变化后没有该属性,属性被删除。

在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)

}

}

})

}

对比子节点

如果新旧节点类型相同,上文中只对比了同层节点,其下面的所有子节点需要遍历对比:

【JS】如何构建自定义React基础虚拟Dom框架(二)

删除节点

当对比更新完成后,需要判断是否有节点被删除了,标志就是旧节点子节点的数量少于新节点子节点的数量,此时需要删除节点:

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调用:

【JS】如何构建自定义React基础虚拟Dom框架(二)

在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(略):

【JS】如何构建自定义React基础虚拟Dom框架(二)
【JS】如何构建自定义React基础虚拟Dom框架(二)
【JS】如何构建自定义React基础虚拟Dom框架(二)

由于类组件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更新。

【JS】如何构建自定义React基础虚拟Dom框架(二)

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属性,将在下一章添加。

以上是 【JS】如何构建自定义React基础虚拟Dom框架(二) 的全部内容, 来源链接: utcz.com/a/104411.html

回到顶部