preact源码学习(3)

前端之家收集整理的这篇文章主要介绍了preact源码学习(3)前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

这是说preact的diff机制。preact在diff的过程中创建,更新与移除真实DOM。diff机制是preact中最难懂的部分。

我们先看render方法

  1. //render.js
  2. import { diff } from './vdom/diff';
  3.  
  4. export function render(vnode,parent,merge) {
  5. return diff(merge,vnode,{},false,false);
  6. }

vnode为虚拟DOM,parent为作为容器的元素节点,merge是另一个真实DOM,但也可能不存在。从这个render方法,我们可以看到,它与官方React出入比较大,因为官方react的render第三个参数是回调。

  1. //用于收集那些等待被调用componentDidMount回调的组件
  2. export const mounts = [];
  3.  
  4. //判定递归的层次
  5. export let diffLevel = 0;
  6. //判定当前的DOM树是否为SVG
  7. let isSvgMode = false;
  8.  
  9. //判定这个元素是否已经缓存了之前的虚拟DOM数据
  10. let hydrating = false;
  11. //批量触发componentDidMount与afterMount
  12. export function flushMounts() {
  13. let c;
  14. while ((c=mounts.pop())) {
  15. if (options.afterMount) options.afterMount(c);
  16. if (c.componentDidMount) c.componentDidMount();
  17. }
  18. }
  19.  
  20. export function diff(dom,context,mountAll,componentRoot) {
  21. if (!diffLevel++) {
  22. //重新判定DOM树的类型
  23. isSvgMode = parent!=null && parent.ownerSVGElement!==undefined;
  24.  
  25. // 判定是否缓存了数据
  26. hydrating = dom!=null && !(ATTR_KEY in dom);
  27. }
  28. //更新dom 或返回新的dom
  29. let ret = idiff(dom,componentRoot);
  30.  
  31. // 插入父节点
  32. if (parent && ret.parentNode!==parent) parent.appendChild(ret);
  33.  
  34. if (!--diffLevel) {
  35. hydrating = false;
  36. // 执行所有DidMount钩子
  37. if (!componentRoot) flushMounts();
  38. }
  39.  
  40. return ret;
  41. }

用户一般的使用来看,传到diff里面的参数一般是

  1. diff(undefined,false);

