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

  • 前言
  • JSX 渲染
    • VNode
    • createElement 函数
    • coerceToVNode 函数
  • 总结
  • 参考资料

系列文章:

  1. 从 Preact 源码一窥 React 原理(一):JSX 渲染(本文)
  2. 从 Preact 源码一窥 React 原理(二):Diff 算法
  3. 从 Preact 源码一窥 React 原理(三):组件

前言

Preact 是什么?
Preact 是 React 的轻量级实现,在 3KB 的量级提供了你所需要的功能:渲染 JSX、组件、虚拟 DOM、Legacy/New Context API,甚至还有一些 React 外的新特性。虽然不包含 React 16 所带来的例如 Fiber 等新特性,但是在这样小巧的体积下,还要什么自行车呢?
可以预见的是,在享受 Preact 的便利,压缩生成代码体积的同时,必不可免的会落到一些随之而来的坑里头去。但是本文的核心并不是 Preact 在工程实践中的例子,而是通过 Preact 来一窥 React 框架的实现原理(由于 React 庞大的体积,直接上手阅读源码实在是不够友好),Preact 便成为了绝佳的学习案例。

JSX 渲染

在一头扎入 Preact 源码里头之前,首先应当明确的是:我们在探求些什么?
漫无目的的寻找只会像没头的苍蝇一般处处碰壁。让我们从这一个最经典的用法出发:

import { h, render } from 'preact';render((<div id="foo"><span>Hello, world!</span><button onClick={ e => alert("hi!") }>Click Me</button></div>
), document.body);

这是 Preact 官网上的一个示例,展示了 Preact 最基础的用法。
如果你曾使用过 React ,那么你对这一段代码应该很熟悉了。但你是否思考过这一段代码中实际上发生了什么事情呢?
在这里,赋给 render函数中的第一个参数是并不属于 JavaScript 标准的 JSX ,其是如何与 render函数相结合的呢?

是的,通过 Babel
如果你足够细心的话,你也许能注意到被 import 却没有被调用的 h函数。
Babel 能够将 JSX 语法提前转化为对 h函数的递归调用,上述代码的转换结果如下所示:

h("div", {id: "foo"},h("div", {onClick: { e => alert("hi!") }}, "Click Me"),h("span", null, "Hello, world!")
)

不难推测, h函数(hyperscript的缩写,也就是 Preact 中的 createElement函数的别名)的所接受的参数为:

  • type 节点类型:上述例子中只存在简单的节点,因此其节点类型均为 string 值,事实上,对于组件节点,其节点类型则为函数;
  • props 节点属性:为 JSX 中对应节点所声明属性的集合;
  • children 子节点:从第三个参数开始,后续的参数均为子节点。子节点中除了递归调用 h函数返回的值以外,还可能是 string、boolean 等。

h函数将根据 Babel 转义的结果生成虚拟节点的树(以下将用createElement替代h称呼,因为createElement字面上更接近其函数的本意)。

VNode

createElement函数的返回值是 Preact 中的所使用的虚拟 DOM 的节点 VNode
VNode包含属性如下:

  • type:节点的类型,可能为 string (元素节点: node.nodeType === Node.ELEMENT_NODE)或 function(组件节点)或 null (值为 boolean 、string 等类型的文本节点: node.nodeType === Node.TEXT_NODE);
  • props:节点的属性。同时,props中包含了 children属性,即包含了所有的子 VNode 节点;
  • text:节点的文本属性,简单的节点解析并挂载到 DOM 上之后为文本节点,此类节点其他属性均为 null,只需要存一个文本属性值;
  • key:节点的键值,用于在 diff 算法中作为元素匹配的标记;
  • ref:React 的 ref属性;
  • _children:通过toChildArray属性将 props.children中的孩子节点展平,也就是将 props.children中的数组中的元素提取出来存入_children中;
  • _dom:虚拟 DOM 节点所对应的实际 DOM 节点;
  • _lastDomChildFragment节点的最后一个 DOM 子节点;
  • _component:组件节点所对应的组件实例。

createElement 函数

createElement函数的实现很简单,根据上述介绍的 Babel 传入参数,提取出 VNode所需的属性即可。
具体的实现如下所示,附上了部分注释:

// src/create-element.js
// createElement 参数:节点类型,节点属性,孩子节点(可能有多个,因此需要从 arguments 中取)
export function createElement(type, props, children) {if (props==null) props = {};// 获取所有的孩子节点if (arguments.length>3) {children = [children];for (let i=3; i<arguments.length; i++) {children.push(arguments[i]);}}if (children!=null) {props.children = children;}// "type" may be undefined during development. The check is needed so that// we can display a nice error message with our debug helpers// 提取组件节点中的默认属性if (type!=null && type.defaultProps!=null) {for (let i in type.defaultProps) {if (props[i]===undefined) props[i] = type.defaultProps[i];}}// 提取出 props 中不需要的 ref 和 key 属性let ref = props.ref;if (ref) delete props.ref;let key = props.key;if (key) delete props.key;// 通过简单的构造创建 VNodereturn createVNode(type, props, null, key, ref);
}

createElement函数首先将 arguments中的孩子节点放入数组中,并赋值给 props.children
对于组件节点中可能存在的 defaultProps预设值,函数将其付给 props中对应的空缺属性。
执行完上述操作之后,函数提取出 props中可能存在的 key以及 ref值,因其不需要提供给开发者,将其从 props中删去。
最终,通过 createVNode函数创建 VNode的实例。 createVNode函数非常简单,仅仅通过字面量创建一个包含给定属性,并将未给定属性置为 null的对象。

coerceToVNode 函数

