资讯专栏INFORMATION COLUMN

你不知道的Virtual DOM(四):key的作用

DirtyMind / 930人阅读

摘要:最后里面没有第四个元素了,才会把苹果从移除。四总结本文基于上一个版本的代码,加入了对唯一标识的支持,很好的提高了更新数组元素的效率。

欢迎关注我的公众号睿Talk,获取我最新的文章:

一、前言

目前最流行的两大前端框架,React和Vue,都不约而同的借助Virtual DOM技术提高页面的渲染效率。那么,什么是Virtual DOM?它是通过什么方式去提升页面渲染效率的呢?本系列文章会详细讲解Virtual DOM的创建过程,并实现一个简单的Diff算法来更新页面。本文的内容脱离于任何的前端框架,只讲最纯粹的Virtual DOM。敲单词太累了,下文Virtual DOM一律用VD表示。

这是VD系列文章的第四篇,以下是本系列其它文章的传送门:
你不知道的Virtual DOM(一):Virtual Dom介绍
你不知道的Virtual DOM(二):Virtual Dom的更新
你不知道的Virtual DOM(三):Virtual Dom更新优化
你不知道的Virtual DOM(四):key的作用
你不知道的Virtual DOM(五):自定义组件
你不知道的Virtual DOM(六):事件处理&异步更新

今天,我们继续在之前项目的基础上进行优化。用过React或者Vue的朋友都知道在渲染数组元素的时候,编译器会提醒加上key这个属性,那么key是用来做什么的呢?

二、key的作用

在渲染数组元素时,它们一般都有相同的结构,只是内容有些不同而已,比如:

</>复制代码

    • 商品:苹果
    • 数量:1
    • 商品:香蕉
    • 数量:2
    • 商品:雪梨
    • 数量:3

可以把这个例子想象成一个购物车。此时如果想往购物车里面添加一件商品,性能不会有任何问题,因为只是简单的在ul的末尾追加元素,前面的元素都不需要更新:

</>复制代码

    • 商品:苹果
    • 数量:1
    • 商品:香蕉
    • 数量:2
    • 商品:雪梨
    • 数量:3
    • 商品:橙子
    • 数量:2

但是,如果我要删除第一个元素,根据VD的比较逻辑,后面的元素全部都要进行更新的操作。dom结构简单还好说,如果是一个复杂的结构,那页面渲染的性能将会受到很大的影响。

</>复制代码

    • 商品:香蕉
    • 数量:2
    • 商品:雪梨
    • 数量:3
    • 商品:橙子
    • 数量:2

有什么方式可以降低这种性能的损耗呢?

最直观的方法肯定是直接删除第一个元素然后其它元素保持不变了。但程序没有这么智能,可以像我们一样一眼就看出变化。程序能做到的是尽量少的修改元素,通过移动元素而不是修改元素来达到更新的目的。为了告诉程序要怎么移动元素,我们必须给每个元素加上一个唯一标识,也就是key。

</>复制代码

    • 商品:苹果
    • 数量:1
    • 商品:香蕉
    • 数量:2
    • 商品:雪梨
    • 数量:3
    • 商品:橙子
    • 数量:2

当把苹果删掉的时候,VD里面第一个元素是香蕉,而dom里面第一个元素是苹果。当元素有key属性的时候,框架就会尝试根据这个key去找对应的元素,找到了就将这个元素移动到第一个位置,循环往复。最后VD里面没有第四个元素了,才会把苹果从dom移除。

三、代码实现

在上一个版本代码的基础上,主要的改动点是diffChildren这个函数。原来的实现很简单,递归的调用diff就可以了:

</>复制代码

  1. function diffChildren(newVDom, parent) {
  2. // 获取子元素最大长度
  3. const childLength = Math.max(parent.childNodes.length, newVDom.children.length);
  4. // 遍历并diff子元素
  5. for (let i = 0; i < childLength; i++) {
  6. diff(newVDom.children[i], parent, i);
  7. }
  8. }

