你不知道的Virtual DOM(六):事件處理&異步更新

歡迎關注個人公衆號睿Talk,獲取我最新的文章:
clipboard.pngjavascript

1、前言

目前最流行的兩大前端框架,React和Vue,都不約而同的藉助Virtual DOM技術提升頁面的渲染效率。那麼,什麼是Virtual DOM?它是經過什麼方式去提高頁面渲染效率的呢?本系列文章會詳細講解Virtual DOM的建立過程,並實現一個簡單的Diff算法來更新頁面。本文的內容脫離於任何的前端框架,只講最純粹的Virtual DOM。敲單詞太累了,下文Virtual DOM一概用VD表示。前端

這是VD系列文章的第六篇,如下是本系列其它文章的傳送門:
你不知道的Virtual DOM(一):Virtual Dom介紹
你不知道的Virtual DOM(二):Virtual Dom的更新
你不知道的Virtual DOM(三):Virtual Dom更新優化
你不知道的Virtual DOM(四):key的做用
你不知道的Virtual DOM(五):自定義組件
你不知道的Virtual DOM(六):事件處理&異步更新java

今天,咱們繼續在以前項目的基礎上擴展功能。在上一篇文章中,介紹了自定義組件的渲染和更新的實現方法。爲了驗證setState是否生效,還定義了一個setTimeout方法,5秒後更新state。在現實的項目中,state的改變每每是經過事件觸發的,如點擊事件、鍵盤事件和滾動事件等。下面,咱們就將事件處理加入到項目當中。git

2、實現事件處理

事件的綁定通常是定義在元素或者組件的屬性當中,以前對屬性的初始化和更新沒有考慮支持事件,只是簡單的賦值操做。github

// 屬性賦值
function setProps(element, props) {
     // 屬性賦值
    element[ATTR_KEY] = props;

    for (let key in props) {
        element.setAttribute(key, props[key]);
    }
}

// 比較props的變化
function diffProps(newVDom, element) {
    let newProps = {...element[ATTR_KEY]};
    const allProps = {...newProps, ...newVDom.props};

    // 獲取新舊全部屬性名後,再逐一判斷新舊屬性值
    Object.keys(allProps).forEach((key) => {
        const oldValue = newProps[key];
        const newValue = newVDom.props[key];

        // 刪除屬性
        if (newValue == undefined) {
            element.removeAttribute(key);
            delete newProps[key];
        } 
        // 更新屬性
        else if (oldValue == undefined || oldValue !== newValue) {
            element.setAttribute(key, newValue);
            newProps[key] = newValue;
        }
    }
)

    // 屬性從新賦值
    element[ATTR_KEY] = newProps;
}

setProps是在建立元素的時候調用的,而diffProps則是在diff過程當中調用的。若是須要支持事件綁定,咱們須要多作一個判斷。若是屬性名稱是on開頭的話,好比onClick,咱們就要在當前元素上註冊或刪除一個事件處理。算法

// 屬性賦值
function setProps(element, props) {
     // 屬性賦值
    element[ATTR_KEY] = props;

    for (let key in props) {
        // on開頭的屬性看成事件處理
        if (key.substring(0, 2) == 'on') {
            const evtName = key.substring(2).toLowerCase();
            element.addEventListener(evtName, evtProxy);
            (element._evtListeners || (element._evtListeners = {}))[evtName] = props[key];
        } else {
            element.setAttribute(key, props[key]);
        }
    }
}

function evtProxy(evt) {
    this._evtListeners[evt.type](evt);
}

// 比較props的變化
function diffProps(newVDom, element) {
    let newProps = {...element[ATTR_KEY]};
    const allProps = {...newProps, ...newVDom.props};

    // 獲取新舊全部屬性名後,再逐一判斷新舊屬性值
    Object.keys(allProps).forEach((key) => {
        const oldValue = newProps[key];
        const newValue = newVDom.props[key];

        // on開頭的屬性看成事件處理
        if (key.substring(0, 2) == 'on') {
            const evtName = key.substring(2).toLowerCase();
            if (newValue) {
                element.addEventListener(evtName, evtProxy);
            } else {
                element.removeEventListener(evtName, evtProxy);
            }
            (element._evtListeners || (element._evtListeners = {}))[evtName] = newValue;
        } else {
            // 刪除屬性
            if (newValue == undefined) {
                element.removeAttribute(key);
                delete newProps[key];
            } 
            // 更新屬性
            else if (oldValue == undefined || oldValue !== newValue) {
                element.setAttribute(key, newValue);
                newProps[key] = newValue;
            }
        }
    }
)

    // 屬性從新賦值
    element[ATTR_KEY] = newProps;
}

