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 入手” 举个例子:

 
   
   
 
  1. import {render} from 'preact';

  2. render((

  3.    <div id="foo">

  4.        <span>Hello, world!</span>

  5.        <button>按钮</button>

  6.    </div>

  7. ), 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 的格式方面与上述连接中有些不同。我的配置如下: json{"jsxPragma":"h"} 为什么要使用 babel5 而不是 babel6 呢?因为对于此部分的代码而言,babel6 转换之后的代码可读性太差了,且我参考的 preact 版本用的也是 babel5。

ok,经过 babel 转换之后,原有的 JSX 变成了下面这个样子。 从图中我们可以看到:经过 babel 转换之后,JSX 变成了 preact.h 的嵌套调用。 因此,问题就转化为:如何编写这样的一个 h 函数,使得上述嵌套调用最终返回一个层级嵌套的对象,此对象表征了 DOM 的结构。

h 函数的编写

h 是这样的一个函数,接收参数为:标签名、属性值和子元素,返回一个对象,该对象描述了一个 DOM 节点。

 
   
   
 
  1. class VNode {

  2.    constructor(nodeName, attributes, children) {

  3.        this.nodeName = nodeName;

  4.        this.attributes = attributes;

  5.        this.children = children;

  6.    }

  7. }

  8. function h(nodeName, attributes, ...args) {

  9.    // 子元素的个数是不确定的

  10.    let children = args.length ? [].concat(...args) : null;

  11.    return new VNode(nodeName, attributes, children);

  12. }

经过这样的 h 函数的嵌套调用,最终返回的结果如下: preact 源码学习:JSX解析与DOM渲染

DOM 渲染

有了上面的 vNode 结构,我们便能将之转换成真实的 DOM 元素。此处逻辑并不复杂,无非是递归的调用,代码如下:

 
   
   
 
  1. function buildDOMByVNode(vNode) {

  2.    if (typeof vNode === 'string') {

  3.        return document.createTextNode(vNode);

  4.    }

  5.    let {nodeName, attributes: attrs, children} = vNode;

  6.    if (typeof nodeName === 'string') {

  7.        let node = document.createElement(nodeName);

  8.        // 处理属性

  9.        if (attrs) {

  10.            for (let key in attrs) {

  11.                if(!attrs.hasOwnProperty(key)) continue;

  12.                setAttributes(node, key, attrs[key]);

  13.            }

  14.        }

  15.        // 处理子元素

  16.        if (children) {

  17.            children.forEach(child => {

  18.                // 递归

  19.                let subNode = buildDOMByVNode(child);

  20.                node.appendChild(subNode);

  21.            });

  22.        }

  23.        return node;

  24.    }

  25. }

 
   
   
 
  1. // 整个 render 的入口

  2. function render(vNode, parent) {

  3.    let builtDOM = buildDOMByVNode(vNode);

  4.    parent.appendChild(builtDOM);

  5.    return builtDOM;

  6. }

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

后话

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

参考资料:WTF is JSX, By developit

一些题外话

  1. 由于无法在文章中添加外部链接,一定程度上影响行文流畅程度,如果想查看文中缺失的超链接,请点击下方的“原文链接”到我的博客下进行阅读。

  2. 如果你觉得我的文章写得不错,想支持我,请“长按识别下方二维码”,你的点滴支持会鼓励我持续创作。



以上是关于preact 源码学习:JSX解析与DOM渲染的主要内容,如果未能解决你的问题,请参考以下文章

赠书Preact(React)核心原理详解

PREACT学习笔记

PREACT学习笔记

ReactReact全家桶React 概述+虚拟DOM的创建与渲染+JSX

ReactReact全家桶React 概述+虚拟DOM的创建与渲染+JSX

vue 渲染函数&jsx