由于 Babel 仅仅会将元素节点或者组件节点的参数传入 createElement函数,因此,在渲染过程中还需要针对于其他类型的节点提供一定的处理。
这些节点仅仅存在于元素节点或者组件节点的 children参数中,Preact 中通过 coerceToVNode函数对其进行处理,例如 boolean 值的节点,抑或是 string 值的节点。
具体函数实现如下所示:

// src/create-element.js
export function coerceToVNode(possibleVNode) {// null / undefined / boolean 等值直接返回 nullif (possibleVNode == null || typeof possibleVNode === 'boolean') return null;// string / number 则返回文本节点if (typeof possibleVNode === 'string' || typeof possibleVNode === 'number') {return createVNode(null, null, possibleVNode, null, null);}// 对于数组则返回 Fragment 节点if (Array.isArray(possibleVNode)) {return createElement(Fragment, null, possibleVNode);}// Clone vnode if it has already been used. ceviche/#57// 并非第一次解析的节点则进行一次克隆操作if (possibleVNode._dom!=null) {let vnode = createVNode(possibleVNode.type, possibleVNode.props, possibleVNode.text, possibleVNode.key, null);vnode._dom = possibleVNode._dom;return vnode;}return possibleVNode;
}

对于 null / undefined / boolean值的节点直接返回 null
对于 string / number值的节点则返回包含其内容的文本节点;
对于数组类型的节点则返回一个 Fragment节点,其是对于多个子元素的聚合;
对于非第一次解析的节点则返回该节点的克隆。

总结

本文作为 Preact 源码解析的第一篇,简单介绍了 JSX 到 Preact 中 VNode 的转化,还未涉及其中真正核心的部分,算是一碟开胃小菜。
后续的文章中将呈现 diff 算法,组件等真正的重头戏。

参考资料

  1. Preact 官网
  2. Peact - Github

从 Preact 源码一窥 React 原理(一):JSX 渲染相关推荐

  1. 从 Preact 源码一窥 React 原理(二):Diff 算法

    从 Preact 源码一窥 React 原理(二):Diff 算法 前言 Diff 算法 渲染 diffChildren 函数 diff 函数 diffElementNodes 函数 diffProp ...

  2. react学习笔记 react-router-dom react-redux基础使用及手写基础源码 组件反射 react原理

    vdom diff 高效的diff算法 新老vdom树比较 更新只需要更新节点 数据变化检测 batch dom读写 组件多重继承 //parent components export default ...

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

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

  4. preact源码分析

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

  5. react 组件遍历】_从 Context 源码实现谈 React 性能优化

    (给前端大全加星标,提升前端技能) 转自:魔术师卡颂 学完这篇文章,你会收获: 了解Context的实现原理 源码层面掌握React组件的render时机,从而写出高性能的React组件 源码层面了解 ...

  6. preact源码分析,有毒

    最近读了读preact源码,记录点笔记,这里采用例子的形式,把代码的执行过程带到源码里走一遍,顺便说明一些重要的点,建议对着preact源码看 vnode和h() 虚拟结点是对真实DOM元素的一个js ...

  7. 老李推荐:第5章5节《MonkeyRunner源码剖析》Monkey原理分析-启动运行: 获取系统服务引用 1...

    老李推荐:第5章5节<MonkeyRunner源码剖析>Monkey原理分析-启动运行: 获取系统服务引用 上一节我们描述了monkey的命令处理入口函数run是如何调用optionPro ...

  8. 老李推荐:第6章1节《MonkeyRunner源码剖析》Monkey原理分析-事件源-事件源概览 1...

    老李推荐:第6章1节<MonkeyRunner源码剖析>Monkey原理分析-事件源-事件源概览 在上一章中我们有简要的介绍了事件源是怎么一回事,但是并没有进行详细的描述.那么往下的这几个 ...

  9. 老李推荐:第6章6节《MonkeyRunner源码剖析》Monkey原理分析-事件源-事件源概览-命令队列...

    老李推荐:第6章6节<MonkeyRunner源码剖析>Monkey原理分析-事件源-事件源概览-命令队列 事件源在获得字串命令并把它翻译成对应的MonkeyEvent事件后,会把这些事件 ...

最新文章

  1. 怎样将无线路由做成无线AP
  2. 微服务架构——不是免费的午餐
  3. F-Strings:超级好用的Python格式字符串!!
  4. 【Nginx】基本数据结构
  5. 2篇word文档比较重复率_继续教育 | 你该知道的论文小技巧——重复率检测
  6. 第一周小组博客作业——1701班5组
  7. html关于拖放叙述错误,CIW页面设计与制作HTML附答案
  8. 测试Spring的“会话”范围
  9. Objective-C 之category
  10. 答网友问:一个abs函数引发的问题
  11. WORD批量更改所有图片大小
  12. 关于MultiActionController异步Ajax,post;
  13. Main线程与main()方法的关系
  14. Linux英伟达驱动程序下载和安装
  15. 2019年美赛B题思路详解
  16. 菜鸟站长之家收集分享一些比较出名的外链发布地址
  17. Eclipse修改JSP新建模板
  18. 小米手机上的便签更改了,如何恢复之前的内容?
  19. 配置安装跟踪服务器Tracker 配置FastDFS存储服务器 Storage
  20. Android Jetpack Startup库分析

热门文章

  1. 图像处理之目标检测与识别
  2. echarts的人员迁徙地图动态效果
  3. iOS 程序员必须收藏的资源大全
  4. Windows 7 x64 (中/英文操作系统)安装SQLServer 2005版本相关解决方法
  5. Python函数之传参
  6. sqlcmd导出备份数据到CSV/交互
  7. 图论算法讲解--最短路--Dijkstra算法
  8. 江湖侠客令服务器维护,江湖侠客令关服公告什么时候关服_关服公告及补偿_3DM页游...
  9. Spring5(从头到尾)笔记总结
  10. 数据分析面试题——统计理论