全部的事件處理函數都存到dom元素的_evtListeners當中,當事件觸發的時候,將事件傳給裏面對應的方法處理。這樣作的好處是若是之後要對瀏覽器傳入的事件evt作進一步的封裝,就能夠在evtProxy函數裏面處理。segmentfault

接下來,咱們在自定義組件裏面新增一個onClick事件,在點擊的時候改變state裏面的值。數組

class MyComp extends Component {
    constructor(props) {
        super(props);
        this.state = {
            name: 'Tina',
            count: 1
        }
    }

    elmClick() {
        this.setState({name: `Jack${this.state.count}`, count: this.state.count + 1 });
    }

    render() {
        return(
            <div id="myComp" onClick={this.elmClick.bind(this)}>
                <div>This is My Component! {this.props.count}</div>
                <div>name: {this.state.name}</div>
            </div>
        )
    }
}

項目運行的效果是每當我點一下MyComp組件的區域,裏面的name就會隨之立刻更新。瀏覽器

clipboard.png

3、setState異步更新

用過React的朋友都知道,爲了減小沒必要要的渲染,提升性能,React並非在咱們每次setState的時候都進行渲染,而是將一個同步操做裏面的多個setState進行合併後再渲染,給人異步渲染的感受。看過源碼的都應該知道,React是經過事務的方式來合併多個setState操做的,本質來講仍是同步的。若是想對其做更深刻的學習,推薦看這篇文章性能優化

爲了達到合併操做,減小渲染的效果,最簡單的方式就是異步渲染,下面咱們來看看如何實現。在上一個版本里,setState是這麼定義的:

class Component {
    ...
        
    setState(newState) {
        this.state = {...this.state, ...newState};
        const vdom = this.render();
        diff(this.dom, vdom, this.parent);
    }

    ...
};

state更新後直接就進行diff操做,進而更新頁面。若是咱們onClick裏面的代碼改爲這樣:

elmClick() {
        this.setState({name: `Jack${this.state.count}`, count: this.state.count + 1 });
        this.setState({name: `Jack${this.state.count}`, count: this.state.count + 1 });
    }

頁面會渲染2次。若是咱們把它改形成下面的樣子:

// 等待渲染的組件數組
let pendingRenderComponents = [];

class Component {
    ...
        
    setState(newState) {
        this.state = {...this.state, ...newState};
        enqueueRender(this);
    }

    ...
};

function enqueueRender(component) {
    // 若是push後數組長度爲1,則將異步刷新任務加入到事件循環當中
    if (pendingRenderComponents.push(component) == 1) {
        if (typeof Promise=='function') {
            Promise.resolve().then(renderComponent);
        } else {
            setTimeout(renderComponent, 0);
        }
    }
}

function renderComponent() {
    // 組件去重
    const uniquePendingRenderComponents = [...new Set(pendingRenderComponents)];

    // 渲染組件
    uniquePendingRenderComponents.forEach(component => {
        const vdom = component.render();
        diff(component.dom, vdom, component.parent);
    });

    // 清空待渲染列表
    pendingRenderComponents = [];
}

當第一次setState成功後,並不會立刻進行渲染,而是將組件存入待渲染組件列表當中。若是列表是空的,則存入組件後將異步刷新任務加入到事件循環當中。當運行環境支持Promise時,經過微任務運行,不然經過宏任務運行。微任務的運行時間是當前事件循環的末尾,而宏任務的運行時間是下一個事件循環。因此優先使用微任務。

緊接着進行第二次setState操做,一樣的,將組件存入待渲染組件列表當中。此時,主線程的任務執行完了,開始執行異步任務。

當異步刷新任務啓動時,將待渲染列表去重後對裏面的組件進行渲染。等渲染完成後再清空待渲染列表。此時,渲染出來的是2次setState合併後的結果,而且只會進行一次diff操做,渲染一次。

4、總結

本文基於上一個版本的代碼,加入了事件處理功能,同時經過異步刷新的方法提升了渲染效率。

這是VD系列的最後一篇文章。本系列從什麼是Virtual Dom這個問題出發,講解了VD的數據結構、比較方式和更新流程,並在此基礎上進行功能擴展和性能優化,支持key元素複用、自定義組件,dom事件綁定和setState異步更新。總共三百多行代碼,實現了mvvm庫的核心功能。

有關VD,若是還有什麼想了解的,歡迎留言,有問必答。

P.S.: 想看完整代碼見這裏,若是有必要建一個倉庫的話請留言給我:代碼

相關文章
相關標籤/搜索