资讯专栏INFORMATION COLUMN

帮你读懂preact的源码(一)

XboxYan / 1695人阅读

摘要:是一个最小的库,但由于其对尺寸的追求,它的很多代码可读性比较差,市面上也很少有全面且详细介绍的文章,本篇文章希望能帮助你学习的源码。建议与源码一起阅读本文。

作为一名前端,我们需要深入学习react的运行机制,但是react源码量已经相当庞大,从学习的角度,性价比不高,所以学习一个react mini库是一个深入学习react的一个不错的方法。

preact是一个最小的react mini库,但由于其对尺寸的追求,它的很多代码可读性比较差,市面上也很少有全面且详细介绍的文章,本篇文章希望能帮助你学习preact的源码。

在最开始我会先介绍preact整体流程,帮助您有一个整体概念,以便不会陷入源码的细枝末节里,然后会分别讲解preact各个值得学习的机制。建议与preact源码一起阅读本文。

希望能帮你理清如下问题:

JSX是怎么被处理的?

diff算法是如何工作的?

vue和react中我们为什么需要一个稳定的key?

preact是怎么处理事件的?

preact的回收机制是如何提高性能的?

setState之后会发生什么?

fiber是用来解决什么问题的?

以下图是preact源码大致流程图,现在看不懂没关系,也不需要刻意记,在学习的过程中,不妨根据此图试着猜想preact每一步都做了什么,下一步要做什么。

JSX

在react的官方文档中,我们可以得知,jsx内容在会被babel编译为以下格式:

</>复制代码

  1. In
  2. const element = (
  3. Hello, world!
  4. );
  5. Out
  6. const element = React.createElement(
  7. "h1",
  8. {className: "greeting"},
  9. "Hello, world!"
  10. );

这样通过createElement就可以生成虚拟dom树,在preact里面对应的函数是h。
h函数根据nodeName,attributes,children,返回一个虚拟dom树,这个虚拟dom树往往有三个属性:

</>复制代码

  1. function h(nodeName, props, ...children){
  2. .... // 其他代码
  3. return {
  4. nodeName,
  5. props, // props中包含children
  6. key, // 为diff算法做准备
  7. }
  8. }

这里不贴出preact的源代码,因为h函数的实现方式有很多,不希望最开始的学习就陷入到细枝末节,只需要明白h函数的作用即可。

diff

从上图中可以看到,preact主流程调用的第一个函数就是render,render函数很简单就是调用了一下diff函数。

</>复制代码

  1. function render(vnode, parent, merge) {
  2. return diff(merge, vnode, {}, false, parent, false);
  3. }

diff函数的主要作用是调用idiff函数,然后将idff函数返回的真实dom append到dom中

</>复制代码

  1. function diff(dom, vnode, context, mountAll, parent, componentRoot) {
  2. // 返回的是一个真实的dom节点
  3. let ret = idiff(dom, vnode, context, mountAll, componentRoot);
  4. // append the element if its a new parent
  5. if (parent && ret.parentNode !== parent) parent.appendChild(ret);
  6. }
idiff

接下来我们要介绍idff函数,开启react高性能diff算法的大门,但在这之前,我们应该了解react diff算法的前提:

两个不同类型的element会产生不同类型的树。

开发者通过一个key标识同一层级的子节点。

基于第一个前提,不同类型的节点就可以不再向下比较,直接销毁,然后重新创建即可。

idiff函数主要分为三块,分别处理vnode三种情况:

vnode是string或者Number,类似于上面例子的"Hello World",一般是虚拟dom树的叶子节点。

vnode中的nodeName是一个function,即vnode对应一个组件,例如上例中的

vnode中nodeName是一个字符串,即vnode对应一个html元素,例如上例中的h1。

对于string或Number:

</>复制代码

  1. // 如果要比较的dom是一个textNode,直接更改dom的nodeValue
  2. // 如果要比较的dom不是一个textNode,就创建textNode,然后回收老的节点树,回收的节点树会保留结构,然后保存在内存中,在// 需要的时候复用。(回收相关的处理会在之后详细说明)
  3. if (typeof vnode === "string" || typeof vnode === "number") {
  4. // update if it"s already a Text node:
  5. if (dom && dom.splitText !== undefined && dom.parentNode && (!dom._component || componentRoot)) {
  6. /* istanbul ignore if */
  7. /* Browser quirk that can"t be covered: https://github.com/developit/preact/commit/fd4f21f5c45dfd75151bd27b4c217d8003aa5eb9 */
  8. if (dom.nodeValue != vnode) {
  9. dom.nodeValue = vnode;
  10. }
  11. } else {
  12. // it wasn"t a Text node: replace it with one and recycle the old Element
  13. out = document.createTextNode(vnode);
  14. if (dom) {
  15. if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
  16. recollectNodeTree(dom, true);
  17. }
  18. }
  19. out.__preactattr_ = true;
  20. return out;
  21. }

如果nodeName是一个function,会直接调用buildComponentFromVNode方法

</>复制代码

  1. let vnodeName = vnode.nodeName;
  2. if (typeof vnodeName === "function") {
  3. return buildComponentFromVNode(dom, vnode, context, mountAll);
  4. }

如果nodeName是一个字符串,以下很长的代码,就是做三步:

对于类型不同的节点,直接做替换操作,不做diff比较。

