DOM-DIFF的相关知识不再赘述,本文将逐步实现完整的DOM-DIFF。欢迎大家观看并指导~
一、实现思路
- 绿色元素:更新
- 蓝色元素:插入
- 橙色元素:插入并更新
- 红色元素:删除
1. 构建map
把老节点的key和其对应的虚拟DOM节点关联起来存到map中,形成:
map = {
'A': 'A对应的虚拟DOM',
'B': 'B对应的虚拟DOM',
'C': 'C对应的虚拟DOM',
'D': 'D对应的虚拟DOM',
'E': 'E对应的虚拟DOM',
'F': 'F对应的虚拟DOM',
}
这样我们就可以通过key去查找对应的老节点
2. 循环新节点数组
遍历的时候记录每一个新节点的key,同时获取这个key对应的虚拟DOM节点。此时,拿着key去第一步的map
中查找,如果找得到就说明有对应的老节点,那么我们就复用老节点:
2-1. 把新节点的属性更新到老节点上
2-2. 判断节点是否移动
判断是否移动,借助变量lastPlaceIndex
上一个不需要移动的节点的索引判断当前节点是否需要移动。如果老节点挂载的索引,大于lastPlaceIndex
的值,就不需要移动;如果小于lastPlaceIndex
的值,就移动到当前索引得到位置。移动的元素新增MOVE类型,然后push
到补丁包patch
中,之后删除这个被复用的节点。
2-3. 获取需要移动的元素
经过上面的步骤,此时老节点数组中剩余的就是没有被复用的节点。我们需要把节点从父节点中删除掉。
2-4. 插入或移动节点
根据新的虚拟DOM创建新的真实DOM后,获取老DOM对应的索引处的新DOM,然后inserBefore
新DOM前。如果没有新DOM则直接appenedChild
。
二、代码实现
1. src/index.js
import React from "./react";
import ReactDOM from "./react-dom";
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
list: ["A", "B", "C", "D", "E", "F"],
};
}
handClick = () => {
this.setState({
list: ["A", "C", "E", "B", "G"],
});
};
render() {
return (
<div>
<ul>
{this.state.list.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
<button onClick={this.handClick}>按钮</button>
</div>
);
}
}
ReactDOM.render(<Counter />, document.getElementById("root"));
2. src/constants.js
//React元素:h1 span div
export const REACT_ELEMENT = Symbol("react.element");
//文本:字符串或数字
export const REACT_TEXT = Symbol("react.text");
// 函数组件转发的ref
export const REACT_FORWARD_REF = Symbol("react.forward_ref");
> > > // 插入节点
> > > export const PLACEMENT = Symbol("PLACEMENT");
> > >
> > > // 移动节点
> > > export const MOVE = Symbol("MOVE");
3. src/react-dom.js
这个文件较长,所以与DOM-DIFF无关的代码做了删减,如果想要查看完整代码,请移步我的码云仓库,或者在前一节文章中查看。
> > > import { REACT_TEXT, REACT_FORWARD_REF, MOVE, PLACEMENT } from "./constants";
import { addEvent } from "./event";
/**
*把虚拟DOM变成真实DOM插入容器
* @param {*} vdom 虚拟DOM/React元素
* @param {*} container 真实DOM容器
*/
function render(vdom, container) {
mount(vdom, container);
}
/** 页面挂载真实DOM */
function mount(vdom, parentDOM) {}
/**
* 把虚拟DOM变成真实DOM
* @param {*} vdom 虚拟DOM
* @return 真实DOM
*/
function createDOM(vdom) {}
/** 挂载类组件 */
function mountClassComponent(vdom) {}
/** 挂载函数组件 */
function mountFunctionComponent(vdom) {}
/** 挂载经过转发的ref的函数组件 */
function mountForwardComponent(vdom) {}
/** 如果子元素为数组,遍历挂载到容器 */
function reconcileChildren(children, parentDOM) {
// 给每个虚拟DOM挂载mountIndex属性记录其索引
children.forEach((childVdom, index) => {
> > > childVdom.mountIndex = index;
> > > mount(childVdom, parentDOM);
});
}
/**
* 把新的属性更新到真实DOM上
* @param {*} dom 真实DOM
* @param {*} oldProps 旧的属性对象
* @param {*} newProps 新的属性对象
*/
function updateProps(dom, oldProps, newProps) {}
/**
* DOM-DIFF:递归比较老的虚拟DOM和新的虚拟DOM,找出两者的差异,把这些差异最小化的同步到真实DOM上
* @param {*} parentDOM 父真实DOM
* @param {*} oldVdom 老的虚拟DOM
* @param {*} newVdom 新的虚拟DOM
* @param {*} nextDOM 新的虚拟DOM
*
*/
export function compareToVdom(parentDOM, oldVdom, newVdom, nextDOM) {
// 1.老-无 新-无:啥也不干
if (!oldVdom && !newVdom) return;
// 2.老-有 新-无:直接删除老节点
if (oldVdom && !newVdom) {
unMountVdom(oldVdom);
}
// 3.老-无 新-有:插入节点
if (!oldVdom && newVdom) {
mountVdom(parentDOM, newVdom, nextDOM);
}
// 4-1.老-有 新-有:判断类型不一样,删除老的,添加新的
if (oldVdom && newVdom && oldVdom.type !== newVdom.type) {
unMountVdom(oldVdom);
mountVdom(parentDOM, newVdom, nextDOM);
}
// 4-2.老-有 新-有:判断类型一样,进行DOM-DIFF,并且节点可复用
if (oldVdom && newVdom && oldVdom.type == newVdom.type) {
updateElement(oldVdom, newVdom);
}
}
/**
* DOM-DIFF精髓之处 --- 新老DOM类型一样的更新
* 如果新老DOM的类型一样,那么节点就可以复用
*/
function updateElement(oldVdom, newVdom) {
// 新老节点都是文本节点:复用老的节点,替换内容
if (oldVdom.type === REACT_TEXT) {
// 老的真实DOM给新的DOM的dom属性,把内容改掉
let currentDOM = (newVdom.dom = findDOM(oldVdom));
currentDOM.textContent = newVdom.props.content;
// 原生节点
} else if (typeof oldVdom.type === "string") {
let currentDOM = (newVdom.dom = findDOM(oldVdom));
// 更新属性
updateProps(currentDOM, oldVdom.props, newVdom.props);
// 递归比较儿子
updateChildren(currentDOM, oldVdom.props.children, newVdom.props.children);
// 类组件或函数组件
} else if (typeof oldVdom.type === "function") {
// 类组件
if (oldVdom.type.isReactComponent) {
// 先同步实例
newVdom.classInstance = oldVdom.classInstance;
updateClassComponent(oldVdom, newVdom);
// 函数组件
} else {
updateFunctionComponent(oldVdom, newVdom);
}
}
}
/**
* 更新类组件
* @param {*} oldVdom
* @param {*} newVdom
*/
function updateClassComponent(oldVdom, newVdom) {
// 复用老的类组件实例
let classInstance = (newVdom.classInstance = oldVdom.classInstance);
if (classInstance.componentWillReceiveProps) {
classInstance.componentWillReceiveProps(newVdom.props);
}
classInstance.updater.emitUpdate(newVdom.props);
}
/**
* 更新函数组件
* @param {*} oldVdom
* @param {*} newVdom
*/
function updateFunctionComponent(oldVdom, newVdom) {
// 获取老的真实DOM的父节点
let parentDOM = findDOM(oldVdom).parentNode;
let { type, props } = newVdom;
let newRenderVdom = type(props);
// 函数组件更新每次都要重新执行函数,拿到新的虚拟DOM
compareToVdom(parentDOM, oldVdom.oldRenderVdom, newRenderVdom);
newVdom.newRenderVdom = newRenderVdom;
}
/**
* 递归比较子节点
* @param {*} parentDOM
* @param {*} oldVChildren
* @param {*} newVChildren
*/
function updateChildren(parentDOM, oldVChildren, newVChildren) {
// 为方便后续进行DOM-DIFF,以数组形式保存
oldVChildren = Array.isArray(oldVChildren) ? oldVChildren : [oldVChildren];
newVChildren = Array.isArray(newVChildren) ? newVChildren : [newVChildren];
> > > // DOM-DIFF 1.构建老map {虚拟DOM的key: 虚拟DOM}
> > > let keyedOldMap = {};
> > > oldVChildren.forEach((oldVChild, index) => {
> > > let oldKey = oldVChild.key ? oldVChild.key : index;
> > > keyedOldMap[oldKey] = oldVChild;
> > > });
> > > // 补丁包:存放要进行的操作
> > > let patch = [];
> > > // 上一个放置好的、不需要移动的索引
> > > let lastPlaceIndex = 0;
> > > // DOM-DIFF 2.遍历新数组查找老虚拟DOM
> > > newVChildren.forEach((newVChild, index) => {
> > > newVChild.mountIndex = index;
> > > let newKey = newVChild.key ? newVChild.key : index;
> > > // 查找老的虚拟DOM中是否存在这个key的节点
> > > let oldVChild = keyedOldMap[newKey];
> > > // 如果找到,复用老节点
> > > if (oldVChild) {
> > > // 先更新
> > > updateElement(oldVChild, newVChild);
> > > // 判断是否移动 把oldVChild移动到mountIndex当前索引处
> > > if (oldVChild.mountIndex < lastPlaceIndex) {
> > > patch.push({
> > > type: MOVE,
> > > oldVChild,
> > > newVChild,
> > > mountIndex: index,
> > > });
> > > }
> > > // 删除已经被复用的节点
> > > delete keyedOldMap[newKey];
> > > lastPlaceIndex = Math.max(oldVChild.mountIndex, lastPlaceIndex);
> > > } else {
> > > // 如果没找到,插入新节点
> > > patch.push({
> > > type: PLACEMENT,
> > > newVChild,
> > > mountIndex: index,
> > > });
> > > }
> > > });
> > > // DOM-DIFF 3.获取需要移动的元素
> > > let moveChildren = patch
> > > .filter((action) => action.type === MOVE)
> > > .map((action) => action.oldVChild);
> > > // 遍历map留下的元素(其实就是没有被复用的)
> > > Object.values(keyedOldMap)
> > > .concat(moveChildren)
> > > .forEach((oldVChild) => {
> > > // 获取老DOM
> > > let currentDOM = findDOM(oldVChild);
> > > parentDOM.removeChild(currentDOM);
> > > });
> > > // DOM-DIFF 4.插入或移动节点
> > > patch.forEach((action) => {
> > > let { type, oldVChild, newVChild, mountIndex } = action;
> > > // 真实DOM节点集合
> > > let childNodes = parentDOM.childNodes;
> > > if (type === PLACEMENT) {
> > > // 根据新的虚拟DOM创建新真实DOM
> > > let newDOM = createDOM(newVChild);
> > > // 获取老DOM中对应的索引处的真实DOM
> > > let childNode = childNodes[mountIndex];
> > > if (childNode) {
> > > parentDOM.insertBefore(newDOM, childNode);
> > > } else {
> > > parentDOM.appendChild(newDOM);
> > > }
> > > } else if (type === MOVE) {
> > > let oldDOM = findDOM(oldVChild);
> > > let childNode = childNodes[mountIndex];
> > > if (childNode) {
> > > parentDOM.insertBefore(oldDOM, childNode);
> > > } else {
> > > parentDOM.appendChild(oldDOM);
> > > }
> > > }
> > > });
> > >
> > > // // 最大长度
> > > // let maxLength = Math.max(oldVChildren.length, newVChildren.length);
> > > // // 每一个都进行深度对比
> > > // for (let i = 0; i < maxLength; i++) {
> > > // // 在老的虚拟DOM查找,有老节点并且老节点真的对应一个真实DOM节点,并且这个索引要比我大(目的是找到本身的下一个节点)
> > > // let nextVdom = oldVChildren.find(
> > > // (item, index) => index > i && item && findDOM(item)
> > > // );
> > > // compareToVdom(
> > > // parentDOM,
> > > // oldVChildren[i],
> > > // newVChildren[i],
> > > // nextVdom && findDOM(nextVdom)
> > > // );
> > > // }
}
/**
* 插入新的真实DOM
* @param {}} parentDOM
* @param {*} vdom
* @param {*} nextDOM
*/
function mountVdom(parentDOM, newVdom, nextDOM) {
let newDOM = createDOM(newVdom);
if (nextDOM) {
parentDOM.insertBefore(newDOM, nextDOM);
} else {
parentDOM.appendChild(newDOM);
}
if (newDOM.componentDidMount) {
newDOM.componentDidMount();
}
}
/**
* 删除老的真实DOM
* @param {*} vdom 老的虚拟DOM
*/
function unMountVdom(vdom) {
let { type, props, ref } = vdom;
// 获取老的真实DOM
let currentDOM = findDOM(vdom);
// 如果这个子节点是类组件,还要执行它的卸载的生命周期函数
if (vdom.classInstance && vdom.classInstance.componentWillUnmount) {
vdom.classInstance.componentWillUnmount();
}
// 如果有ref,删除ref对应的真实DOM
if (ref) ref.current = null;
// 取消监听函数
Object.keys(props).forEach((propName) => {
if (propName.slice(0, 2) === "on") {
// 事件在真实dom就这样做
// const eventName = propName.slice(2).toLowerCase()
// currentDOM.removeEventListener(eventName, props[propName])
//但是我们先处理了合成事件,事件注册再store上
delete currentDOM.store;
}
});
// 如果有子节点,递归删除所有子节点
if (props.children) {
let children = Array.isArray(props.children)
? props.children
: [props.children];
children.forEach(unMountVdom);
}
// 从父节点中把自己删除
if (currentDOM) currentDOM.parentNode.removeChild(currentDOM);
}
/** 虚拟DOM返回的真实DOM */
export function findDOM(vdom) {
if (!vdom) return null;
// 如果有dom属性,说明这个vdom是原生组件的虚拟DOM,会有dom属性指向真实dom
if (vdom.dom) {
return vdom.dom;
} else {
return findDOM(vdom.oldRenderVdom);
}
}
const ReactDOM = {
render,
};
export default ReactDOM;
三、总结
移动原则:新数组中索引较小的,地位较高的不移动。按照以上逻辑,如果把最后一个元素移动到第一个元素,其余的元素都要发生移动,所以我们应该尽量减少置顶的操作。