preact源碼解讀(2)

前言

  • 這裏是第二篇,第一篇在這裏javascript

  • 此次講Component,以及它的一些輕量依賴。vue

  • 順便說下司徒正美的preact源碼學習java

  • 感受比我寫的好多了,圖文並茂,還能提出和其它如React的源碼比較。node

  • 我惟一好點的可能就是代碼幾乎每行都有註釋,而且使用了typescript添加了類型的標註。react

Component使用

import { h, Component, render } from "preact"

class App extends Component {
    constructor(props, context) {
        super(props, context)
        this.state = {
            num: 0
        }
    }
    test() {
        this.setState(state => {
            state.num += 1
        })
    }
    render(props, state, context) {
        return <h1 onClick={test.bind(this)}>{state.num}<h1/>
    }
}
render(<App/>, document.body)

上面是一個簡單的點擊改變當前狀態的組件示例。
其中與vue不一樣preact經過Component.prototype.setState來觸發新的dom改變。
固然preact還有其它的更新方式。typescript

Component代碼

這裏的代碼是經過typescript重寫過的因此有所不一樣,
可是更好的瞭解一個完整的Component總體應該有什麼。segmentfault

import { FORCE_RENDER } from "./constants";
import { renderComponent } from "./vdom/component";
import { VNode } from "./vnode";
import { enqueueRender } from "./render-queue";
import { extend } from "./util";
import { IKeyValue } from "./types";

export class Component {
    /**
     * 默認props
     */
    public static defaultProps?: IKeyValue;
    /**
     * 當前組件的狀態,能夠修改
     */
    public state: IKeyValue;
    /**
     * 由父級組件傳遞的狀態,不可修改
     */
    public props: IKeyValue;
    /**
     * 組件上下文,由父組件傳遞
     */
    public context: IKeyValue;
    /**
     * 組件掛載後的dom
     */
    public base?: Element;
    /**
     * 自定義組件名
     */
    public name?: string;
    /**
     * 上一次的屬性
     */
    public prevProps?: IKeyValue;
    /**
     * 上一次的狀態
     */
    public prevState?: IKeyValue;
    /**
     * 上一次的上下文
     */
    public prevContext?: IKeyValue;
    /**
     * 被移除時的dom緩存
     */
    public nextBase?: Element;
    /**
     * 在一個組件被渲染到 DOM 以前
     */
    public componentWillMount?: () => void;
    /**
     * 在一個組件被渲染到 DOM 以後
     */
    public componentDidMount?: () => void;
    /**
     * 在一個組件在 DOM 中被清除以前
     */
    public componentWillUnmount?: () => void;
    /**
     * 在新的 props 被接受以前
     * @param { IKeyValue } nextProps
     * @param { IKeyValue } nextContext
     */
    public componentWillReceiveProps?: (nextProps: IKeyValue, nextContext: IKeyValue) => void;
    /**
     * 在 render() 以前. 若返回 false,則跳過 render,與 componentWillUpdate 互斥
     * @param { IKeyValue } nextProps
     * @param { IKeyValue } nextState
     * @param { IKeyValue } nextContext
     * @returns { boolean }
     */
    public shouldComponentUpdate?: (nextProps: IKeyValue, nextState: IKeyValue, nextContext: IKeyValue) => boolean;
    /**
     * 在 render() 以前,與 shouldComponentUpdate 互斥
     * @param { IKeyValue } nextProps
     * @param { IKeyValue } nextState
     * @param { IKeyValue } nextContext
     */
    public componentWillUpdate?: (nextProps: IKeyValue, nextState: IKeyValue, nextContext: IKeyValue) => void;
    /**
     * 在 render() 以後
     * @param { IKeyValue } previousProps
     * @param { IKeyValue } previousState
     * @param { IKeyValue } previousContext
     */
    public componentDidUpdate?: (previousProps: IKeyValue, previousState: IKeyValue, previousContext: IKeyValue) => void;
    /**
     * 獲取上下文,會被傳遞到全部的子組件
     */
    public getChildContext?: () => IKeyValue;
    /**
     * 子組件
     */
    public _component?: Component;
    /**
     * 父組件
     */
    public _parentComponent?: Component;
    /**
     * 是否加入更新隊列
     */
    public _dirty: boolean;
    /**
     * render 執行完後的回調隊列
     */
    public _renderCallbacks?: any[];
    /**
     * 當前組件的key用於複用
     */
    public _key?: string;
    /**
     * 是否停用
     */
    public _disable?: boolean;
    /**
     * react標準用於設置component實例
     */
    public _ref?: (component: Component | null) => void;
    /**
     * VDom暫定用於存放組件根dom的上下文
     */
    public child?: any;
    constructor(props: IKeyValue, context: IKeyValue) {
        // 初始化爲true
        this._dirty = true;
        this.context = context;
        this.props = props;
        this.state = this.state || {};
    }
    /**
     * 設置state並經過enqueueRender異步更新dom
     * @param state 對象或方法
     * @param callback render執行完後的回調。
     */
    public setState(state: IKeyValue, callback?: () => void): void {
        const s: IKeyValue = this.state;
        if (!this.prevState) {
            // 把舊的狀態保存起來
            this.prevState = extend({}, s);
        }
        // 把新的state和併到this.state
        if (typeof state === "function") {
            const newState = state(s, this.props);
            if (newState) {
                extend(s, newState);
            }
        } else {
            extend(s, state);
        }
        if (callback) {
            // 添加回調
            this._renderCallbacks = this._renderCallbacks || [];
            this._renderCallbacks.push(callback);
        }
        // 異步隊列更新dom,經過enqueueRender方法能夠保證在一個任務棧下屢次setState可是隻會發生一次render
        enqueueRender(this);
    }
    /**
     * 手動的同步更新dom
     * @param callback 回調
     */
    public forceUpdate(callback: () => void) {
        if (callback) {
            this._renderCallbacks = this._renderCallbacks || [];
            this._renderCallbacks.push(callback);
        }
        // 從新同步執行render
        renderComponent(this, FORCE_RENDER);
    }
    /**
     * 用來生成VNode的函數
     * @param props
     * @param state
     * @param context
     */
    public render(props?: IKeyValue, state?: IKeyValue, context?: IKeyValue): VNode | void {
        // console.error("not set render");
    }
}

