preact 源码学习:JSX解析与DOM渲染
Posted 前端控
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了preact 源码学习:JSX解析与DOM渲染相关的知识,希望对你有一定的参考价值。
前言
一直以来我都想研究 React 的源码,但总是看不懂。即便是去翻看最早的源码,代码量也有1万多行,研究起来难度太高了,这个问题困扰了我很久。 直到前两天我跟同事讨论起这个问题,忽然发现一个可行的方法:React 的源码太难懂,可以看 preact 的源码啊!为什么呢?因为 React 代表的是一种思想,能实现这种思想的不只有 React。preact 便是一个 mini 版的 React,其代码量很少,目前也就 1000多行,但是已经实现了 React 的主要功能。 这的确是一个很不错的研究方法,值得推荐。那么,我们便按着这种思路来研究,此次参考的是 preact 2.0.1 版本。
目标
React 类框架功能很多,应该从哪个角度入手的。 答案:从“解析 JSX,渲染 DOM 入手” 举个例子:
import {render} from 'preact';
render((
<div id="foo">
<span>Hello, world!</span>
<button>按钮</button>
</div>
), document.body);
问题是:给定一段 JSX 和一个挂载点,如何解析 JSX,生成真实 DOM 并挂载到页面中呢?
谁来解析 JSX
要想生成真实 DOM,必须有一个层级嵌套的对象,此对象表征了 DOM 的嵌套结构。我们只需要遍历此对象,便可拼接出 DOM。 问题是: 如何把这段 JSX 格式的字符串转化为对应嵌套结构的对象呢? 这本质上是 html 解析器所要完成的事情。然而,我无力实现一个 HTML 解析器,怎么办呢? → 答案是使用 babel。 babel 作为一个代码转换工具,它不仅仅能将 ES6 转化成 ES5,也能够将 JSX 转换成某个函数嵌套调用的结构,而这个函数是可以自定义的,具体请参考 transform-react-jsx
请注意一下两点:
JSX 并不是 React 专有的,它本质上是一种 DOM 表示结构,不过是因为 React 流行起来而被大家所熟知而已。
我使用的是 babel5,而非 babel6,所以在自定义 pragma 的格式方面与上述连接中有些不同。我的配置如下:
json{"jsxPragma":"h"}
为什么要使用 babel5 而不是 babel6 呢?因为对于此部分的代码而言,babel6 转换之后的代码可读性太差了,且我参考的 preact 版本用的也是 babel5。
ok,经过 babel 转换之后,原有的 JSX 变成了下面这个样子。 从图中我们可以看到:经过 babel 转换之后,JSX 变成了 preact.h 的嵌套调用。 因此,问题就转化为:如何编写这样的一个 h 函数,使得上述嵌套调用最终返回一个层级嵌套的对象,此对象表征了 DOM 的结构。
h 函数的编写
h 是这样的一个函数,接收参数为:标签名、属性值和子元素,返回一个对象,该对象描述了一个 DOM 节点。
class VNode {
constructor(nodeName, attributes, children) {
this.nodeName = nodeName;
this.attributes = attributes;
this.children = children;
}
}
function h(nodeName, attributes, ...args) {
// 子元素的个数是不确定的
let children = args.length ? [].concat(...args) : null;
return new VNode(nodeName, attributes, children);
}
经过这样的 h 函数的嵌套调用,最终返回的结果如下:
DOM 渲染
有了上面的 vNode 结构,我们便能将之转换成真实的 DOM 元素。此处逻辑并不复杂,无非是递归的调用,代码如下:
function buildDOMByVNode(vNode) {
if (typeof vNode === 'string') {
return document.createTextNode(vNode);
}
let {nodeName, attributes: attrs, children} = vNode;
if (typeof nodeName === 'string') {
let node = document.createElement(nodeName);
// 处理属性
if (attrs) {
for (let key in attrs) {
if(!attrs.hasOwnProperty(key)) continue;
setAttributes(node, key, attrs[key]);
}
}
// 处理子元素
if (children) {
children.forEach(child => {
// 递归
let subNode = buildDOMByVNode(child);
node.appendChild(subNode);
});
}
return node;
}
}
// 整个 render 的入口
function render(vNode, parent) {
let builtDOM = buildDOMByVNode(vNode);
parent.appendChild(builtDOM);
return builtDOM;
}
最终实现效果如下图所示:
后话
本文实现的具体代码参考这里,这只是一个最基本的 demo,后续还有很多有待探索的功能,比如构造 Component 类,比如 DOM 的 diff 和 update 等等。
参考资料:WTF is JSX, By developit
一些题外话
由于无法在文章中添加外部链接,一定程度上影响行文流畅程度,如果想查看文中缺失的超链接,请点击下方的“原文链接”到我的博客下进行阅读。
如果你觉得我的文章写得不错,想支持我,请“长按识别下方二维码”,你的点滴支持会鼓励我持续创作。
以上是关于preact 源码学习:JSX解析与DOM渲染的主要内容,如果未能解决你的问题,请参考以下文章
ReactReact全家桶React 概述+虚拟DOM的创建与渲染+JSX