建议学习 preact 之前,先看 Build Your Own React - 若叶知秋Build your own React

项目初始化

preact src 下最基础的目录结构

  /Users/simon/github/react/stb-preact/src:
  total used in directory 80K available 4.3 GiB
  drwxr-xr-x  4 simon staff  128 Jun 20 17:32 diff
  -rw-r--r--  1 simon staff   56 Jun 20 17:29 clone-element.js
  -rw-r--r--  1 simon staff   45 Jun 20 17:29 component.js
  -rw-r--r--  1 simon staff   58 Jun 20 17:30 create-context.js
  -rw-r--r--  1 simon staff  373 Jun 20 17:28 create-element.js
  -rw-r--r--  1 simon staff 8.2K Jun 20 17:21 index.d.ts
  -rw-r--r--  1 simon staff  391 Jun 20 17:28 index.js
  -rw-r--r--  1 simon staff 4.9K Jun 20 17:32 internal.d.ts
  -rw-r--r--  1 simon staff  31K Jun 20 17:21 jsx.d.ts
  -rw-r--r--  1 simon staff  587 Jun 20 17:32 options.js
  -rw-r--r--  1 simon staff  103 Jun 20 17:27 render.js

重点代码:

  1. diff 节点比较算法核心代码

  2. component.js Component 组件类

  3. render.js render 函数

基本使用

官方的栗子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { h, render } from 'preact';
// Tells babel to use h for JSX. It's better to configure this globally.
// See https://babeljs.io/docs/en/babel-plugin-transform-react-jsx#usage
// In tsconfig you can specify this with the jsxFactory
/** @jsx h */

// create our tree and append it to document.body:
render(<main><h1>Hello</h1></main>, document.body);

// update the tree in-place:
render(<main><h1>Hello World!</h1></main>, document.body);
// ^ this second invocation of render(...) will use a single DOM call to update the text of the <h1>

函数组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import {  render, h } from 'preact'
import { useState } from 'preact/hooks'

/** @jsx h*/
const App = () => {
  const [input, setInput] = useState('')
  return (
    <div>
      <p>some thing....</p>
      <input value={input} onChange={e => setInput(e.target.value)}/>
    </div>
  )
}

render(<App/>, document.body)

所以,首先需要实现的是 h/createElementrender, 前者构造 VNode 树,后者利 用 VNode tree 实施渲染,加入 DOM 。

h/createElement 函数

feat: h -> createElement · gcclll/stb-preact@efb88ce

src/create-element.js 内容:

name参数brief
createElementtype, props, childrenh 函数,构造 VNode
createVNodetype,props,key,ref,originalcreateElement调用
createRef-{current: null}
Fragmentprops-
isValidElementvnode-

createElement(type, props, children):

  1. props 处理,过滤出 key,ref 属性,这两个非元素原生属性

  2. 检测 children 合并成数组,当作改节点的子节点

  3. 如果 type 是个函数,考虑是否有初始化默认的 props

  4. 最后调用 createVNode 构造虚拟节点

 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
export function createElement(type, props, children) {
	let normalizedProps = {},
		key,
		ref,
		i;

	for (i in props) {
		if (i == 'key') key = props[i];
		else if (i == 'ref') ref = props[i];
		else normalizedProps[i] = props[i];
	}

	// 有 children 事的处理,
	// h('div', { ref: 'xxx' }, children[])
	// h('div', { ref: 'xxx' }, child1, child2, child3, ...)
	if (arguments.length > 2) {
		normalizedProps.children =
			arguments.length > 3 ? slice.call(arguments, 2) : children;
	}

	// 函数组件?
	// If a Component VNode, check for and apply defaultProps
	// Note: type may be undefined in development, must never error here.
	if (typeof type === 'function' && type.defaultProps != null) {
		for (i in type.defaultProps) {
			if (normalizedProps[i] === undefined) {
				normalizedProps[i] = type.defaultProps[i];
			}
		}
	}

	return createVNode(type, normalizedProps, key, ref, null);
}

createVNode(type, props, key, ref, original):

单纯初始化虚拟节点的结构。

  1. _nextDom 用来链接下一个被渲染的节点

    这跟 react 的 fiber 貌似有点关联,不知道这里有没用到 fiber. 根据 fiber 结构原 理,查找下一个执行单元的优先级是: first child -> sibling -> parent sibling

  2. constructor 这里赋值为 undefined 目的是为了识别 VNode(vue 的 __v_isVNode ???) ?

 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