diffAttrites

diffChildren

</>复制代码

  1. // Tracks entering and exiting SVG namespace when descending through the tree.
  2. isSvgMode = vnodeName === "svg" ? true : vnodeName === "foreignObject" ? false : isSvgMode;
  3. // If there"s no existing element or it"s the wrong type, create a new one:
  4. vnodeName = String(vnodeName);
  5. // 如果不存在dom对象,或者dom的nodeName和vnodeName不一样的情况下
  6. if (!dom || !isNamedNode(dom, vnodeName)) {
  7. out = createNode(vnodeName, isSvgMode);
  8. if (dom) {
  9. // 在后面你会发现preact的diffChildren的方式,是通过把真实dom的子节点与虚拟dom的子节点相比较,所以需要老的// 孩子暂时先移动到新的节点上
  10. // move children into the replacement node
  11. while (dom.firstChild) {
  12. out.appendChild(dom.firstChild);
  13. } // if the previous Element was mounted into the DOM, replace it inline
  14. if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
  15. // recycle the old element (skips non-Element node types)
  16. recollectNodeTree(dom, true);
  17. }
  18. }
  19. let fc = out.firstChild,
  20. props = out.__preactattr_,
  21. vchildren = vnode.children;
  22. // 把dom节点的attributes都放在了dom["__preactattr_"]上
  23. if (props == null) {
  24. props = out.__preactattr_ = {};
  25. for (let a = out.attributes, i = a.length; i--;) {
  26. props[a[i].name] = a[i].value;
  27. }
  28. }
  29. // 如果vchildren只有一个节点,且是textnode节点时,直接更改nodeValue,优化性能
  30. // Optimization: fast-path for elements containing a single TextNode:
  31. if (!hydrating && vchildren && vchildren.length === 1 && typeof vchildren[0] === "string" && fc != null && fc.splitText !== undefined && fc.nextSibling == null) {
  32. if (fc.nodeValue != vchildren[0]) {
  33. fc.nodeValue = vchildren[0];
  34. }
  35. }
  36. // 比较子节点,将真实dom的children与vhildren比较
  37. // otherwise, if there are existing or new children, diff them:
  38. else if (vchildren && vchildren.length || fc != null) {
  39. innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML != null);
  40. }
  41. diffAttributes(out, vnode.attributes, props);
  42. return out;

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

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

相关文章

  • 帮你读懂preact源码(三)

    摘要:对回收的处理在中,回收调用了两个方法,节点的回收一般会调用,组件的回收会调用。个人理解从以上源码阅读中我们可以看到,最大的性能问题在于递归的,中的与也是为了缓解这个问题。为不同类型的更新分配优先级。 对回收的处理 在preact中,回收调用了两个方法,dom节点的回收一般会调用recollectNodeTree,组件的回收会调用unmountComponent。 preact复用dom...

    yuanxin 评论0 收藏0
  • 帮你读懂preact源码(二)

    摘要:最后删除新的树中不存在的节点。而中会记录对其做了相应的优化,节点的的情况下,不做移动操作。这种情况,在中得到了优化,通过四个指针,在每次循环中先处理特殊情况,并通过缩小指针范围,获得性能上的提升。 上篇文章已经介绍过idff的处理逻辑主要分为三块,处理textNode,element及component,但具体怎么处理component还没有详细介绍,接下来讲一下preact是如何处理...

    Warren 评论0 收藏0
  • 源码入手,文带你读懂Spring AOP面向切面编程

    摘要:,,面向切面编程。,切点,切面匹配连接点的点,一般与切点表达式相关,就是切面如何切点。例子中,注解就是切点表达式,匹配对应的连接点,通知,指在切面的某个特定的连接点上执行的动作。,织入,将作用在的过程。因为源码都是英文写的。 之前《零基础带你看Spring源码——IOC控制反转》详细讲了Spring容器的初始化和加载的原理,后面《你真的完全了解Java动态代理吗?看这篇就够了》介绍了下...

    wawor4827 评论0 收藏0
  • 少啰嗦!分钟带你读懂JavaNIO和经典IO区别

    摘要:的选择器允许单个线程监视多个输入通道。一旦执行的线程已经超过读取代码中的某个数据片段,该线程就不会在数据中向后移动通常不会。 1、引言 很多初涉网络编程的程序员,在研究Java NIO(即异步IO)和经典IO(也就是常说的阻塞式IO)的API时,很快就会发现一个问题:我什么时候应该使用经典IO,什么时候应该使用NIO? 在本文中,将尝试用简明扼要的文字,阐明Java NIO和经典IO之...

    Meils 评论0 收藏0
  • 篇文章教你读懂UI绘制流程

    摘要:最近有好多人问我没信心去深造了,找不到好的工作,其实我以一个他们进行回复,发现他们主要是内心比较浮躁,要知道技术行业永远缺少的是高手。至此整体绘制过程我们就已经非常清楚了。我门可以根据这种绘制的流程来操作自己的自定义组件。 最近有好多人问我Android没信心去深造了,找不到好的工作,其实我以一个他们进行回复,发现他们主要是内心比较浮躁,要知道技术行业永远缺少的是高手。建议先阅读浅谈A...

    ghnor 评论0 收藏0

发表评论

0条评论

XboxYan

|高级讲师

TA的文章

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