[关闭]
@gyyin 2020-02-06T15:07:33.000000Z 字数 3383 阅读 333

浅谈react diff算法

React


这是一篇硬核文,因此不会用生动幽默的语言来讲述,这篇文章大概更是自己心血来潮的总结吧哈哈哈哈。
有很多文章讲过react的diff算法,但要么是晦涩难懂的源码分析,让人很难读进去,要么就是流于表面的简单讲解,实际上大家看完后还是一头雾水,因此我将react-lite中的diff算法实现稍微整理了一下,希望能够帮助大家解惑。
对于react diff,我们已知的有两点,一个是会通过key来做比较,另一个是react默认是同级节点做diff,不会考虑到跨层级节点的diff(事实是前端开发中很少有DOM节点跨层级移动的)。
image_1da3op2k3207vppgli1ff91a749.png-56.8kB

递归更新

首先,抛给我们一个问题,那就是react怎么对那么深层次的DOM做的diff?实际上react是对DOM进行递归来做的,遍历所有子节点,对子节点再做递归。

  1. // 超简单代码实现
  2. const compareTwoVnodes(oldVnode, newVnode, dom) {
  3. let newNode = dom
  4. // 如果新的虚拟DOM是null,那么就将前一次的真实DOM移除掉
  5. if (newVnode == null) {
  6. destroyVnode(oldVnode, dom)
  7. dom.parentNode.removeChild(dom)
  8. } else if (oldVnode.type !== newVnode.type || oldVnode.key !== newVnode.key) {
  9. // replace
  10. destroyVnode(oldVnode, dom)
  11. newNode = initVnode(newVnode, parentContext, dom.namespaceURI)
  12. dom.parentNode.replaceChild(newNode, dom)
  13. } else if (oldVnode !== newVnode || parentContext) {
  14. // same type and same key -> update
  15. newNode = updateVNode(oldVnode, newVnode, dom, parentContext)
  16. }
  17. }
  18. /**
  19. * 更新虚拟DOM
  20. * 这里的type需要注意一下,如果vnode是个html元素,例如h1,那么type就是'h1'
  21. ** 如果vnode是一个函数组件,例如const Header = () => <h1>header</h1>,那么type就是函数Header
  22. ** 如果vnode是一个class组件,那么type就是那个class
  23. */
  24. const updateVNode = (vnode, node) => {
  25. const { type } = vnode; // type是指虚拟DOM的类型
  26. // 如果是class组件
  27. if (type === VCOMPONENT) {
  28. return updateComponent(vnode, node)
  29. } else (type === VSTATELESS){
  30. return updateStateLess(vnode, node)
  31. }
  32. updateVChildren(vnode, node)
  33. }
  34. // 更新class组件(调用render方法拿到新的虚拟DOM)
  35. const updateComponent = (vnode, node) => {
  36. const { type: Component } = vnode; // type是指虚拟DOM的类型
  37. const newVNode = new Component().render();
  38. compareTwoVnodes(newVNode, vnode, node);
  39. }
  40. // 更新无状态组件(直接执行函数拿到新的虚拟DOM)
  41. const updateStateLess = (vnode, node) => {
  42. const { type: Component } = vnode; // type是指虚拟DOM的类型
  43. const newVNode = Component();
  44. compareTwoVnodes(newVNode, vnode, node);
  45. }
  46. const updateVChildren = (vnode, node) => {
  47. for (let i = 0; i < node.children.length; i++) {
  48. updateVNode(vnode.children[i], node.children[i])
  49. }
  50. }

因此,我们这里以其中一层节点来讲解diff是如何做到列表的更新。

状态收集

假设我们的react组件渲染成功后,在浏览器中显示的真实DOM节点是A、B、C、D,我们更新后的虚拟DOM是B、A、E、D。
那我们这里需要做的操作就是,将原来DOM中已经存在的A、B、D进行更新,将原来DOM中存在,而现在不存的C移除掉,再创建新的D节点。
这样一来,问题就简化了很多,我们只需要收集到需要create、remove和update的节点信息就行了。

diff

  1. // oldDoms是真实DOM,newDoms是最新的虚拟DOM
  2. const oldDoms = [A, B, C, D],
  3. newDoms = [B, A, E, D],
  4. updates = [],
  5. removes = [],
  6. creates = [];
  7. // 进行两层遍历,获取到哪些节点需要更新,哪些节点需要移除。
  8. for (let i = 0; i < oldDoms.length; i++) {
  9. const oldDom = oldDoms[i]
  10. let shouldRemove = true
  11. for (let j = 0; j < newDoms.length; j++) {
  12. const newDom = newDoms[j];
  13. if (
  14. oldDom.key === newDom.key &&
  15. oldDom.type === newDom.type
  16. ) {
  17. updates[j] = {
  18. index: j,
  19. node: oldDom,
  20. parentNode: parentNode // 这里真实DOM的父节点
  21. }
  22. shouldRemove = false
  23. }
  24. }
  25. if (shouldRemove) {
  26. removes.push({
  27. node: oldDom
  28. })
  29. }
  30. }
  31. // 从虚拟DOM节点来取出不要更新的节点,这就是需要新创建的节点。
  32. for (let j = 0; j < newDoms.length; j++) {
  33. if (!updates[j]) {
  34. creates.push({
  35. index: j,
  36. vnode: newDoms[j],
  37. parentNode: parentNode // 这里真实DOM的父节点
  38. })
  39. }
  40. }

这样,我们便拿到了想要的状态信息。

diff

在得到需要create、update和remove的节点后,我们这时就可以开始进行渲染了。
状态

首先,我们遍历所有需要remove的节点,将其从真实DOM中remove掉。因此这里需要remove掉C节点,最后渲染结果是A、B、D。

  1. const remove = (removes) => {
  2. removes.forEach(remove => {
  3. const node = remove.node
  4. node.parentNode.removeChild(node)
  5. })
  6. }

其次,我们再遍历需要更新的节点,将其插入到对应的位置中。所以这里最后渲染结果是B、A、D。

  1. const update = (updates) => {
  2. updates.forEach(update => {
  3. const index = update.index,
  4. parentNode = update.parentNode,
  5. node = update.node,
  6. curNode = parentNode.children[index];
  7. if (curNode !== node) {
  8. parentNode.insertBefore(node, curNode)
  9. }
  10. })
  11. }

最后一步,我们需要创建新的DOM节点,并插入到正确的位置中,最后渲染结果为B、A、E、D。

  1. const create = (creates) => {
  2. creates.forEach(create => {
  3. const index = create.index,
  4. parentNode = create.parentNode,
  5. vnode = create.vnode,
  6. curNode = parentNode.children[index],
  7. node = createNode(vnode); // 创建DOM节点
  8. parentNode.insertBefore(node, curNode)
  9. })
  10. }

虽然这篇文章写的比较简单,但是一个完整的diff流程就是这样了,可以加深对react的一些理解。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注