03.React DOM DIFF算法源码实现
具体代码演化过程请观看视频,这里呈现关键代码:
js
// utils.js
export const REACT_TEXT = Symbol('react.text')///
export const MOVE = Symbol('dom.diff.move')//
export const CREATE = Symbol('dom.diff.create')//
export const toVNode = (node) => {//
return typeof node === 'string' || typeof node === 'number' ? {//
type: REACT_TEXT, props: {text: node}//
} : node//
}//js
// react.js
import { REACT_ELEMENT, REACT_FORWARD_REF, toVNode } from './utils' ///
if(arguments.length > 3){
props.children = Array.prototype.slice.call(arguments, 2).map(toVNode);///
}else{
props.children = toVNode(children)///
}js
// Component.js
update(){
// 1. 获取重新执行render函数后的虚拟DOM 新虚拟DOM
// 2. 根据新虚拟DOM生成新的真实DOM
// 3. 将真实DOM挂载到页面上
let oldVNode = this.oldVNode; // TODO: 让类组件拥有一个oldVNode属性保存类组件实例对应的的虚拟DOM
let oldDOM = findDomByVNode(oldVNode) // TODO: 将真实DOM保存到对应的虚拟DOM上
let newVNode = this.render()
updateDomTree(oldVNode, newVNode, oldDOM) ////
this.oldVNode = newVNode
}js
// react-dom.js
import { REACT_ELEMENT, REACT_FORWARD_REF, MOVE, CREATE, REACT_TEXT } from './utils'///
function createDOM(VNode){
// 1.创建元素 2.处理子元素 3.处理属性值
...
if (type === REACT_TEXT){ ////
dom = document.createTextNode(props.text);///
} if(type && VNode.$$typeof === REACT_ELEMENT){///
dom = document.createElement(type)///
}///
if(props){
if(typeof props.children === 'object' && props.children.type){
mount(props.children, dom)
}else if(Array.isArray(props.children)){
mountArray(props.children, dom)
}////
/////
}
setPropsForDOM(dom, props)
VNode.dom = dom
ref && (ref.current = dom)
return dom
}
function mountArray(children, parent){
if(!Array.isArray(children)) return
for(let i = 0; i < children.length; i++){
children[i].index = i;////
mount(children[i], parent)////
}
}
function updateChildren(parentDOM, oldVNodeChildren, newVNodeChildren) {
oldVNodeChildren = (Array.isArray(oldVNodeChildren) ? oldVNodeChildren : [oldVNodeChildren]).filter(Boolean);
newVNodeChildren = (Array.isArray(newVNodeChildren) ? newVNodeChildren : [newVNodeChildren]).filter(Boolean);
// 利用Map数据结构为旧的虚拟DOM数组找到key和节点的关系,为后续根据key查找是否有可复用的虚拟DOM创造条件
let lastNotChangedIndex = -1;
let oldKeyChildMap = {};
oldVNodeChildren.forEach((oldVNode, index) => {
let oldKey = oldVNode && oldVNode.key ? oldVNode.key : index;
oldKeyChildMap[oldKey] = oldVNode;
});
// 遍历新的子虚拟DOM树组,找到可以复用但需要移动的、需要重新创建的、需要删除的节点,剩下的都是不用动的节点
let actions = [];
newVNodeChildren.forEach((newVNode, index) => {
if(typeof newVNode !== 'string'){
newVNode.index = index;
}
let newKey = newVNode.key ? newVNode.key : index;
let oldVNode = oldKeyChildMap[newKey];
if (oldVNode) {
deepDOMDiff(oldVNode, newVNode);
if (oldVNode.index < lastNotChangedIndex) {
actions.push({
type: MOVE,
oldVNode,
newVNode,
index
});
}
delete oldKeyChildMap[newKey]
lastNotChangedIndex = Math.max(lastNotChangedIndex, oldVNode.index);
} else {
actions.push({
type: CREATE,
newVNode,
index
});
}
});
// 可以复用但需要移动位置的节点,以及用不上需要删除的节点,都从父节点上移除
let VNodeToMove = actions.filter(action => action.type === MOVE).map(action => action.oldVNode);
let VNodeToDelete = Object.values(oldKeyChildMap)
VNodeToMove.concat(VNodeToDelete).forEach(oldVChild => {
let currentDOM = findDomByVNode(oldVChild);
currentDOM.remove();
});
// 对需要移动以及需要新创建的节点统一插入到正确的位置
actions.forEach(action => {
debugger
let { type, oldVNode, newVNode, index } = action;
let childNodes = parentDOM.childNodes;
const getDomForInsert = () => {
if(type === CREATE){
return createDOM(newVNode)
}
if(type === MOVE){
return findDomByVNode(oldVNode)
}
}
let childNode = childNodes[index];
if (childNode) {
parentDOM.insertBefore(getDomForInsert(), childNode)
} else {
parentDOM.appendChild(getDomForInsert());
}
});
}
function updateClassComponent(oldVNode, newVNode) {
const classInstance = newVNode.classInstance = oldVNode.classInstance;
classInstance.updater.launchUpdate();
}
function updateFunctionComponent(oldVNode, newVNode) {
let oldDOM = findDomByVNode(oldVNode);
if (!oldDOM) return;
const { type, props } = newVNode;
let newRenderVNode = type(props);
updateDomTree(oldVNode.oldRenderVNode, newRenderVNode, oldDOM);
newVNode.oldRenderVNode = newRenderVNode;
}
function deepDOMDiff(oldVNode, newVNode) {
let diffTypeMap = {
ORIGIN_NODE: typeof oldVNode.type === 'string', // 原生节点
CLASS_COMPONENT: typeof oldVNode.type === 'function' && oldVNode.type.isReactComponent,
FUNCTION_COMPONENT: typeof oldVNode.type === 'function',
TEXT: oldVNode.type === REACT_TEXT
}
let DIFF_TYPE = Object.keys(diffTypeMap).filter(key => diffTypeMap[key])[0]
switch (DIFF_TYPE) {
case 'ORIGIN_NODE':
let currentDOM = newVNode.dom = findDomByVNode(oldVNode);
setPropsForDOM(currentDOM, newVNode.props)
updateChildren(currentDOM, oldVNode.props.children, newVNode.props.children);
break;
case 'CLASS_COMPONENT':
updateClassComponent(oldVNode, newVNode);
break;
case 'FUNCTION_COMPONENT':
updateFunctionComponent(oldVNode, newVNode);
break;
case 'TEXT':
newVNode.dom = findDomByVNode(oldVNode);
newVNode.dom.textContent = newVNode.props.text;
break;
default:
break;
}
}
function removeVNode(vNode) {
const currentDOM = findDomByVNode(vNode);
if (currentDOM) currentDOM.remove();
}
// 开始dom-diff
export function updateDomTree(oldVNode, newVNode, oldDOM) {
const typeMap = {
NO_OPERATE: !oldVNode && !newVNode,
ADD: !oldVNode && newVNode,
DELETE: oldVNode && !newVNode,
REPLACE: oldVNode && newVNode && oldVNode.type !== newVNode.type // 类型不同
}
let UPDATE_TYPE = Object.keys(typeMap).filter(key => typeMap[key])[0]
switch (UPDATE_TYPE) {
case 'NO_OPERATE':
break
case 'DELETE':
removeVNode(oldVNode);
break
case 'ADD':
oldDOM.parentNode.appendChild(createDOM(newVNode));
break
case 'REPLACE':
removeVNode(oldVNode);
// 这里直接追加到尾巴上
oldDOM.parentNode.appendChild(createDOM(newVNode));
break
default:
// 深度的 dom-diff,新老虚拟DOM都存在且类型相同
deepDOMDiff(oldVNode, newVNode)
break;
}js
// index.js
import React from './react';
import ReactDOM from './react-dom';
class MyClassComponent extends React.Component{
isReset = false
oldArr = ['A', 'B', 'C', 'D', 'E']
newArr = ['C', 'B', 'E', 'F', 'A']
constructor(props) {
super(props);
this.state = { arr: this.oldArr };
}
updateShowArr(){
this.setState({
arr: this.isReset ? this.oldArr : this.newArr
})
this.isReset = !this.isReset
}
render(){
return <div>
<div className='test-class' style={
{
color: 'red',
cursor: 'pointer',
border: '1px solid gray',
borderRadius: '6px',
display: 'inline-block',
padding: '6px 12px'
}
} onClick={ () => this.updateShowArr() }>Change The Text</div>
<div>
{
this.state.arr.map(item => {
return <div key={item}>{item}</div>
})
}
</div>
</div>
}
}
ReactDOM.render(<MyClassComponent />, document.getElementById('root'))