它的参数严重不足,我们再看idiff。

  1. function idiff(dom,componentRoot) {
  2. let out = dom,prevSvgMode = isSvgMode;
  3.  
  4. // 转换null,undefined,boolean为空字符
  5. if (vnode==null || typeof vnode==='boolean') vnode = '';
  6. //将字符串与数字转换为文本节点
  7. if (typeof vnode==='string' || typeof vnode==='number') {
  8.  
  9. // 如果已经存在,注意在IE6-8下,文本节点是不能添加自定义属性,因此dom._component总是为undefined
  10. if (dom && dom.splitText!==undefined && dom.parentNode && (!dom._component || componentRoot)) {
  11. if (dom.nodeValue!=vnode) {
  12. dom.nodeValue = vnode;
  13. }
  14. }
  15. else {
  16. // 创建新的虚拟DOM
  17. out = document.createTextNode(vnode);
  18. if (dom) {
  19. if (dom.parentNode) dom.parentNode.replaceChild(out,dom);
  20. recollectNodeTree(dom,true);
  21. }
  22. }
  23.  
  24. out[ATTR_KEY] = true;
  25.  
  26. return out;
  27. }
  28.  
  29.  
  30. // 如果是组件
  31. let vnodeName = vnode.nodeName;
  32. if (typeof vnodeName==='function') {
  33. return buildComponentFromVNode(dom,mountAll);
  34. }
  35.  
  36.  
  37. // 更新isSvgMode
  38. isSvgMode = vnodeName==='svg' ? true : vnodeName==='foreignObject' ? false : isSvgMode;
  39.  
  40.  
  41. //这个应该是防御性代码,因为到这里都是div,p,span这样的标签
  42. vnodeName = String(vnodeName);
  43. //如果没有DOM,或标签类型不一致
  44. if (!dom || !isNamedNode(dom,vnodeName)) {
  45. out = createNode(vnodeName,isSvgMode);
  46.  
  47. if (dom) {
  48. // 转移里面的真实DOM
  49. while (dom.firstChild) out.appendChild(dom.firstChild);
  50.  
  51. // 插入到父节点
  52. if (dom.parentNode) dom.parentNode.replaceChild(out,dom);
  53.  
  54. // GC
  55. recollectNodeTree(dom,true);
  56. }
  57. }
  58.  
  59.  
  60. let fc = out.firstChild,//取得之前的虚拟DOM的props
  61. props = out[ATTR_KEY],vchildren = vnode.children;
  62.  
  63. if (props==null) {
  64. //将元素节点的attributes转换为props,方便进行比较
  65. //不过这里有一个致命的缺憾在IE6-7中,因为IE6-7不区分attributes与property,这里会存在大量的属性,导致巨耗性能
  66. props = out[ATTR_KEY] = {};
  67. for (let a=out.attributes,i=a.length; i--; ) props[a[i].name] = a[i].value;
  68. }
  69.  
  70. // Optimization: fast-path for elements containing a single TextNode:
  71. // 如果当前位置的真实DOM 是文本节点,并没有缓存任何数据,而虚拟DOM 则是一个字符串,那么直接修改nodeValue
  72. if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && fc!=null && fc.splitText!==undefined && fc.nextSibling==null) {
  73. if (fc.nodeValue!=vchildren[0]) {
  74. fc.nodeValue = vchildren[0];
  75. }
  76. }
  77. //更新这个真实DOM 的孩子
  78. else if (vchildren && vchildren.length || fc!=null) {
  79. innerDiffNode(out,vchildren,hydrating || props.dangerouslySetInnerHTML!=null);
  80. }
  81.  
  82.  
  83. // 更新这个真实DOM 的属性
  84. diffAttributes(out,vnode.attributes,props);
  85.  
  86.  
  87. // 还原isSvgMode
  88. isSvgMode = prevSvgMode;
  89.  
  90. return out;
  91. }

idiff的逻辑可分成这几步

  1. 保存现有的文档为型
  2. 更新或创建文本节点
  3. 更新或创建组件对应的真实DOM
  4. 更新普通元素节点
  5. 收集元素当前的真实属性
  6. 更新元素的内部(孩子)
  7. diff元素的属性
  8. 还原之前的文档类型

可以看作是对当个元素的diff实现。

而更外围的diff方法,主要通过diffLevel这个变量,控制所有插入组件的DidMount钩子的调用

idiff内部有一个叫innerDiffNode方法,如果是我作主,我更愿意命名为diffChildren.

