preact 源码学习系列之一:JSX解析与DOM渲染

@youngwind 2017-05-25 02:14:19发表于 youngwind/blog preact 源码

前言

一直以来我都想研究 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

请注意一下两点:

  1. JSX 并不是 React 专有的,它本质上是一种 DOM 表示结构,不过是因为 React 流行起来而被大家所熟知而已。
  2. 我使用的是 babel5,而非 babel6,所以在自定义 pragma 的格式方面与上述连接中有些不同。我的配置如下:
    {
        "jsxPragma": "h"
    }
    为什么要使用 babel5 而不是 babel6 呢?因为对于此部分的代码而言,babel6 转换之后的代码可读性太差了,且我参考的 preact 版本用的也是 babel5。

ok,经过 babel 转换之后,原有的 JSX 变成了下面这个样子。
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 函数的嵌套调用,最终返回的结果如下:
vNode

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;
}

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

后话

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

参考资料:WTF is JSX, By developit

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