若是你看過原來的preact的代碼會發覺多了不少可選屬性,
其中除了child這個屬性其它實際上官方的也有,可是都是可選屬性。緩存

這裏重點說setStateforceUpdate這兩個觸發dom更新dom

setState保存舊的this.statethis.prevState裏,而後新的state是直接設置在this.state
而後經過enqueueRender來加入隊列中,這個更新是在異步中的。因此不要寫出這種代碼異步

test() {
    // 這裏的setState已經入異步棧,
    this.setState({...})
    $.post(...() => {
        // 再次入異步棧,再一次執行,
        this.setState({...})
    })
}

能夠把兩次setState合併到一塊兒作。

render-queue

import { Component } from "./component";
import options from "./options";
import { defer } from "./util";
import { renderComponent } from "./vdom/component";

let items: Component[] = [];

/**
 * 把Component放入隊列中等待更新
 * @param component 組件
 */
export function enqueueRender(component: Component) {
    if (!component._dirty) {
        // 防止屢次render
        component._dirty = true;
        const len = items.push(component);
        if (len === 1) {
            // 在第一次時添加一個異步render,保證同步代碼執行完只有一個異步render。
            const deferFun = options.debounceRendering || defer;
            deferFun(rerender);
        }
    }
}

/**
 * 根據Component隊列更新dom。
 * 能夠setState後直接執行這個方法強制同步更新dom
 */
export function rerender() {
    let p: Component | undefined;
    const list = items;
    items = [];
    while (p = list.pop()) {
        if (p._dirty) {
            // 防止屢次render。
            renderComponent(p);
        }
    }
}

最終經過renderComponent來從新diff更新dom

forceUpdate則是直接同步更新不過傳入了一個標記FORCE_RENDER

順便寫下options

import { VNode } from "./vnode";
import { Component } from "component";

const options: {
    // render更新後鉤子比componentDidUpdate更後面執行
    afterUpdate?: (component: Component) => void;
    // dom卸載載前鉤子比componentWillUnmount更先執行
    beforeUnmount?: (component: Component) => void;
    // dom掛載後鉤子比componentDidMount更先執行
    afterMount?: (component: Component) => void;
    // setComponentProps時強制爲同步render
    syncComponentUpdates?: boolean;
    // 自定義異步調度方法,會異步執行傳入的方法
    debounceRendering?: (render: () => void) => void;
    // vnode實例建立時的鉤子
    vnode?: (vnode: VNode) => void;
    // 事件鉤子,能夠對event過濾返回的會代替event參數
    event?: (event: Event) => any;
    // 是否自動對事件方法綁定this爲組件,默認爲true(preact沒有)
    eventBind?: boolean;
} = {
    eventBind: true,
};

export default options;

後記

  • 感受有了更多的註釋,就沒有必要說明太多了。

  • 下一篇應該是到了renderComponentdiff部分了。

  • 原文地址

相關文章
相關標籤/搜索