// 构造虚拟节点
export function createVNode(type, props, key, ref, original) {
	const vnode = {
		type,
		props,
		key,
		ref,
		_children: null,
		_parent: null,
		_depth: 0,
		_dom: null,
		// 必须初始化成 `undefined`, 最终回被设置成 dom.nextSibling 的值
		// react fiber 结构查找优先级: first child -> sibling -> parent sibling
		_nextDom: undefined,
		_component: null,
		_hydrating: null,
		constructor: undefined, // 用来检测是不是有效的元素?
		_original: original == null ? ++vnodeId : original
	};

	// ??? 可以加工处理???
	if (options.vnode != null) options.vnode(vnode);

	return vnode;
}

最后有个 if (options.vnode != null) options.vnode(vnode); 判断,这个不知道是不 是提供给开发者对 vnode 进行加工处理的能力?

1
2
const react = require(process.env.HOME + '/github/react/stb-preact/dist/preact.js');
console.log(react);
{
  render: [Function (anonymous)],
  hydrate: [Function (anonymous)],
  createElement: [Function: u],
  h: [Function: u],
  Fragment: [Function (anonymous)],
  createRef: [Function (anonymous)],
  isValidElement: [Function: o],
  Component: [Function (anonymous)],
  cloneElement: [Function (anonymous)],
  createContext: [Function (anonymous)],
  toChildArray: [Function (anonymous)],
  options: { __e: [Function: __e] }
}
undefined

虚拟节点:

1
2
3
const { h } = require(process.env.HOME + '/github/react/stb-preact/dist/preact.js');
console.log(">>> h('div', { key: 1 })");
console.log(h('div', { key: 1 }));
>>> h('div', { key: 1 })
{
  type: 'div',
  props: {},
  key: 1,
  ref: undefined,
  __k: null,
  __: null,
  __b: 0,
  __e: null,
  __d: undefined,
  __c: null,
  __h: null,
  constructor: undefined,
  __v: 1
}
undefined

上面输出结果,被别名化了,这跟 preact 用的打包方式有关系,对应关系在 mangle.json 中。

keyvalue
__k_children
___list
__b_depth
__e_force
__d_nextDom
__c_cleanup
__h_pendingEffects
__v_original

render()

render(vnode, parentDom, replaceNode)

feat: render · gcclll/stb-preact@dcd5b41

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function render(vnode, parentDom, replaceNode) {
	if (options._root) options._root(vnode, parentDom);

	let isHydrating = typeof replaceNode === 'function';

	// 为了支持在同一个 DOM node 上多次调用 `render()`, 那么就需要有个
	// 引用能之前上一次渲染的树结构。默认没有该属性,同时也代表是该树第一次
	// 加载
	let oldVNode = isHydrating
		? null
		: (replaceNode && replaceNode._children) || parentDom._children;

	// 让父节点持有子节点引用
	vnode = (
		(!isHydrating && replaceNode) ||
		parentDom
	)._children = createElement(Fragment, null, [vnode]);

	// 1. diff()

	// 2. commitRoot(commitQueue, vnode)
}

Fragment 实现:

1
2
3
export function Fragment(props) {
	return props.children;
}

很奇怪吗?

createElement(Fragment, null, [vnode]) 等价于

createElement(props.children, null, [vnode])

Fragment 是个函数,而 createElement 对 type 的判断为函数时的处理是对 default props 的检测和合并操作。

根据 函数组件处理 可知这个处理会在 commit 阶段完成,先不管这个。

继续往下,将涉及到两个核心内容:

  1. diff, 进行对比更新 VNode

  2. work unit commit, 提交渲染任务

两个都是重点且核心的内容。

Component

在阅读 diff 源码之前,先看下 Component 是如何实现的,里面又包含哪些内容?

diff()

feat: diff init · gcclll/stb-preact@0ec1eaa

 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
function render() {
  // 1. create element -> vnode

  // 2. diff
  // List of effects that need to be called after diffing.
	let commitQueue = [];
	diff(
		parentDom,
		// Determine the new vnode tree and store it on the DOM element on
		// our custom `_children` property.
		vnode, // new vnode
		oldVNode || EMPTY_OBJ, // old vnode
		EMPTY_OBJ,
		parentDom.ownerSVGElement !== undefined,
		!isHydrating && replaceNode
			? [replaceNode]
			: oldVNode
			? null
			: parentDom.firstChild
			? slice.call(parentDom.childNodes)
			: null,
		commitQueue,
		!isHydrating && replaceNode
			? replaceNode
			: oldVNode
			? oldVNode._dom
			: parentDom.firstChild,
		isHydrating
	);

  // 3. commit
}

diff 函数目的:比较 vnode 更新,渲染更新后的节点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export function diff(
	parentDom,
	newVNode,
	oldVNode,
	globalContext,
	isSvg,
	excessDomChildren,
	commitQueue,
	oldDom,
	isHydrating
) { /*...*/ }

疑难点

TODO 为何用 Fragment 将 vnode 包一层?