preact源码学习(1)

更新于 2017-07-25  约 12 分钟

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

阅读 4.3k更新于 2017-07-25

推荐阅读
目录