摘要:接下來我們深入函數(shù),看看它干了什么。在我們寫的代碼里,我們會(huì)手動(dòng)將元素掛載到樹上。到這里,我們已經(jīng)完成了元素掛載的全過程,接下來我們看一看更新的時(shí)候會(huì)發(fā)生什么。這部分應(yīng)該是負(fù)責(zé)的,我們要在組件的方法中調(diào)用。
etch簡介
首先我們有必要介紹一下etch。
etch是atom團(tuán)隊(duì)下的開源項(xiàng)目,是一套非常簡潔然而功能十分完善的virtualDOM機(jī)制。我在偶然的情況下接觸到了這個(gè)開源項(xiàng)目,在讀README時(shí)為它簡潔的設(shè)計(jì)而驚嘆,而在閱讀源碼的過程中也為它巧妙的實(shí)現(xiàn)而贊嘆。
個(gè)人覺得etch針對是一個(gè)非常好的學(xué)習(xí)內(nèi)容,實(shí)際代碼才七百來行,邏輯極度清晰,很適合作為想了解vdom的人的入門項(xiàng)目。
etch項(xiàng)目地址
我將個(gè)人對etch源碼的實(shí)踐和理解寫成了一個(gè)項(xiàng)目,地址為源碼解讀地址
個(gè)人建議是直接去我這個(gè)項(xiàng)目看,我在項(xiàng)目中整理的整體的流程,也對具體的代碼添加的筆記,應(yīng)該很好懂,不過,如果你只是想簡單了解一下,那么可以繼續(xù)看這篇文章。
首先我們看一下項(xiàng)目的文件結(jié)構(gòu)
正常來說我們應(yīng)該從index.js開始看,但是index.js只是負(fù)責(zé)將函數(shù)匯總了一下,所以我們從真正的開始——component-helpers文件的initialize函數(shù)開始。
這個(gè)函數(shù)負(fù)責(zé)以一個(gè)component實(shí)例為參數(shù)(具體表現(xiàn)形式為在一個(gè)component的constructor中調(diào)用,參數(shù)為this。
舉個(gè)栗子
/** @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... } }
上面就是一個(gè)非常標(biāo)準(zhǔn)的etch組件,在constructor中使用etch.initialize就保證了當(dāng)一個(gè)組件被實(shí)例化的時(shí)候必然會(huì)調(diào)用initialize然后完成必要的初始化)。接下來我們深入initialize函數(shù),看看它干了什么。
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干的非常簡單——調(diào)用component實(shí)例的render函數(shù)返回jsx轉(zhuǎn)成的virtualNode,然后調(diào)用render將virtualNode轉(zhuǎn)化為DOM元素,最后將virtualNode和DOM元素都掛載在component上。在我們寫的代碼里,我們會(huì)手動(dòng)將DOM元素掛載到dom樹上。
接下來我們分兩條線看,一條是jsx如何如何變成virtualNode。很簡單,babel轉(zhuǎn)碼器,react就是用的這個(gè)。然而transform-react-jsx插件的默認(rèn)入口是React.createElement,這里需要我們配置一下,將其改成etch.dom。(入口的意思是jsx轉(zhuǎn)碼后的東西應(yīng)該傳到哪里)。
以下是.babelrc配置文件內(nèi)容 { "presets": ["env"], "plugins": [ ["transform-react-jsx", { "pragma": "etch.dom" // default pragma is React.createElement }],"transform-object-rest-spread","transform-regenerator" ] }
dom文件下的dom函數(shù)所做的就是將傳入的參數(shù)進(jìn)行處理,然后返回一個(gè)貨真價(jià)實(shí)的virtualNode,具體實(shí)現(xiàn)如下
function dom (tag, props, ...children) { let ambiguous = [] //這里其實(shí)就是我之前在bl寫的flatternChildren,作用就是對children進(jìn)行一些處理,將數(shù)組或者是字符串轉(zhuǎn)化為真正的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進(jìn)行處理,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} }
到此,我們應(yīng)該明白了,當(dāng)我們碰到一個(gè)jsx時(shí)候,我們實(shí)際收到的是一個(gè)經(jīng)過dom函數(shù)處理過的virtualNode(沒錯(cuò),我說的就是每個(gè)component的render返回的東西,另外所謂virtualNode說到底就是一個(gè)擁有特定屬性的對象)。
接下來我們看另一條線,那就是render如何將virtualNode轉(zhuǎn)化為一個(gè)真正的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 }
其實(shí)很簡單,通過對virtualNode的tag進(jìn)行判斷,我們可以輕易的判斷virtualNode是什么類型的(比如組件,比如基本元素,比如字符元素),然后針對不同的類型進(jìn)行處理(基本的好說),組件的話,要再走一遍組件的創(chuàng)建和掛載流程。若為基礎(chǔ)元素,則我們可以將對應(yīng)的屬性放到DOM元素上,最后返回創(chuàng)建好的DOM元素(其實(shí)virtualNode上的所有元素基本最后都是要反映到基礎(chǔ)DOM元素上的,可能是屬性,可能是子元素)。
到這里,我們已經(jīng)完成了DOM元素掛載的全過程,接下來我們看一看更新的時(shí)候會(huì)發(fā)生什么。
更新的話,我們會(huì)在自己寫的update函數(shù)中調(diào)用component-helpers的update函數(shù)(后面我們叫它etch.update),而etch.update和initialize一樣會(huì)以component實(shí)例作為參數(shù),具體來說就是組件class中的this。然后在etch.update中會(huì)以異步的形式來進(jìn)行更新,這樣可以保證避免更新冗余,極大的提升性能
function update (component, replaceNode=true) { if (syncUpdatesInProgressCounter > 0) { updateSync(component, replaceNode) return Promise.resolve() } //這是一個(gè)可以完成異步的機(jī)制 let scheduler = getScheduler() //通過這個(gè)判斷保證了再一次DOM實(shí)質(zhì)性更新完成之前不會(huì)再次觸發(fā) if (!componentsWithPendingUpdates.has(component)) { componentsWithPendingUpdates.add(component) scheduler.updateDocument(function () { componentsWithPendingUpdates.delete(component) //而根據(jù)這個(gè)我們可以很清楚的發(fā)現(xiàn)真正的更新還是靠同步版update updateSync(component, replaceNode) }) } return scheduler.getNextUpdatePromise() }
。但是etch.update真正進(jìn)行更新的部分卻是在etch.updateSync。看函數(shù)名我們就知道這是這是一個(gè)更新的同步版。這個(gè)函數(shù)會(huì)讓component實(shí)時(shí)更新,而etch.update實(shí)際上是以異步的形式調(diào)用的這個(gè)同步版。
接下來我們深入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-- }
事實(shí)上由于scheduler的騷操作,在調(diào)用updateSync之前實(shí)質(zhì)性的更新已經(jīng)全部調(diào)用,然后我們要做的就是調(diào)用component.render獲取新的virtualNode,然后通過patch函數(shù)根據(jù)新舊virtualNode判斷哪些部分需要更新,然后對DOM進(jìn)行更新,最后處理生命周期函數(shù),完美。
那么scheduler的騷操作到底是什么呢?其實(shí)就是靠requestAnimationFrame保證所有的更新都在同一幀內(nèi)解決。另外通過weakSet機(jī)制,可以保證一個(gè)組件在它完成自己的實(shí)質(zhì)性更新之前絕不會(huì)再重繪(這里是說數(shù)據(jù)會(huì)更新,但不會(huì)反映到實(shí)際的DOM元素上,這就很完美的做到了避免冗余的更新)
最后我們看一看組件的卸載和銷毀部分。這部分應(yīng)該是destroy負(fù)責(zé)的,我們要在組件的destory方法中調(diào)用etch.destory。要說一下,etch.destory和etch.update一樣是異步函數(shù).然后我們可以根據(jù)update很輕松的猜出一定含有一個(gè)同步版的destroySync。沒錯(cuò),就是這樣,真正的卸載是在destroySync中完成的。邏輯也很簡單,組件上的destory會(huì)被調(diào)用,它的子組件上具有destory的也會(huì)被調(diào)用,這樣一直遞歸。最后從DOM樹上刪除掉component對應(yīng)的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,很簡單,很有趣,很巧妙。
整篇文章絮絮叨叨的,而且還是源碼這種冷門的東西,估計(jì)沒什么人愿意看。不過我還是想發(fā)上來,作為自己的筆記,也希望能對他人有用。這篇文章是我在segmentfault上發(fā)的第一篇技術(shù)文章,生澀的很,我會(huì)努力進(jìn)步。另外,我真的建議直接去我那個(gè)項(xiàng)目看筆記,應(yīng)該比這篇文章清晰的多。
2018.4.11于學(xué)校
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/94132.html
摘要:表示調(diào)用棧在下一將要執(zhí)行的任務(wù)。兩方性能解藥我們一般有兩種方案突破上文提到的瓶頸將耗時(shí)高成本高易阻塞的長任務(wù)切片,分成子任務(wù),并異步執(zhí)行這樣一來,這些子任務(wù)會(huì)在不同的周期執(zhí)行,進(jìn)而主線程就可以在子任務(wù)間隙當(dāng)中執(zhí)行更新操作。 showImg(https://segmentfault.com/img/remote/1460000016008111); 性能一直以來是前端開發(fā)中非常重要的話題...
摘要:前言的基本概念組件的構(gòu)建方法以及高級用法這背后的一切如何運(yùn)轉(zhuǎn)深入內(nèi)部的實(shí)現(xiàn)機(jī)制和原理初探源碼代碼組織結(jié)構(gòu)包含一系列的工具方法插件包含一系列同構(gòu)方法包含一些公用或常用方法如等包含一些測試方法等包含一些邊界錯(cuò)誤的測試用例是代碼的核心部分它包含了 前言 React的基本概念,API,組件的構(gòu)建方法以及高級用法,這背后的一切如何運(yùn)轉(zhuǎn),深入Virtual DOM內(nèi)部的實(shí)現(xiàn)機(jī)制和原理. 初探Rea...
摘要:模型模型負(fù)責(zé)底層框架的構(gòu)建工作它擁有一整套的標(biāo)簽并負(fù)責(zé)虛擬節(jié)點(diǎn)及其屬性的構(gòu)建更新刪除等工作其實(shí)構(gòu)建一套簡易模型并不復(fù)雜它只需要具備一個(gè)標(biāo)簽所需的基本元素即可標(biāo)簽名屬性樣式子節(jié)點(diǎn)唯一標(biāo)識(shí)中的節(jié)點(diǎn)稱為它分為種類型其中又分為和創(chuàng)建元素輸入輸出通過 Virtual DOM模型 1.Virtual DOM模型負(fù)責(zé)Virtual DOM底層框架的構(gòu)建工作,它擁有一整套的Virtual DOM標(biāo)簽,...
摘要:具體而言,就是每次數(shù)據(jù)發(fā)生變化,就重新執(zhí)行一次整體渲染。而給出了解決方案,就是。由于只關(guān)注,通過閱讀兩個(gè)庫的源碼,對于的定位有了更深一步的理解。第二個(gè)而且,技術(shù)本身不是目的,能夠更好地解決問題才是王道嘛。 前言 React 好像已經(jīng)火了很久很久,以致于我們對于 Virtual DOM 這個(gè)詞都已經(jīng)很熟悉了,網(wǎng)上也有非常多的介紹 React、Virtual DOM 的文章。但是直到前不久...
閱讀 3174·2021-09-22 15:54
閱讀 4098·2021-09-09 11:34
閱讀 1834·2019-08-30 12:48
閱讀 1221·2019-08-30 11:18
閱讀 3516·2019-08-26 11:48
閱讀 979·2019-08-23 17:50
閱讀 2182·2019-08-23 17:17
閱讀 1313·2019-08-23 17:12