preact是目前最小的react兼容库了,因此学习它对提升anujs有很大的帮助。
preact的一些模块非常简单。
//vnode.jsexport function VNode() {}
一句话一个模块,其实这个在preact-compat 会被扩展原型。
//util.js//糅杂,相当于es6的Object.assignexport 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.jsexport 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;//forceUpdateexport 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.jsimport { 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 ; * } * } */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 这样的开差,用来调济更新的频率。