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 这样的开差,用来调济更新的频率。