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

我们先看render方法。

//render.js
import { diff } from './vdom/diff';export function render(vnode, parent, merge) {return diff(merge, vnode, {}, false, parent, false);
}

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

//用于收集那些等待被调用componentDidMount回调的组件
export const mounts = [];//判定递归的层次
export let diffLevel = 0;
//判定当前的DOM树是否为SVG
let isSvgMode = false;//判定这个元素是否已经缓存了之前的虚拟DOM数据
let hydrating = false;
//批量触发componentDidMount与afterMount
export function flushMounts() {let c;while ((c=mounts.pop())) {if (options.afterMount) options.afterMount(c);if (c.componentDidMount) c.componentDidMount();}
}export function diff(dom, vnode, context, mountAll, parent, componentRoot) {if (!diffLevel++) {//重新判定DOM树的类型isSvgMode = parent!=null && parent.ownerSVGElement!==undefined;// 判定是否缓存了数据hydrating = dom!=null && !(ATTR_KEY in dom);}//更新dom 或返回新的domlet ret = idiff(dom, vnode, context, mountAll, componentRoot);// 插入父节点if (parent && ret.parentNode!==parent) parent.appendChild(ret);if (!--diffLevel) {hydrating = false;// 执行所有DidMount钩子if (!componentRoot) flushMounts();}return ret;
}

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

diff(undefined, vnode, {}, false, parent, false);

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

function idiff(dom, vnode, context, mountAll, componentRoot) {let out = dom,prevSvgMode = isSvgMode;// 转换null, undefined, boolean为空字符if (vnode==null || typeof vnode==='boolean') vnode = '';//将字符串与数字转换为文本节点if (typeof vnode==='string' || typeof vnode==='number') {// 如果已经存在,注意在IE6-8下,文本节点是不能添加自定义属性,因此dom._component总是为undefinedif (dom && dom.splitText!==undefined && dom.parentNode && (!dom._component || componentRoot)) {if (dom.nodeValue!=vnode) {dom.nodeValue = vnode;}}else {// 创建新的虚拟DOMout = document.createTextNode(vnode);if (dom) {if (dom.parentNode) dom.parentNode.replaceChild(out, dom);recollectNodeTree(dom, true);}}out[ATTR_KEY] = true;return out;}// 如果是组件let vnodeName = vnode.nodeName;if (typeof vnodeName==='function') {return buildComponentFromVNode(dom, vnode, context, mountAll);}// 更新isSvgModeisSvgMode = vnodeName==='svg' ? true : vnodeName==='foreignObject' ? false : isSvgMode;//这个应该是防御性代码,因为到这里都是div, p, span这样的标签名vnodeName = String(vnodeName);//如果没有DOM,或标签类型不一致if (!dom || !isNamedNode(dom, vnodeName)) {out = createNode(vnodeName, isSvgMode);if (dom) {// 转移里面的真实DOMwhile (dom.firstChild) out.appendChild(dom.firstChild);// 插入到父节点if (dom.parentNode) dom.parentNode.replaceChild(out, dom);// GCrecollectNodeTree(dom, true);}}let fc = out.firstChild,//取得之前的虚拟DOM的propsprops = out[ATTR_KEY],vchildren = vnode.children;if (props==null) {//将元素节点的attributes转换为props,方便进行比较//不过这里有一个致命的缺憾在IE6-7中,因为IE6-7不区分attributes与property,这里会存在大量的属性,导致巨耗性能props = out[ATTR_KEY] = {};for (let a=out.attributes, i=a.length; i--; ) props[a[i].name] = a[i].value;}// Optimization: fast-path for elements containing a single TextNode:// 如果当前位置的真实DOM 是文本节点,并没有缓存任何数据,而虚拟DOM 则是一个字符串,那么直接修改nodeValueif (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && fc!=null && fc.splitText!==undefined && fc.nextSibling==null) {if (fc.nodeValue!=vchildren[0]) {fc.nodeValue = vchildren[0];}}//更新这个真实DOM 的孩子else if (vchildren && vchildren.length || fc!=null) {innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML!=null);}// 更新这个真实DOM 的属性diffAttributes(out, vnode.attributes, props);// 还原isSvgModeisSvgMode = prevSvgMode;return out;
}

idiff的逻辑可分成这几步

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

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

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

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

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

function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) {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;// 如果真实DOM 存在孩子,可以进行diff,这时要收集设置到key属性的孩子到keyed对象,剩余的则放在children数组中if (len!==0) {for (let i=0; i<len; i++) {let child = originalChildren[i],props = child[ATTR_KEY],key = vlen && props ? child._component ? child._component.__key : props.key : null;if (key!=null) {keyedLen++;keyed[key] = child;}else if (props || (child.splitText!==undefined ? (isHydrating ? child.nodeValue.trim() : true) : isHydrating)) {children[childrenLen++] = child;}}}if (vlen!==0) {
//遍历当前虚拟DOM childrenfor (let i=0; i<vlen; i++) {vchild = vchildren[i];child = null;// 先尝试根据key来寻找已有的DOMlet key = vchild.key;if (key!=null) {if (keyedLen && keyed[key]!==undefined) {child = keyed[key];keyed[key] = undefined;keyedLen--;}}// 如果没有key ,那么就根据nodeName来寻找最近的那个节点else if (!child && min<childrenLen) {for (j=min; j<childrenLen; j++) {if (children[j]!==undefined && isSameNodeType(c = children[j], vchild, isHydrating)) {child = c;children[j] = undefined;if (j===childrenLen-1) childrenLen--;if (j===min) min++;break;}}}// 更新它的孩子与属性child = idiff(child, vchild, context, mountAll);f = originalChildren[i];if (child && child!==dom && child!==f) {//各种形式的插入DOM树if (f==null) {dom.appendChild(child);}else if (child===f.nextSibling) {removeNode(f);}else {dom.insertBefore(child, f);}}}}// GCif (keyedLen) {for (let i in keyed) if (keyed[i]!==undefined) recollectNodeTree(keyed[i], false);}// GCwhile (min<=childrenLen) {if ((child = children[childrenLen--])!==undefined) recollectNodeTree(child, false);}
}export function isSameNodeType(node, vnode, hydrating) {if (typeof vnode==='string' || typeof vnode==='number') {
//文本节点与字符串,文本节点是对等的,但我不明白为什么不用nodeType === 3来判定文本节点return node.splitText!==undefined;}if (typeof vnode.nodeName==='string') {return !node._componentConstructor && isNamedNode(node, vnode.nodeName);}return hydrating || node._componentConstructor===vnode.nodeName;
}

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

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

<div>{[1,2,3].map((el,index)=>{ <span key={"x"+index}>{el}</span>  })}xxx
{[4,5,6].map((el,index)=>{ <span key={"x"+index}>{el}</span>  })}
</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时少传参,坏处是需要做好内存泄露的工作。

preact源码学习(3)相关推荐

  1. preact源码学习(2)

    本节我们看如何更新组件.在上一节也反复提到renderComponent这个方法了,这节直接从它入手吧.它位于src/vdom/component.js文件中. 从参数来看,我们会惊讶它竟然会有这么多 ...

  2. 深入Preact源码分析(4.20更新)

    React的源码多达几万行,对于我们想要快速阅读并看懂是相当有难度的,而Preact是一个轻量级的类react库,几千行代码就实现了react的大部分功能.因此阅读preact源码,对于我们学习rea ...

  3. preact源码分析

    前言 前两个星期花了一些时间学习preact的源码, 并写了几篇博客.但是现在回头看看写的并不好,而且源码的有些地方(diffChildren的部分)我还理解?错了.实在是不好意思.所以这次准备重新写 ...

  4. 从 Preact 源码一窥 React 原理(一):JSX 渲染

    从 Preact 源码一窥 React 原理(一):JSX 渲染 前言 JSX 渲染 VNode createElement 函数 coerceToVNode 函数 总结 参考资料 系列文章: 从 P ...

  5. Shiro源码学习之二

    接上一篇 Shiro源码学习之一 3.subject.login 进入login public void login(AuthenticationToken token) throws Authent ...

  6. Shiro源码学习之一

    一.最基本的使用 1.Maven依赖 <dependency><groupId>org.apache.shiro</groupId><artifactId&g ...

  7. mutations vuex 调用_Vuex源码学习(六)action和mutation如何被调用的(前置准备篇)...

    前言 Vuex源码系列不知不觉已经到了第六篇.前置的五篇分别如下: 长篇连载:Vuex源码学习(一)功能梳理 长篇连载:Vuex源码学习(二)脉络梳理 作为一个Web前端,你知道Vuex的instal ...

  8. vue实例没有挂载到html上,vue 源码学习 - 实例挂载

    前言 在学习vue源码之前需要先了解源码目录设计(了解各个模块的功能)丶Flow语法. src ├── compiler # 把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能. ├── ...

  9. 2021-03-19Tomcat源码学习--WebAppClassLoader类加载机制

    Tomcat源码学习--WebAppClassLoader类加载机制 在WebappClassLoaderBase中重写了ClassLoader的loadClass方法,在这个实现方法中我们可以一窥t ...

最新文章

  1. Ansible — Overview
  2. “李子柒”们的商业模式,到底需不需要微念
  3. Flume sink=avro rpc connection error
  4. linux内核那些事之E820
  5. 代码统计工具有哪几种_DevOps:优秀代码分析工具的自我修养
  6. Rtsp之rtp包解析
  7. spring 容器的理论知识
  8. HNUCM-1322:调酒壶里的酸奶
  9. CVPR 2021 UniT: Multimodal Multitask Learning with a Unifified Transformer
  10. MVP简单使用+RecyclerView
  11. H5 在iPhone真机上调试H5页面
  12. OpenWRT配置 -- 网络配置network文件
  13. java爬取前程无忧(51job)
  14. 统计一个字符串中单词的个数(C语言)
  15. inveta PLSB 点线面体 示例工程
  16. 如何看待 Java 大厂 P6+ 这一岗位能力要求?
  17. 人脸识别行业应用状况及发展前景模式分析报告
  18. 0.2度背后的美国抉择:压不住新能源
  19. 《管理学》计划及其制订-学习笔记
  20. 计算机软件应用职业规划,计算机软件专业的职业生涯规划

热门文章

  1. 关于Timestamp的valueOf()方法
  2. 【Spark】sparksql中使用自定义函数
  3. 【Oracle】恢复重做日志组
  4. Unity中UI界面颤抖解决方法
  5. ERROR org.hibernate.hql.internal.ast.ErrorCounter unexpected token: form 异常解决
  6. C# WebApi Xml序列化问题解决方法:“ObjectContent`1”类型未能序列化内容类型“application/xml;charset=utf-8“的响应正文。...
  7. Struts2中的ModelDriven机制及其运用、refreshModelBeforeResult属性解决的问题
  8. 解决安装mysql动态库libstdc++.so.6、libc.so.6版本过低问题
  9. Oracle数据库中文乱码问题解决
  10. class path resource [logback.xml] cannot be resolved to URL because it does not exist 问题解决