现在,我们要对这个函数进行一个大改造,让他支持key的查找:

</>复制代码

  1. function diffChildren(newVDom, parent) {
  2. // 有key的子元素
  3. const nodesWithKey = {};
  4. let nodesWithKeyCount = 0;
  5. // 没key的子元素
  6. const nodesWithoutKey = [];
  7. let nodesWithoutKeyCount = 0;
  8. const childNodes = parent.childNodes,
  9. nodeLength = childNodes.length;
  10. const vChildren = newVDom.children,
  11. vLength = vChildren.length;
  12. // 用于优化没key子元素的数组遍历
  13. let min = 0;
  14. // 将子元素分成有key和没key两组
  15. for (let i = 0; i < nodeLength; i++) {
  16. const child = childNodes[i],
  17. props = child[ATTR_KEY];
  18. if (props !== undefined && props.key !== undefined) {
  19. nodesWithKey[props.key] = child;
  20. nodesWithKeyCount++;
  21. } else {
  22. nodesWithoutKey[nodesWithoutKeyCount++] = child;
  23. }
  24. }
  25. // 遍历vdom的所有子元素
  26. for (let i = 0; i < vLength; i++) {
  27. const vChild = vChildren[i],
  28. vProps = vChild.props;
  29. let dom;
  30. vKey = vProps!== undefined ? vProps.key : undefined;
  31. // 根据key来查找对应元素
  32. if (vKey !== undefined) {
  33. if (nodesWithKeyCount && nodesWithKey[vKey] !== undefined) {
  34. dom = nodesWithKey[vKey];
  35. nodesWithKey[vKey] = undefined;
  36. nodesWithKeyCount--;
  37. }
  38. }
  39. // 如果没有key字段,则找一个类型相同的元素出来做比较
  40. else if (min < nodesWithoutKeyCount) {
  41. for (let j = 0; j < nodesWithoutKeyCount; j++) {
  42. const node = nodesWithoutKey[j];
  43. if (node !== undefined && isSameType(node, vChild)) {
  44. dom = node;
  45. nodesWithoutKey[j] = undefined;
  46. if (j === min) min++;
  47. if (j === nodesWithoutKeyCount - 1) nodesWithoutKeyCount--;
  48. break;
  49. }
  50. }
  51. }
  52. // diff返回是否更新元素
  53. const isUpdate = diff(dom, vChild, parent);
  54. // 如果是更新元素,且不是同一个dom元素,则移动到原先的dom元素之前
  55. if (isUpdate) {
  56. const originChild = childNodes[i];
  57. if (originChild !== dom) {
  58. parent.insertBefore(dom, originChild);
  59. }
  60. }
  61. }
  62. // 清理剩下的未使用的dom元素
  63. if (nodesWithKeyCount) {
  64. for (key in nodesWithKey) {
  65. const node = nodesWithKey[key];
  66. if (node !== undefined) {
  67. node.parentNode.removeChild(node);
  68. }
  69. }
  70. }
  71. // 清理剩下的未使用的dom元素
  72. while (min <= nodesWithoutKeyCount) {
  73. const node = nodesWithoutKey[nodesWithoutKeyCount--];
  74. if ( node !== undefined) {
  75. node.parentNode.removeChild(node);
  76. }
  77. }
  78. }

代码比较长,主要是以下几个步骤:

将所有dom子元素分为有key和没key两组

遍历VD子元素,如果VD子元素有key,则去查找有key的分组;如果没key,则去没key的分组找一个类型相同的元素出来

diff一下,得出是否更新元素的类型

如果是更新元素且子元素不是原来的,则移动元素

最后清理删除没用上的dom子元素

diff也要改造一下,如果是新建、删除或者替换元素,返回false。更新元素则返回true:

