ZEROMAKE | keep codeing and thinking!
2017-07-24 | source

preact 源码解读(1)

前言

  • 和上次说的一样这次带来preact的解读
  • preact 实际上把它当作是一个精简版react就好了。
  • 这次我抄下了preact,并且改写了代码, 命名为zreact
  • 把之前将事件,props 之类的单独放出来,这样这份zreact
  • 可以支持 ie8,虽然并没有什么用。
  • 这次代码解读顺序按使用 preact 的代码顺序。
  • 这里是第一篇,createElement,也就是 vue,react 的 render 所返回的 VNode 对象。
  • 平常则是使用 babel+jsx 来生成 createElement 调用。
  • vue 常用则是 template,但是通过 webpack 会做到预先转换为 render。

一、jsx 的转换原理。

对于 preact 来说,最常见的就是 jsx。
下面是一个最简单的使用 preact。

1
import {h, render, Component} from "preact";
2
3
/** @jsx h */
4
// 通过上面的注释告诉babel使用什么方法作为VNode的创建函数。
5
// 如果不使用这个默认会是React.createElement,
6
// 或者通过下面的babel配置来修改
7
class App extends Component {
8
render(props) {
9
return <h1>App</h1>;
10
}
11
}
12
var test = "";
13
render(
14
<div className="test">
15
<span style={test}>测试</span>
16
<App></App>
17
</div>
18
, document.body)

.babelrc

1
{
2
"presets": ["es2015"],
3
"plugins": [["transform-react-jsx", { "pragma": "h" }]]
4
}

通过 babel 转换后会变成

1
import {h, render} from "preact";
2
3
class App extends Component {
4
render() {
5
return h("h1", null, "App");
6
}
7
}
8
var test = "";
9
render(
10
h(
11
"div",
12
{ className: "test" },
13
h("span", { style: test }, "测试"),
14
h(App)
15
)
16
document.body
17
)

所以对于 preact 最先执行的东西是这个h函数也就是createElement
对于jsx标准的createElement函数签名为

1
interface IKeyValue {
2
[name: string]: any;
3
}
4
/**
5
* 标准JSX转换函数
6
* @param {string|Component} nodeName 组件
7
* @param {IKeyValue} attributes 组件属性
8
* @param {any[]} childs 这个VNode的子组件
9
*/
10
function h(
11
nodeName: string | function,
12
attributes: IKeyValue,
13
...childs: any[]
14
): VNode;
15
class VNode {
16
public nodeName: string | Component;
17
public children: any[];
18
public attributes: IKeyValue | undefined;
19
public key: any | undefined;
20
}

所以这里的标准jsx非常简单。

  1. 第一个参数为原生 html 组件或者Component类。
  2. 第二个参数为该组件的属性,及自定义属性。
  3. 第三个参数及后面的所有都是这个组件的子组件。
  4. 其中第三个及后面的参数为数组就会被分解放入子组件中。
  5. 最后返回一个VNode实例。

二、createElement 的实现

1
function h(
2
nodeName: string | Component,
3
attributes: IKeyValue,
4
...args: any[]
5
) {
6
// 初始化子元素列表
7
const stack: any[] = [];
8
const children: any[] = [];
9
// let i: number;
10
// let child: any;
11
// 是否为原生组件
12
let simple: boolean;
13
// 上一个子元素是否为原生组件
14
let lastSimple: boolean = false;
15
// 把剩余的函数参数全部倒序放入stack
16
for (let i = args.length; i--; ) {
17
stack.push(args[i]);
18
}
19
// 把元素上属性的children放入栈
20
if (attributes && attributes.children != null) {
21
if (!stack.length) {
22
stack.push(attributes.children);
23
}
24
// 删除
25
delete attributes.children;
26
}
27
// 把stack一次一次取出
28
while (stack.length) {
29
// 取出最后一个
30
let child: any = stack.pop();
31
if (child && child.pop !== undefined) {
32
// 如果是个数组就倒序放入stack
33
for (let i = child.length; i--; ) {
34
stack.push(child[i]);
35
}
36
} else {
37
// 清空布尔
38
if (typeof child === "boolean") {
39
child = null;
40
}
41
// 判断当前组件是否为自定义组件
42
simple = typeof nodeName !== "function";
43
if (simple) {
44
// 原生组件的子元素处理
45
if (child == null) {
46
// null to ""
47
child = "";
48
} else if (typeof child === "number") {
49
// num to string
50
child = String(child);
51
} else if (typeof child !== "string") {
52
// 不是 null,number,string 的不做处理
53
// 并且设置标记不是一个字符串
54
simple = false;
55
}
56
}
57
if (simple && lastSimple) {
58
// 当前为原生组件且子元素为字符串,并且上一个也是。
59
// 就把当前子元素加到上一次的后面。
60
children[children.length - 1] += child;
61
} else {
62
// 其它情况直接加入children
63
children.push(child);
64
}
65
/* else if (children === EMPTY_CHILDREN) {
66
children = [child];
67
} */
68
// 记录这次的子元素状态
69
lastSimple = simple;
70
}
71
}
72
const p = new VNode();
73
// 设置原生组件名字或自定义组件class(function)
74
p.nodeName = nodeName;
75
// 设置子元素
76
p.children = children;
77
// 设置属性
78
p.attributes = attributes == null ? undefined : attributes;
79
// 设置key
80
p.key = attributes == null ? undefined : attributes.key;
81
// vnode 钩子
82
if (options.vnode !== undefined) {
83
options.vnode(p);
84
}
85
return p;
86
}

这个标准 jsx 的VNode生成函数很简单,这边要注意的是子组件是连续的字符串。
会被合并成一个,这样可以防止在生成 dom 时,创建多余的Text

三、clone-element

1
import { h } from "./h";
2
import { VNode } from "./vnode";
3
import { extend } from "./util";
4
5
/**
6
* 通过VNode对象新建一个自定义的props,children的VNode对象
7
* @param vnode 旧vnode
8
* @param props 新的props
9
* @param children 新的子组件
10
*/
11
export function cloneElement(vnode: VNode, props: any, ...children: any[]) {
12
const child: any = children.length > 0 ? children : vnode.children;
13
return h(vnode.nodeName, extend({}, vnode.attributes, props), child);
14
}

clone-element 依赖于 createElement

四、后记

  • 这次的 blog 感觉好短,我已经没有东西写了。
  • 话说回来 vue 的 template,现在看来不如说是一个变异的 jsx 语法。
  • 感觉明明是在读 preact 源码却对 vue 的实现更加的理解了。
  • 下一篇应该是Component了。

五、资料

  1. preact 源码
  2. zreact 源码
  3. React 初窥:JSX 详解