有不少文章讲过react的diff算法,但要么是晦涩难懂的源码分析,让人很难读进去,要么就是流于表面的简单讲解,实际上你们看完后仍是一头雾水,所以我将react-lite(基于react v15)中的diff算法实现稍微整理了一下,但愿可以帮助你们解惑。javascript
在看本文以前,建议先看一下这篇文章,看完后会对react中diff的基本原理有一些理解:blog.csdn.net/sexy_squirr…html
对于react diff,咱们已知的有两点,一个是会经过key来作比较,另外一个是react默认是同级节点作diff,不会考虑到跨层级节点的diff(事实是前端开发中不多有DOM节点跨层级移动的)。前端
首先,抛给咱们一个问题,那就是react怎么对那么深层次的DOM作的diff?实际上react是对DOM进行递归来作的,遍历全部子节点,对子节点再作递归。java
// 超简单代码实现
const compareTwoVnodes(oldVnode, newVnode, dom) {
let newNode = dom
// 若是新的虚拟DOM是null,那么就将前一次的真实DOM移除掉
if (newVnode == null) {
destroyVnode(oldVnode, dom)
dom.parentNode.removeChild(dom)
} else if (oldVnode.type !== newVnode.type || oldVnode.key !== newVnode.key) {
// replace
destroyVnode(oldVnode, dom)
newNode = initVnode(newVnode, parentContext, dom.namespaceURI)
dom.parentNode.replaceChild(newNode, dom)
} else if (oldVnode !== newVnode || parentContext) {
// same type and same key -> update
newNode = updateVNode(oldVnode, newVnode, dom, parentContext)
}
}
/** * 更新虚拟DOM * 这里的type须要注意一下,若是vnode是个html元素,例如h1,那么type就是'h1' * 若是vnode是一个函数组件,例如const Header = () => <h1>header</h1>,那么type就是函数Header * 若是vnode是一个class组件,那么type就是那个class */
const updateVNode = (vnode, node) => {
const { type } = vnode; // type是指虚拟DOM的类型
// 若是是class组件
if (type === VCOMPONENT) {
return updateComponent(vnode, node)
} else (type === VSTATELESS){
return updateStateLess(vnode, node)
}
updateVChildren(vnode, node)
}
// 更新class组件(调用render方法拿到新的虚拟DOM)
const updateComponent = (vnode, node) => {
const { type: Component } = vnode; // type是指虚拟DOM的类型
const newVNode = new Component().render();
compareTwoVnodes(newVNode, vnode, node);
}
// 更新无状态组件(直接执行函数拿到新的虚拟DOM)
const updateStateLess = (vnode, node) => {
const { type: Component } = vnode; // type是指虚拟DOM的类型
const newVNode = Component();
compareTwoVnodes(newVNode, vnode, node);
}
const updateVChildren = (vnode, node) => {
for (let i = 0; i < node.children.length; i++) {
updateVNode(vnode.children[i], node.children[i])
}
}
复制代码
所以,咱们这里以其中一层节点来说解diff是如何作到列表更新的。node
假设咱们的react组件渲染成功后,在浏览器中显示的真实DOM节点是A、B、C、D,咱们更新后的虚拟DOM是B、A、E、D。
那咱们这里须要作的操做就是,将原来DOM中已经存在的A、B、D进行更新,将原来DOM中存在,而如今不存的C移除掉,再建立新的D节点。
这样一来,问题就简化了不少,咱们只须要收集到须要create、remove和update的节点信息就好了。react
// oldDoms是真实DOM,newDoms是最新的虚拟DOM
const oldDoms = [A, B, C, D],
newDoms = [B, A, E, D],
updates = [],
removes = [],
creates = [];
// 进行两层遍历,获取到哪些节点须要更新,哪些节点须要移除。
for (let i = 0; i < oldDoms.length; i++) {
const oldDom = oldDoms[i]
let shouldRemove = true
for (let j = 0; j < newDoms.length; j++) {
const newDom = newDoms[j];
if (
oldDom.key === newDom.key &&
oldDom.type === newDom.type
) {
updates[j] = {
index: j,
node: oldDom,
parentNode: parentNode // 这里真实DOM的父节点
}
shouldRemove = false
}
}
if (shouldRemove) {
removes.push({
node: oldDom
})
}
}
// 从虚拟DOM节点来取出不要更新的节点,这就是须要新建立的节点。
for (let j = 0; j < newDoms.length; j++) {
if (!updates[j]) {
creates.push({
index: j,
vnode: newDoms[j],
parentNode: parentNode // 这里真实DOM的父节点
})
}
}
复制代码
这样,咱们便拿到了想要的状态信息。算法
在获得须要create、update和remove的节点后,咱们这时就能够开始进行渲染了。
数组
首先,咱们遍历全部须要remove的节点,将其从真实DOM中remove掉。所以这里须要remove掉C节点,最后渲染结果是A、B、D。浏览器
const remove = (removes) => {
removes.forEach(remove => {
const node = remove.node
node.parentNode.removeChild(node)
})
}
复制代码
其次,咱们再遍历须要更新的节点,将其插入到对应的位置中。因此这里最后渲染结果是B、A、D。bash
const update = (updates) => {
updates.forEach(update => {
const index = update.index,
parentNode = update.parentNode,
node = update.node,
curNode = parentNode.children[index];
if (curNode !== node) {
parentNode.insertBefore(node, curNode)
}
})
}
复制代码
最后一步,咱们须要建立新的DOM节点,并插入到正确的位置中,最后渲染结果为B、A、E、D。
const create = (creates) => {
creates.forEach(create => {
const index = create.index,
parentNode = create.parentNode,
vnode = create.vnode,
curNode = parentNode.children[index],
node = createNode(vnode); // 建立DOM节点
parentNode.insertBefore(node, curNode)
})
}
复制代码
虽然这篇文章写的比较简单,可是一个完整的diff流程就是这样了,能够加深对react的一些理解。