继上一章讲完根底搭建之后,本章将持续讲述解决组件及比照更新。
组件解决
在持续之前,须要明确一点,针对类组件和函数组件,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属性,将在下一章增加。