preact是目前最小的react兼容库了,因此学习它对提升anujs有很大的帮助。
preact的一些模块非常简单。
//vnode.js
export function VNode() {}
一句话一个模块,其实这个在preact-compat 会被扩展原型。
//util.js
//糅杂,相当于es6的Object.assign
export function extend(obj, props) {
for (let i in props) obj[i] = props[i];
return obj;
}
//用于异步执行一个函数,Promise比setTimeout的执行间隔太短
export const defer = typeof Promise=='function' ? Promise.resolve().then.bind(Promise.resolve()) : setTimeout;
有关异步的内容可以看我的书《javascript框架设计》,这里有详细介绍。这其实也涉及到microtask, macrotask的概念,有兴趣的人可以搜索一下。
preact的工具模块是我见过的库中最精简的。
//options.js
export default {
// 用于同步刷新组件
//syncComponentUpdates: true,
// 用于扩展VNode实例
//vnode(vnode) { }
// 在组件插入DOM时调用,不同于componentDidMount,它是专门给框架或组件内部使用,比如说chrome debug tools这样的工具进行扩展
// afterMount(component) { }
// 同上,内置的后门
// afterUpdate(component) { }
// 同上,内置的后门
// beforeUnmount(component) { }
};
options这个模块是用于扩展preact的功能,从而兼容官方react。
// constants.js
// 各种渲染模式
export const NO_RENDER = 0; //不渲染
export const SYNC_RENDER = 1;//React.render就是同步
export const FORCE_RENDER = 2;//forceUpdate
export const ASYNC_RENDER = 3;//组件的更新是异步
export const ATTR_KEY = '__preactattr_';//在节点中添加的属性
//用于识别那些样式不用自动添加px的正则
export const IS_NON_DIMENSIONAL = /acit|ex(?:s|g|n|p|$)|rph|ows|mnc|ntw|ine[ch]|zoo|^ord/i;
下面是h.js,其实就是React.createElement,这里做了一个不同于react的操作,就是立即将children扁平化,并且在扁平化过程成进行hydrate操作。hydrate是最早出现于inferno(另一个著名的react-like框架),并相邻的简单数据类型合并成一个字符串。因为在react的虚拟DOM体系中,字符串相当于一个文本节点。减少children中的个数,就相当减少实际生成的文本节点的数量,也减少了以后diff的数量,能有效提高性能。
// h.js
import { VNode } from './vnode';
import options from './options';
const stack = [];
const EMPTY_CHILDREN = [];
/**
* nodeName相当于react的type
* attributes相当于react的props
* 这是preact早期设计不周,这个标新立异导致它在兼容官方react要走许多弯路
*/
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;
//对最终生成的虚拟DOM进行扩展
if (options.vnode!==undefined) options.vnode(p);
return p;
}
属性 | react | preact |
---|---|---|
类别 | type | nodeName |
属性包 | props | attributes |
孩子 | props.children | children |
数组追踪用的trace by属性 | key | key |
cloneElement与createElement是一对的,cloneElement是基于createElement实现
import { extend } from './util';
import { h } from './h';
export function cloneElement(vnode, props) {
return h(
vnode.nodeName,
extend(extend({}, vnode.attributes), props),
arguments.length>2 ? [].slice.call(arguments, 2) : vnode.children
);
}
React.Component的实现
import { FORCE_RENDER } from './constants';
import { extend } from './util';
import { renderComponent } from './vdom/component';
import { enqueueRender } from './render-queue';
/** Base Component class.
* Provides `setState()` and `forceUpdate()`, which trigger rendering.
* @public
*
* @example
* class MyFoo extends Component {
* render(props, state) {
* return <div />;
* }
* }
*/
export function Component(props, context) {
//只有在_dirty为true时才能更新组件
this._dirty = true;
this.context = context;
this.props = props;
this.state = this.state || {};
}
extend(Component.prototype, {
/**
* 立即对state进行合并,而官方react是将state先放到一个数组中
*/
setState(state, callback) {
let s = this.state;
if (!this.prevState) this.prevState = extend({}, s);
extend(s, typeof state==='function' ? state(s, this.props) : state);
if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
enqueueRender(this);
},
//强制渲染,注意它与setState的实现是不一样的
forceUpdate(callback) {
if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
renderComponent(this, FORCE_RENDER);
},
//将方法要求返回虚拟DOM或null
render() {}
});
Component依赖两个方法enqueueRender与renderComponent,一个是异步的,一个是同步的。enqueueRender则是基于renderComponent上构建的。
我们看render-queue.js,这模块名与里面的方法名对应不一致,算是一个瑕疵。
import options from './options';
import { defer } from './util';
import { renderComponent } from './vdom/component';
let items = [];
//用于延迟渲染当前组件(setState)
export function enqueueRender(component) {
if (!component._dirty && (component._dirty = true) && items.push(component)==1) {
(options.debounceRendering || defer)(rerender);
}
}
export function rerender() {
let p, list = items;
items = [];
while ( (p = list.pop()) ) {
if (p._dirty) renderComponent(p);
}
}
到这里,比较简单的模块已经介绍完了。render.js?这个模块其实放到vdom文件夹比较合适。读preact的源码,其实可以给我们带来许多启迪,原来组件的渲染是有许多种模式的。这是一个要点。如何每次setState都是同步更新,这性能肯定好差,而异步则要求怎么更新才是最适合。于是有了enqueueRender这样的函数。下一节,我们还会看到_disabled 这样的开差,用来调济更新的频率。