Preact 中的 h
方法,相当于 React 中的 createElement
,在 JSX 中用于生产 VNode 的方法。
TL;DR;
请直接看最后,带注释的源码。
什么是 JSX
按照源码指示的这篇文章,这有一个最简单的例子:
1 2
| let foo = <div id="foo">Hello!</div>;
|
Babel 等工具可以将其转化为
1
| var foo = h('div', {id:"foo"}, 'Hello!');
|
至于为什么叫 h
,是因为这个 idea 最初来自 hyperscript (“hypertext“ + “javascript”)。
什么是 VNode
字面解析,VNode = “Virtual“ + “Node”,虚拟 DOM 节点,大概会是以下这么一个结构:
1 2 3 4 5 6 7
| { nodeName: "div", attributes: { "id": "foo" }, children: ["Hello!"] }
|
看起来很简单嘛,3行代码实现 h
方法:
1 2 3 4
| function h(nodeName, attributes, ...args) { let children = args.length ? [].concat(...args) : null; return { nodeName, attributes, children }; }
|
但是,实时当然没有那么简单。
源码解析
光看代码有点难解释,我们先来一个尽可能复杂的简单例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import { Component } from 'preact';
function getItem(text, idx) { return <li key={`p${idx}`}>{text}</li>; }
class Section extends Component { render() { const { children, ...rest } = this.props; return <section {...rest}>{children}</section>; } }
const node = <Section id="root" style={{ color: 'red' }}> <ul id="list"> { [ 'Hello', 'World' ].map(getItem) } </ul> <div id="text"> number: {18}; string: {'str'}; boolean: {true}; {/* comment */} </div> </Section>;
|
通过 Babel 即可转换为 js 内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| var _preact = require('preact');
function getItem(text, idx) { return h( 'li', { key: 'p' + idx }, text ); }
var Section = function (_Component) { _inherits(Section, _Component); function Section() { } _createClass(Section, []); return Section; }(_preact.Component);
var node = h( Section, { id: 'root', style: { color: 'red' } }, h( 'ul', { id: 'list' }, ['Hello', 'World'].map(getItem) ), h( 'div', { id: 'text' }, 'number: ', 18, '; string: ', 'str', '; boolean: ', true, ';' ) );
|
为了更加直观,我们可以将 Section
和 .map(getItem)
的代码执行结果获取出来,得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| var Section = function () { };
var node = h( Section, { id: 'root', style: { color: 'red' } }, h( 'ul', { id: 'list' }, [ h('li', { key: 'p0' }, 'Hello'), h('li', { key: 'p1' }, 'World') ] ), h( 'div', { id: 'text' }, 'number: ', 18, '; string: ', 'str', '; boolean: ', true, ';' ) );
|
下面我们将看看,这里面的 5 个 h()
或做些什么。为了避免源码解析中有任何差异,我这里参照的是 Preact@8.2.7 的代码:
单个 Child 的简单 VNode
我们首先处理两个最简单的 <li>
: h('li', { key: 'p0' }, 'Hello')
。首先,可以看到 L39 到 L45,将第三个及以后的参数都作为 children 推入 stack,如果有 attributes.children,也推入 stack,并删除这个属性。于是,我们将得到:
接下来开始通过 while (stack.length)
遍历这个数组,但实际上自有 'Hello'
这一个 child
,且 L53 到 L57 判断 simple
为 true
,所以我们在 L63 得到:
因此由 L73 到 L77 得到这个 VNode,更直观地,我们用一个 JSON 来记录:
1 2 3 4 5 6
| { nodeName: 'li', children: [ 'Hello' ], attributes: { key: 'p0' }, key: 'p0', }
|
Simple Child 优化
再看看 h('div', { id: 'text' }, ... }
这部分,和上面的 li
差不多,({/* comment */}
部分已经在 Babel 中被去除),注意全部内容已经反序:
1 2 3 4 5 6 7 8 9
| stack = [ ';' true, '; boolean: ', 'str', '; string: ', 18, 'number: ', ];
|
而因为 L53 到 L57 判断 simple
一直为 true
(因为 L51 和 L54,boolean 后面的 true
被转换成了 ‘’),所以 child 一直在 我们在 L60 进行字符串拼接,最终得到:
1 2 3 4 5
| { nodeName: 'div', children: [ 'number: 18; string: str; boolean: ;' ], attributes: { id: 'text' }, }
|
数组 Child 的优化
回到刚刚构建好的两个 <li>
,现在可以看看 <ul>
,伪代码表示:
1 2 3 4 5
| h( 'ul', { id: 'list' }, [ VNode('p0'), VNode('p1') ] ),
|
在 while
之前得到的是:
1
| stack = [ [ VNode('p0'), VNode('p1') ] ];
|
如果还是原本的套路,最终 children 也将会是一个“数组套数组”的结构,于是可以看到 L48 将数组的内容再次拆开 push 进 stack。于是可以得到一个更好看的结果:
1 2 3 4 5
| { nodeName: 'ul', children: [ VNode('p0'), VNode('p1') ], attributes: { id: 'list' }, }
|
自定义 Component
最后一个 h
是最外层的 <Section>
,这里有一个最大的差异在于, nodeName = Section
已经不是一个类似 div
一般的字符串,而是一个 Component
,即为一个 function
,在 L53 中可以看到这类的 nodeName 是 不 simple 的,于是直接会把说有 child push 进 children 里面。
1 2 3 4 5
| { nodeName: Section, children: [ VNode('ul#list'), VNode('div#text') ], attributes: { id: 'root', style: { color: 'red' } }, }
|
注意,因为 不 simple,即使 stack 里有连串的 string 或 number,也不会做拼接:
1 2 3 4 5 6 7 8 9 10 11 12
| h( Section, null, 'string: ', 'str', ';' );
{ nodeName: Section, children: [ 'string: ', 'str', ';' ], }
|
最终结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| { nodeName: Section, attributes: { id: 'root', style: { color: 'red' } }, children: [{ nodeName: 'ul', attributes: { id: 'list' }, children: [{ nodeName: 'li', key: 'p0', attributes: { key: 'p0' }, children: [ 'Hello' ], }, { nodeName: 'li', key: 'p1', attributes: { key: 'p1' }, children: [ 'World' ], }], }, { nodeName: 'div', children: [ 'number: 18; string: str; boolean: ;' ], attributes: { id: 'text' }, }], }
|
带注释的源码
最后将上面的全部内容写成注释,放到源码中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| import { VNode } from './vnode'; import options from './options';
const stack = [];
const EMPTY_CHILDREN = [];
export function h(nodeName, attributes) { let children=EMPTY_CHILDREN, lastSimple, child, simple, i; for (i=arguments.length; i-- > 2; ) { stack.push(arguments[i]); } if (attributes && attributes.children!=null) { if (!stack.length) stack.push(attributes.children); delete attributes.children; } while (stack.length) { if ((child = stack.pop()) && child.pop!==undefined) { for (i=child.length; i--; ) stack.push(child[i]); } else { if (typeof child==='boolean') child = null;
if ((simple = typeof nodeName!=='function')) { if (child==null) child = ''; else if (typeof child==='number') child = String(child); else if (typeof child!=='string') simple = false; }
if (simple && lastSimple) { children[children.length-1] += child; } else if (children===EMPTY_CHILDREN) { children = [child]; } else { children.push(child); }
lastSimple = simple; } }
let p = new VNode(); p.nodeName = nodeName; p.children = children; p.attributes = attributes==null ? undefined : attributes; p.key = attributes==null ? undefined : attributes.key;
if (options.vnode!==undefined) options.vnode(p);
return p; }
|