歡迎關注個人公衆號睿Talk
,獲取我最新的文章:
javascript
目前最流行的兩大前端框架,React和Vue,都不約而同的藉助Virtual DOM技術提升頁面的渲染效率。那麼,什麼是Virtual DOM?它是經過什麼方式去提高頁面渲染效率的呢?本系列文章會詳細講解Virtual DOM的建立過程,並實現一個簡單的Diff算法來更新頁面。本文的內容脫離於任何的前端框架,只講最純粹的Virtual DOM。敲單詞太累了,下文Virtual DOM一概用VD表示。html
這是VD系列文章的第五篇,如下是本系列其它文章的傳送門:
你不知道的Virtual DOM(一):Virtual Dom介紹
你不知道的Virtual DOM(二):Virtual Dom的更新
你不知道的Virtual DOM(三):Virtual Dom更新優化
你不知道的Virtual DOM(四):key的做用
你不知道的Virtual DOM(五):自定義組件
你不知道的Virtual DOM(六):事件處理&異步更新前端
今天,咱們繼續在以前項目的基礎上擴展功能。如今流行的前端框架都支持自定義組件,組件化開發已經成爲提升前端開發效率的銀彈。下面咱們就將自定義組件功能加到項目中去,目標是正確的渲染和更新自定義組件。java
要想正確的渲染組件,第一步就是要告訴JSX某個標籤是自定義組件。這個實現起來很簡單,只要標籤名的首字母大寫就能夠了。下面的例子裏,MyComp就是一個自定義組件。node
<div> <div>普通標籤</div> <MyComp></MyComp> </div>
通過JSX編譯後,是下面這個樣子。git
h( 'div', null, h( 'div', null, '\u666E\u901A\u6807\u7B7E' ), h(MyComp, null) );
當首字母大寫當時候,JSX會將標籤名看成變量處理,而不是像普通標籤同樣當字符串處理。解決了識別自定義標籤的問題,下一步就是定義標籤了。github
在React中,全部自定義組件都要繼承Component基類,它爲咱們提供了一系列生命週期方法和修改組件的方法。咱們也對應的定義一個本身的Component類:算法
class Component { constructor(props) { this.props = props; this.state = {}; } setState(newState) { this.state = {...this.state, ...newState}; const vdom = this.render(); diff(this.dom, vdom, this.parent); } render() { throw new Error('component should define its own render method') } };
若是用一句話描述Component,那就是屬性和狀態的UI表達
。咱們先不考慮生命週期函數,先定義一個最精簡版的Component。首先在初始化的時候,須要傳入props屬性,而後提供一個setState方法來改變組件的狀態,最後就是子類必需要實現的render
函數。若是子類沒有實現,就會沿着原型鏈查找到Component類,而後會拋出一個錯誤。segmentfault
有了Component基類後,咱們就能夠定義本身的組件了。咱們來定義一個最簡單的顯示屬性和狀態信息的組件。前端框架
class MyComp extends Component { constructor(props) { super(props); this.state = { name: 'Tina' } } render() { return( <div> <div>This is My Component! {this.props.count}</div> <div>name: {this.state.name}</div> </div> ) } }
定義好組件後,就要考慮渲染的邏輯了。
在對VD進行diff操做的時候,要對tag爲函數類型(自定義組件)的節點作特殊處理,同時對新建的節點,也要加入一些額外的邏輯。
function diff(dom, newVDom, parent, componentInst) { if (typeof newVDom == 'object' && typeof newVDom.tag == 'function') { buildComponentFromVDom(dom, newVDom, parent); return false; } // 新建node if (dom == undefined) { const dom = createElement(newVDom); // 自定義組件 if (componentInst) { dom._component = componentInst; dom._componentConstructor = componentInst.constructor; componentInst.dom = dom; } parent.appendChild(dom); return false; } ... } function buildComponentFromVDom(dom, vdom, parent) { const cpnt = vdom.tag; if (!typeof cpnt === 'function') { throw new Error('vdom is not a component type'); } const props = getVDomProps(vdom); let componentInst = dom && dom._component; // 建立組件 if (componentInst == undefined) { try { componentInst = new cpnt(props); setTimeout(() => {componentInst.setState({name: 'Dickens'})}, 5000); } catch (error) { throw new Error(`component creation error: ${cpnt.name}`); } } // 組件更新 else { componentInst.props = props; } const componentVDom = componentInst.render(); diff(dom, componentVDom, parent, componentInst); } function getVDomProps(vdom) { const props = vdom.props; props.children = vdom.children; return props; }
若是是自定義組件,會調用buildComponentFromVDom
方法。先經過getVDomProps
方法獲取vdom最新的屬性,包括children。若是dom對象有_component屬性,說明是組件更新的過程,不然爲組件建立的過程。若是是建立過程則直接實例化一個對象,setTimeout
部分主要爲了驗證setState能不能正常工做,能夠先忽略。若是是更新過程,則傳入最新的props。最後經過組件的render
方法獲得最新的vdom後,再進行diff操做。
diff多了一個componentInst
的參數,在新建dom節點的時候,若是有這個參數,說明是自定義組件建立的節點,須要用_component
和_componentConstructor
作一下標識。其中_component
上面就用到了,用來判斷是組件更新過程仍是組件建立過程。_componentConstructor
用在組件更新過程當中判斷組件的類型是否相同。
function isSameType(element, newVDom) { if (typeof newVDom.tag == 'function') { return element._componentConstructor == newVDom.tag; } ... }
到此爲止,自定義組件的被動更新過程已經完成了,下面來看看主動更新的邏輯。
setState
的邏輯很簡單,就是更新state後再render
一次,獲取到最新的vdom,再走一遍diff的過程。setState
的前提是組件已經實例化而且已經渲染出來了,this.dom
就是組件渲染出來的dom的頂級節點。
setState(newState) { this.state = {...this.state, ...newState}; const vdom = this.render(); diff(this.dom, vdom, this.parent); } function buildComponentFromVDom(dom, vdom, parent) { ... // 建立組件 if (componentInst == undefined) { ... setTimeout(() => {componentInst.setState({name: 'Dickens'})}, 5000); ... }
爲了驗證setState
可否按預期運行,在建立組件的時候咱們在5秒後更新一下state,看看名字可否正確更新。咱們的頁面是長這個樣子的:
function view() { const elm = arr.pop(); // 用於測試能不能正常刪除元素 if (state.num !== 9) arr.unshift(elm); // 用於測試能不能正常添加元素 if (state.num === 12) arr.push(9); return ( <div> Hello World <MyComp count={state.num}/> <ul myText="dickens"> { arr.map( i => ( <li id={i} class={`li-${i}`} key={i}> 第{i} </li> )) } </ul> </div> ); }
剛開始渲染出來是這個樣子:
5秒以後是這個樣子:
能夠看到props
和state
都獲得了正確都渲染。
本文基於上一個版本的代碼,加入了對自定義組件的支持,大大提升代碼的複用性。基於當前這個版本的代碼還能作怎樣的優化呢,請看下一篇的內容:你不知道的Virtual DOM(六):事件處理&異步更新。
P.S.: 想看完整代碼見這裏,若是有必要建一個倉庫的話請留言給我:代碼