</>复制代码

  1. function diff(dom, newVDom, parent) {
  2. // 新建node
  3. if (dom == undefined) {
  4. parent.appendChild(createElement(newVDom));
  5. return false;
  6. }
  7. // 删除node
  8. if (newVDom == undefined) {
  9. parent.removeChild(dom);
  10. return false;
  11. }
  12. // 替换node
  13. if (!isSameType(dom, newVDom)) {
  14. parent.replaceChild(createElement(newVDom), dom);
  15. return false;
  16. }
  17. // 更新node
  18. if (dom.nodeType === Node.ELEMENT_NODE) {
  19. // 比较props的变化
  20. diffProps(newVDom, dom);
  21. // 比较children的变化
  22. diffChildren(newVDom, dom);
  23. }
  24. return true;
  25. }

为了看效果,view函数也要改造下:

</>复制代码

  1. const arr = [0, 1, 2, 3, 4];
  2. function view() {
  3. const elm = arr.pop();
  4. // 用于测试能不能正常删除元素
  5. if (state.num !== 9) arr.unshift(elm);
  6. // 用于测试能不能正常添加元素
  7. if (state.num === 12) arr.push(9);
  8. return (
  9. Hello World
    • {
    • arr.map( i => (
    • 第{i}
    • ))
    • }
  10. );
  11. }

通过变换数组元素的顺序和适时的添加/删除元素,验证了代码按照我们的设计思路正确运行。

四、总结

本文基于上一个版本的代码,加入了对唯一标识(key)的支持,很好的提高了更新数组元素的效率。基于当前这个版本的代码还能做怎样的优化呢,请看下一篇的内容:你不知道的Virtual DOM(五):自定义组件。

P.S.: 想看完整代码见这里,如果有必要建一个仓库的话请留言给我:代码

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97155.html

相关文章

  • 你不知道Virtual DOM(三):Virtual Dom更新优化

    摘要:经过这次优化,计算的时间快了那么几毫秒。基于当前这个版本的代码还能做怎样的优化呢,请看下一篇的内容你不知道的四的作用。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 目前最流行的两大前端框架,React和Vue,都不约而同的借助Virtual DOM技术提高页面的渲染效率。那么,什...

    xiongzenghui 评论0 收藏0
  • 你不知道Virtual DOM(二):Virtual Dom更新

    摘要:变化的只有种更新和删除。页面的元素的数量随着而变。四总结本文详细介绍如何实现一个简单的算法,再根据计算出的差异去更新真实的。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 目前最流行的两大前端框架,React 和 Vue,都不约而同的借助 Virtual DOM 技术提高页面的渲染...

    testbird 评论0 收藏0
  • 你不知道Virtual DOM(一):Virtual Dom介绍

    摘要:不同的框架对这三个属性的命名会有点差别,但表达的意思是一致的。它们分别是标签名属性和子元素对象。我们先来看下页面的更新一般会经过几个阶段。元素有可能是数组的形式,需要将数组解构一层。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 目前最流行的两大前端框架,React和Vue,都不约...

    lavor 评论0 收藏0
  • 你不知道Virtual DOM(六):事件处理&异步更新

    摘要:如果列表是空的,则存入组件后将异步刷新任务加入到事件循环当中。四总结本文基于上一个版本的代码,加入了事件处理功能,同时通过异步刷新的方法提高了渲染效率。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、前言 目前最流行的两大前端框架,React和Vue,都不约而同的借助Virtual DO...

    caozhijian 评论0 收藏0
  • 你不知道Virtual DOM(五):自定义组件

    摘要:现在流行的前端框架都支持自定义组件,组件化开发已经成为提高前端开发效率的银弹。二对自定义组件的支持要想正确的渲染组件,第一步就是要告诉某个标签是自定义组件。下面的例子里,就是一个自定义组件。解决了识别自定义标签的问题,下一步就是定义标签了。 欢迎关注我的公众号睿Talk,获取我最新的文章:showImg(https://segmentfault.com/img/bVbmYjo); 一、...

    lk20150415 评论0 收藏0

发表评论

0条评论

DirtyMind

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<