摘要:接下來我們深入函數,看看它干了什么。在我們寫的代碼里,我們會手動將元素掛載到樹上。到這里,我們已經完成了元素掛載的全過程,接下來我們看一看更新的時候會發生什么。這部分應該是負責的,我們要在組件的方法中調用。
etch簡介
首先我們有必要介紹一下etch。
etch是atom團隊下的開源項目,是一套非常簡潔然而功能十分完善的virtualDOM機制。我在偶然的情況下接觸到了這個開源項目,在讀README時為它簡潔的設計而驚嘆,而在閱讀源碼的過程中也為它巧妙的實現而贊嘆。
個人覺得etch針對是一個非常好的學習內容,實際代碼才七百來行,邏輯極度清晰,很適合作為想了解vdom的人的入門項目。
etch項目地址
我將個人對etch源碼的實踐和理解寫成了一個項目,地址為源碼解讀地址
個人建議是直接去我這個項目看,我在項目中整理的整體的流程,也對具體的代碼添加的筆記,應該很好懂,不過,如果你只是想簡單了解一下,那么可以繼續看這篇文章。
首先我們看一下項目的文件結構
正常來說我們應該從index.js開始看,但是index.js只是負責將函數匯總了一下,所以我們從真正的開始——component-helpers文件的initialize函數開始。
這個函數負責以一個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 } // 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函數,看看它干了什么。
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樹上。
接下來我們分兩條線看,一條是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于學校
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/94132.html
摘要:表示調用棧在下一將要執行的任務。兩方性能解藥我們一般有兩種方案突破上文提到的瓶頸將耗時高成本高易阻塞的長任務切片,分成子任務,并異步執行這樣一來,這些子任務會在不同的周期執行,進而主線程就可以在子任務間隙當中執行更新操作。 showImg(https://segmentfault.com/img/remote/1460000016008111); 性能一直以來是前端開發中非常重要的話題...
摘要:前言的基本概念組件的構建方法以及高級用法這背后的一切如何運轉深入內部的實現機制和原理初探源碼代碼組織結構包含一系列的工具方法插件包含一系列同構方法包含一些公用或常用方法如等包含一些測試方法等包含一些邊界錯誤的測試用例是代碼的核心部分它包含了 前言 React的基本概念,API,組件的構建方法以及高級用法,這背后的一切如何運轉,深入Virtual DOM內部的實現機制和原理. 初探Rea...
摘要:模型模型負責底層框架的構建工作它擁有一整套的標簽并負責虛擬節點及其屬性的構建更新刪除等工作其實構建一套簡易模型并不復雜它只需要具備一個標簽所需的基本元素即可標簽名屬性樣式子節點唯一標識中的節點稱為它分為種類型其中又分為和創建元素輸入輸出通過 Virtual DOM模型 1.Virtual DOM模型負責Virtual DOM底層框架的構建工作,它擁有一整套的Virtual DOM標簽,...
摘要:具體而言,就是每次數據發生變化,就重新執行一次整體渲染。而給出了解決方案,就是。由于只關注,通過閱讀兩個庫的源碼,對于的定位有了更深一步的理解。第二個而且,技術本身不是目的,能夠更好地解決問題才是王道嘛。 前言 React 好像已經火了很久很久,以致于我們對于 Virtual DOM 這個詞都已經很熟悉了,網上也有非常多的介紹 React、Virtual DOM 的文章。但是直到前不久...
閱讀 3100·2021-09-22 15:54
閱讀 3988·2021-09-09 11:34
閱讀 1772·2019-08-30 12:48
閱讀 1164·2019-08-30 11:18
閱讀 3437·2019-08-26 11:48
閱讀 921·2019-08-23 17:50
閱讀 2123·2019-08-23 17:17
閱讀 1246·2019-08-23 17:12