innerDiffNode方法是非常长,好像每次我阅读它,它都变长一点。一点点猴子补丁往上加,完全不考虑用设计模式对它进行拆分。

  1. function innerDiffNode(dom,isHydrating) {
  2. let originalChildren = dom.childNodes,children = [],keyed = {},keyedLen = 0,min = 0,len = originalChildren.length,childrenLen = 0,vlen = vchildren ? vchildren.length : 0,j,c,f,vchild,child;
  3.  
  4. // 如果真实DOM 存在孩子,可以进行diff,这时要收集设置到key属性的孩子到keyed对象,剩余的则放在children数组中
  5. if (len!==0) {
  6. for (let i=0; i<len; i++) {
  7. let child = originalChildren[i],props = child[ATTR_KEY],key = vlen && props ? child._component ? child._component.__key : props.key : null;
  8. if (key!=null) {
  9. keyedLen++;
  10. keyed[key] = child;
  11. }
  12. else if (props || (child.splitText!==undefined ? (isHydrating ? child.nodeValue.trim() : true) : isHydrating)) {
  13. children[childrenLen++] = child;
  14. }
  15. }
  16. }
  17.  
  18. if (vlen!==0) {
  19. //遍历当前虚拟DOM children
  20. for (let i=0; i<vlen; i++) {
  21. vchild = vchildren[i];
  22. child = null;
  23.  
  24. // 先尝试根据key来寻找已有的DOM
  25. let key = vchild.key;
  26. if (key!=null) {
  27. if (keyedLen && keyed[key]!==undefined) {
  28. child = keyed[key];
  29. keyed[key] = undefined;
  30. keyedLen--;
  31. }
  32. }
  33. // 如果没有key,那么就根据nodeName来寻找最近的那个节点
  34. else if (!child && min<childrenLen) {
  35. for (j=min; j<childrenLen; j++) {
  36. if (children[j]!==undefined && isSameNodeType(c = children[j],isHydrating)) {
  37. child = c;
  38. children[j] = undefined;
  39. if (j===childrenLen-1) childrenLen--;
  40. if (j===min) min++;
  41. break;
  42. }
  43. }
  44. }
  45.  
  46. // 更新它的孩子与属性
  47. child = idiff(child,mountAll);
  48.  
  49. f = originalChildren[i];
  50. if (child && child!==dom && child!==f) {
  51. //各种形式的插入DOM树
  52. if (f==null) {
  53. dom.appendChild(child);
  54. }
  55. else if (child===f.nextSibling) {
  56. removeNode(f);
  57. }
  58. else {
  59. dom.insertBefore(child,f);
  60. }
  61. }
  62. }
  63. }
  64.  
  65.  
  66. // GC
  67. if (keyedLen) {
  68. for (let i in keyed) if (keyed[i]!==undefined) recollectNodeTree(keyed[i],false);
  69. }
  70.  
  71. // GC
  72. while (min<=childrenLen) {
  73. if ((child = children[childrenLen--])!==undefined) recollectNodeTree(child,false);
  74. }
  75. }
  76.  
  77.  
  78. export function isSameNodeType(node,hydrating) {
  79. if (typeof vnode==='string' || typeof vnode==='number') {
  80. //文本节点与字符串,文本节点是对等的,但我不明白为什么不用nodeType === 3来判定文本节点
  81. return node.splitText!==undefined;
  82. }
  83. if (typeof vnode.nodeName==='string') {
  84. return !node._componentConstructor && isNamedNode(node,vnode.nodeName);
  85. }
  86. return hydrating || node._componentConstructor===vnode.nodeName;
  87. }

innerDiffNode方法在创建keyed对象中其实存在巨大的缺憾,它无法阻止用户在同一组孩子 使用两个相同的key的情况,因此会出错。而官方react,其实还结合父节点的深度,因此可以规避。

比如下面的JSX ,preact在diff时就会出错:

  1. <div>{[1,2,3].map((el,index)=>{ <span key={"x"+index}>{el}</span> })}xxx
  2. {[4,5,6].map((el,index)=>{ <span key={"x"+index}>{el}</span> })}
  3. </div>

这里我们比较一下官方react与preact的diff差异。官方react是有两组虚拟DOM 树在diff,diff完毕再将差异点应用于真实DOM 中。在preact则是先从真实DOM树中还原出之前的虚拟DOM出来,然后新旧vtree进行边diff边patch的操作。

之于怎么还原呢,利用缓存数据与nodeValue!

真实DOM 拥有_component对象的元素节点 拥有ATTR_KET对象的元素节点 拥有ATTR_KET布尔值的文本节点
对应的prevVNode 组件虚拟DOM 元素虚拟DOM 简单类型的虚拟DOM

这种深度耦合DOM 树的实现的优缺点都很明显,好处是它总是最真实地反映之前的虚拟DOM树的情况,diff时少传参,坏处是需要做好内存泄露的工作。

猜你在找的React相关文章