系列文章:
Preact 是什么?
Preact 是 React 的轻量级实现,在 3KB 的量级提供了你所需要的功能:渲染 JSX、组件、虚拟 DOM、Legacy/New Context API,甚至还有一些 React 外的新特性。虽然不包含 React 16 所带来的例如 Fiber 等新特性,但是在这样小巧的体积下,还要什么自行车呢?
可以预见的是,在享受 Preact 的便利,压缩生成代码体积的同时,必不可免的会落到一些随之而来的坑里头去。但是本文的核心并不是 Preact 在工程实践中的例子,而是通过 Preact 来一窥 React 框架的实现原理(由于 React 庞大的体积,直接上手阅读源码实在是不够友好),Preact 便成为了绝佳的学习案例。
在一头扎入 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
函数的别名)的所接受的参数为:
h
函数返回的值以外,还可能是 string、boolean 等。h
函数将根据 Babel 转义的结果生成虚拟节点的树(以下将用createElement
替代h
称呼,因为createElement
字面上更接近其函数的本意)。
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 节点;_lastDomChild
:Fragment
节点的最后一个 DOM 子节点;_component
:组件节点所对应的组件实例。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;
// 通过简单的构造创建 VNode
return createVNode(type, props, null, key, ref);
}
createElement
函数首先将 arguments
中的孩子节点放入数组中,并赋值给 props.children
。
对于组件节点中可能存在的 defaultProps
预设值,函数将其付给 props
中对应的空缺属性。
执行完上述操作之后,函数提取出 props
中可能存在的 key
以及 ref
值,因其不需要提供给开发者,将其从 props
中删去。
最终,通过 createVNode
函数创建 VNode
的实例。 createVNode
函数非常简单,仅仅通过字面量创建一个包含给定属性,并将未给定属性置为 null
的对象。
由于 Babel 仅仅会将元素节点或者组件节点的参数传入 createElement
函数,因此,在渲染过程中还需要针对于其他类型的节点提供一定的处理。
这些节点仅仅存在于元素节点或者组件节点的 children
参数中,Preact 中通过 coerceToVNode
函数对其进行处理,例如 boolean 值的节点,抑或是 string 值的节点。
具体函数实现如下所示:
// src/create-element.js
export function coerceToVNode(possibleVNode) {
// null / undefined / boolean 等值直接返回 null
if (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 算法,组件等真正的重头戏。