首先咱們有必要介紹一下etch。 css
etch是atom團隊下的開源項目,是一套很是簡潔然而功能十分完善的virtualDOM機制。我在偶然的狀況下接觸到了這個開源項目,在讀README時爲它簡潔的設計而驚歎,而在閱讀源碼的過程當中也爲它巧妙的實現而讚歎。 node
我的以爲etch針對是一個很是好的學習內容,實際代碼才七百來行,邏輯極度清晰,很適合做爲想了解vdom的人的入門項目。
etch項目地址react
我將我的對etch源碼的實踐和理解寫成了一個項目,地址爲源碼解讀地址 git
我的建議是直接去我這個項目看,我在項目中整理的總體的流程,也對具體的代碼添加的筆記,應該很好懂,不過,若是你只是想簡單瞭解一下,那麼能夠繼續看這篇文章。 github
首先咱們看一下項目的文件結構
express
正常來講咱們應該從index.js開始看,可是index.js只是負責將函數彙總了一下,因此咱們從真正的開始——component-helpers文件的initialize函數開始。 segmentfault
這個函數負責以一個component實例爲參數(具體表現形式爲在一個component的constructor中調用,參數爲this。
舉個栗子數組
/** @jsx etch.dom */ const etch = require('etch') class MyComponent { // Required: Define an ordinary constructor to initialize your component. constructor (props, children) { // perform custom initialization here... // then call `etch.initialize`: etch.initialize(this) } // Required: The `render` method returns a virtual DOM tree representing the // current state of the component. Etch will call `render` to build and update // the component's associated DOM element. Babel is instructed to call the // `etch.dom` helper in compiled JSX expressions by the `@jsx` pragma above. render () { return <div></div> } // Required: Update the component with new properties and children. update (props, children) { // perform custom update logic here... // then call `etch.update`, which is async and returns a promise return etch.update(this) } // Optional: Destroy the component. Async/await syntax is pretty but optional. async destroy () { // call etch.destroy to remove the element and destroy child components await etch.destroy(this) // then perform custom teardown logic here... } }
上面就是一個很是標準的etch組件,在constructor中使用etch.initialize就保證了當一個組件被實例化的時候必然會調用initialize而後完成必要的初始化)。接下來咱們深刻initialize函數,看看它幹了什麼。promise
function initialize(component) { if (typeof component.update !== 'function') { throw new Error('Etch components must implement `update(props, children)`.') } let virtualNode = component.render() if (!isValidVirtualNode(virtualNode)) { let namePart = component.constructor && component.constructor.name ? ' in ' + component.constructor.name : '' throw new Error('invalid falsy value ' + virtualNode + ' returned from render()' + namePart) } applyContext(component, virtualNode) component.refs = {} component.virtualNode = virtualNode component.element = render(component.virtualNode, { refs: component.refs, listenerContext: component }) }
咱們能夠清楚的看到initialize乾的很是簡單——調用component實例的render函數返回jsx轉成的virtualNode,而後調用render將virtualNode轉化爲DOM元素,最後將virtualNode和DOM元素都掛載在component上。在咱們寫的代碼裏,咱們會手動將DOM元素掛載到dom樹上。babel
接下來咱們分兩條線看,一條是jsx如何如何變成virtualNode。很簡單,babel轉碼器,react就是用的這個。然而transform-react-jsx插件的默認入口是React.createElement,這裏須要咱們配置一下,將其改爲etch.dom。(入口的意思是jsx轉碼後的東西應該傳到哪裏)。
如下是.babelrc配置文件內容 { "presets": ["env"], "plugins": [ ["transform-react-jsx", { "pragma": "etch.dom" // default pragma is React.createElement }],"transform-object-rest-spread","transform-regenerator" ] }
dom文件下的dom函數所作的就是將傳入的參數進行處理,而後返回一個貨真價實的virtualNode,具體實現以下
function dom (tag, props, ...children) { let ambiguous = [] //這裏其實就是我以前在bl寫的flatternChildren,做用就是對children進行一些處理,將數組或者是字符串轉化爲真正的vnode for (let i = 0; i < children.length;) { const child = children[i] switch (typeof child) { case 'string': case 'number': children[i] = {text: child} i++ break; case 'object': if (Array.isArray(child)) { children.splice(i, 1, ...child) } else if (!child) { children.splice(i, 1) } else { if (!child.context) { ambiguous.push(child) if (child.ambiguous && child.ambiguous.length) { ambiguous = ambiguous.concat(child.ambiguous) } } i++ } break; default: throw new Error(`Invalid child node: ${child}`) } } //對於props進行處理,props包括全部在jsx上的屬性 if (props) { for (const propName in props) { const eventName = EVENT_LISTENER_PROPS[propName] //處理事件掛載 if (eventName) { if (!props.on) props.on = {} props.on[eventName] = props[propName] } } //處理css類掛載 if (props.class) { props.className = props.class } } return {tag, props, children, ambiguous} }
到此,咱們應該明白了,當咱們碰到一個jsx時候,咱們實際收到的是一個通過dom函數處理過的virtualNode(沒錯,我說的就是每一個component的render返回的東西,另外所謂virtualNode說到底就是一個擁有特定屬性的對象)。
接下來咱們看另外一條線,那就是render如何將virtualNode轉化爲一個真正的DOM元素。
unction render (virtualNode, options) { let domNode if (virtualNode.text != null) { domNode = document.createTextNode(virtualNode.text) } else { const {tag, children} = virtualNode let {props, context} = virtualNode if (context) { options = {refs: context.refs, listenerContext: context} } if (typeof tag === 'function') { let ref if (props && props.ref) { ref = props.ref } const component = new tag(props || {}, children) virtualNode.component = component domNode = component.element // console.log(domNode,"!!!",virtualNode) if (typeof ref === "function") { ref(component) } else if (options && options.refs && ref) { options.refs[ref] = component } } else if (SVG_TAGS.has(tag)) { domNode = document.createElementNS("http://www.w3.org/2000/svg", tag); if (children) addChildren(domNode, children, options) if (props) updateProps(domNode, null, virtualNode, options) } else { domNode = document.createElement(tag) if (children) addChildren(domNode, children, options) if (props) updateProps(domNode, null, virtualNode, options) } } virtualNode.domNode = domNode return domNode }
其實很簡單,經過對virtualNode的tag進行判斷,咱們能夠輕易的判斷virtualNode是什麼類型的(好比組件,好比基本元素,好比字符元素),而後針對不一樣的類型進行處理(基本的好說),組件的話,要再走一遍組件的建立和掛載流程。若爲基礎元素,則咱們能夠將對應的屬性放到DOM元素上,最後返回建立好的DOM元素(其實virtualNode上的全部元素基本最後都是要反映到基礎DOM元素上的,多是屬性,多是子元素)。
到這裏,咱們已經完成了DOM元素掛載的全過程,接下來咱們看一看更新的時候會發生什麼。
更新的話,咱們會在本身寫的update函數中調用component-helpers的update函數(後面咱們叫它etch.update),而etch.update和initialize同樣會以component實例做爲參數,具體來講就是組件class中的this。而後在etch.update中會以異步的形式來進行更新,這樣能夠保證避免更新冗餘,極大的提高性能
function update (component, replaceNode=true) { if (syncUpdatesInProgressCounter > 0) { updateSync(component, replaceNode) return Promise.resolve() } //這是一個能夠完成異步的機制 let scheduler = getScheduler() //經過這個判斷保證了再一次DOM實質性更新完成以前不會再次觸發 if (!componentsWithPendingUpdates.has(component)) { componentsWithPendingUpdates.add(component) scheduler.updateDocument(function () { componentsWithPendingUpdates.delete(component) //而根據這個咱們能夠很清楚的發現真正的更新仍是靠同步版update updateSync(component, replaceNode) }) } return scheduler.getNextUpdatePromise() }
。可是etch.update真正進行更新的部分倒是在etch.updateSync。看函數名咱們就知道這是這是一個更新的同步版。這個函數會讓component實時更新,而etch.update其實是以異步的形式調用的這個同步版。
接下來咱們深刻etch.updateSync來看看它究竟是怎麼作的。
function updateSync (component, replaceNode=true) { if (!isValidVirtualNode(component.virtualNode)) { throw new Error(`${component.constructor ? component.constructor.name + ' instance' : component} is not associated with a valid virtualNode. Perhaps this component was never initialized?`) } if (component.element == null) { throw new Error(`${component.constructor ? component.constructor.name + ' instance' : component} is not associated with a DOM element. Perhaps this component was never initialized?`) } let newVirtualNode = component.render() if (!isValidVirtualNode(newVirtualNode)) { const namePart = component.constructor && component.constructor.name ? ' in ' + component.constructor.name : '' throw new Error('invalid falsy value ' + newVirtualNode + ' returned from render()' + namePart) } applyContext(component, newVirtualNode) syncUpdatesInProgressCounter++ let oldVirtualNode = component.virtualNode let oldDomNode = component.element let newDomNode = patch(oldVirtualNode, newVirtualNode, { refs: component.refs, listenerContext: component }) component.virtualNode = newVirtualNode if (newDomNode !== oldDomNode && !replaceNode) { throw new Error('The root node type changed on update, but the update was performed with the replaceNode option set to false') } else { component.element = newDomNode } // We can safely perform additional writes after a DOM update synchronously, // but any reads need to be deferred until all writes are completed to avoid // DOM thrashing. Requested reads occur at the end of the the current frame // if this method was invoked via the scheduler. Otherwise, if `updateSync` // was invoked outside of the scheduler, the default scheduler will defer // reads until the next animation frame. if (typeof component.writeAfterUpdate === 'function') { component.writeAfterUpdate() } if (typeof component.readAfterUpdate === 'function') { getScheduler().readDocument(function () { component.readAfterUpdate() }) } syncUpdatesInProgressCounter-- }
事實上因爲scheduler的騷操做,在調用updateSync以前實質性的更新已經所有調用,而後咱們要作的就是調用component.render獲取新的virtualNode,而後經過patch函數根據新舊virtualNode判斷哪些部分須要更新,而後對DOM進行更新,最後處理生命週期函數,完美。
那麼scheduler的騷操做究竟是什麼呢?其實就是靠requestAnimationFrame保證全部的更新都在同一幀內解決。另外經過weakSet機制,能夠保證一個組件在它完成本身的實質性更新以前毫不會再重繪(這裏是說數據會更新,但不會反映到實際的DOM元素上,這就很完美的作到了避免冗餘的更新)
最後咱們看一看組件的卸載和銷燬部分。這部分應該是destroy負責的,咱們要在組件的destory方法中調用etch.destory。要說一下,etch.destory和etch.update同樣是異步函數.而後咱們能夠根據update很輕鬆的猜出必定含有一個同步版的destroySync。沒錯,就是這樣,真正的卸載是在destroySync中完成的。邏輯也很簡單,組件上的destory會被調用,它的子組件上具備destory的也會被調用,這樣一直遞歸。最後從DOM樹上刪除掉component對應的DOM元素。
unction destroySync (component, removeNode=true) { syncDestructionsInProgressCounter++ destroyChildComponents(component.virtualNode) if (syncDestructionsInProgressCounter === 1 && removeNode) component.element.remove() syncDestructionsInProgressCounter-- } /** * 若爲組件直接摧毀,不然摧毀子元素中爲組件的部分 * @param {*} virtualNode */ function destroyChildComponents(virtualNode) { if (virtualNode.component && typeof virtualNode.component.destroy === 'function') { virtualNode.component.destroy() } else if (virtualNode.children) { virtualNode.children.forEach(destroyChildComponents) } }
到這裏咱們就走徹底部流程了。這就是一套etch virtualNode,很簡單,頗有趣,很巧妙。
整篇文章絮絮不休的,並且仍是源碼這種冷門的東西,估計沒什麼人願意看。不過我仍是想發上來,做爲本身的筆記,也但願能對他人有用。這篇文章是我在segmentfault上發的第一篇技術文章,生澀的很,我會努力進步。另外,我真的建議直接去我那個項目看筆記,應該比這篇文章清晰的多。
2018.4.11於學校