歡迎關注個人公衆號睿Talk
,獲取我最新的文章:javascript
目前最流行的兩大前端框架,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
事件的綁定通常是定義在元素或者組件的屬性當中,以前對屬性的初始化和更新沒有考慮支持事件,只是簡單的賦值操做。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就會隨之立刻更新。瀏覽器
用過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
操做,渲染一次。
本文基於上一個版本的代碼,加入了事件處理功能,同時經過異步刷新的方法提升了渲染效率。
這是VD系列的最後一篇文章。本系列從什麼是Virtual Dom
這個問題出發,講解了VD的數據結構、比較方式和更新流程,並在此基礎上進行功能擴展和性能優化,支持key元素複用、自定義組件,dom事件綁定和setState異步更新。總共三百多行代碼,實現了mvvm庫的核心功能。
有關VD,若是還有什麼想了解的,歡迎留言,有問必答。
P.S.: 想看完整代碼見這裏,若是有必要建一個倉庫的話請留言給我:代碼