浏览器解析jsx_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((

Hello, world!

按钮

), 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 的格式方面与上述连接中有些不同。我的配置如下:

{

"jsxPragma": "h"

}

为什么要使用 babel5 而不是 babel6 呢?因为对于此部分的代码而言,babel6 转换之后的代码可读性太差了,且我参考的 preact 版本用的也是 babel5。

ok,经过 babel 转换之后,原有的 JSX 变成了下面这个样子。

44bf375bdcef3f226a3083a7290123bb.png

从图中我们可以看到:经过 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 函数的嵌套调用,最终返回的结果如下:

1111c17d38030762c51329c08da783c5.png

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;

}

最终实现效果如下图所示:

32a9e0ed889c7421e4a6d478e4b728c4.png

后话

本文实现的具体代码参考这里,这只是一个最基本的 demo,后续还有很多有待探索的功能,比如构造 Component 类,比如 DOM 的 diff 和 update 等等。

参考资料:WTF is JSX, By developit

--------------- EOF ----------------

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值