摘要:翻譯自截止目前我們已經可以使用來創建并渲染頁面。是由子元素對應實例組成的數組。出現這個錯誤是因為我們沒有考慮節點需要移除的情況。注意這地方返回了這一節,我們為增加了更新的功能。我們通過重用節點,避免了頻繁的創建和移除節點,提高了的工作效率。
翻譯自:https://engineering.hexacta.c...
截止目前我們已經可以使用JSX來創建并渲染頁面DOM。在這一節我們將會把重點放在如何更新DOM上。
在介紹setState之前,更新DOM只能通過更改入參并再次調用render方法來實現。如果我們想實現一個時鐘,代碼大概下面這個樣子:
const rootDom = document.getElementById("root"); function tick() { const time = new Date().toLocaleTimeString(); const clockElement ={time}
; render(clockElement, rootDom); } tick(); setInterval(tick, 1000);
事實上,上面的代碼運行后并不能達到預期的效果,多次調用當前版本的render方法只會不斷往頁面上添加新的元素,而不是我們預期的更新已經存在的元素。下面我們想辦法實現更新操作。在render方法末尾,我們可以去檢查父類元素是否含有子元素,如果有,我們就用新生成的元素去替換舊的元素。
function render(element, parentDom){ // ... // Create dom from element // ... if(!parentDom.lastChild){ parentDom.appendChild(dom); } else { parentDom.replaceChild(dom, parentDom.lastChild); } }
針對開頭那個時鐘的例子,上面render的實現是沒問題的。但對于更復雜的情況,比如有多個子元素時上面代碼就不能滿足要求了。正確的做法是我們需要比較前后兩次調用render方法時所生成的元素樹,對比差異后只更新有變化的部分。
Virtual DOM and ReconciliationReact把一致性校驗的過程稱作“diffing”,我們要做的和React一樣。首先需要把當前的元素樹保存起來以便和后面新的元素樹比較,也就是說,我們需要把當前頁面內容所對應的虛擬DOM保存下來。
這顆虛擬DOM樹的節點有必要討論一下。一種選擇是使用Didact Elements,它們已經含有props.children屬性,我們可以根據這個屬性構建出虛擬DOM樹。現在有兩個問題擺在面前:首先,為了方便比較,我們需要保存每個虛擬DOM指向的真實DOM的引用(校驗過程中我們有需要會去更新實際DOM的屬性),并且元素還要是不可變的;第二,目前元素還不支持含有內部狀態(state)的組件。
Instances我們需要引入一個新的概念-----instances-----來解決上面的問題。一個實例表示一個已經渲染到DOM的元素,它是含有element,dom和childInstances屬性的一個JS對象。childInstances是由子元素對應實例組成的數組。
注意,這里說的實例和Dan Abramov在React Components, Elements, and Instances中提到的實例并不是一回事。Dan說的是公共實例,是調用繼承自React.Component的組件的構造函數后返回的東西。我們將在后面的章節添加公共實例。
每個DOM節點都會有對應的實例。一致性校驗的目的之一就是盡量避免去創建或者移除實例。創建和移除實例意味著我們要修改DOM樹,所以我們越多的重用實例就會越少的去修改DOM樹。
Refactoring接下來我們來重寫render方法,增加一致性校驗算法,同時增加一個instantiate方法來為元素創建實例。
let rootInstance = null; // 用來保存上一次調用render產生的實例 function render(element, container){ const prevInstance = rootInstance; const nextInstance = reconcile(container, prevInstance, element); rootInstance = nextInstace; } // 目前只是針對根元素的校驗,沒有處理到子元素 function reconcile(parentDom, instance, element){ if(instance === null){ const newInstance = instantiate(element); parentDom.appendChild(newInstance.dom); return newInstance; } else { const newInstance = instantiate(element); parentDom.replaceChild(newInstance.dom, instance.dom); return newInstance; } } // 生成元素對應實例的方法 function instantiate(element){ const { type, props} = element; const isTextElement = type === "TEXT_ELEMENT"; const dom = isTextElement ? document.createTextNode("") : document.createElement(type); // 添加事件 const isListener = name => name.startsWith("on"); Object.keys(props).filter(isListener).forEach(name => { const eventType = name.toLowerCase().substring(2); dom.addEventListener(eventType, props[name]); }); // 設置屬性 const isAttribute = name => !isListener(name) && name != "children"; Object.keys(props).filter(isAttribute).forEach(name => { dom[name] = props[name]; }); const childElements = props.children || []; const childInstances = childElements.map(instantiate); const childDoms = childInstances.map(childInstance => childInstace.dom); childDoms.forEach(childDom => dom.appendChild(childDOm)); const instance = {dom, element, childInstances}; return instance; }
上面的render方法和之前的差不多,不同之處是保存了上次調用render方法產生的實例。我們還把一致性校驗的功能從創建實例的代碼中分離了出來。
為了重用dom節點,我們需要一個能更新dom屬性的方法,這樣就不用每次都創建新的dom節點了。我們來改造一下現有代碼中設置屬性的那部分的代碼。
function instantiate(element) { const { type, props } = element; // 創建DOM元素 const isTextElement = type === "TEXT_ELEMENT"; const dom = isTextElement ? document.createTextNode("") : document.createElement(type); updateDomProperties(dom, [], props); // 實例化一個新的元素 // 實例化并添加子元素 const childElements = props.children || []; const childInstances = childElements.map(instantiate); const childDoms = childInstances.map(childInstance => childInstance.dom); childDoms.forEach(childDom => dom.appendChild(childDom)); const instance = { dom, element, childInstances }; return instance; } function updateDomProperties(dom, prevProps, nextProps){ const isEvent = name => name.startsWith("on"); const isAttribute = name => !isEvent(name) && name != "children"; Object.keys(prevProps).filter(isEvent).forEach(name => { const eventType = name.toLowerCase().substring(2); dom.removeEventListener(eventType, prevProps[name]); }); Object.keys(preProps).filter(isAttribute).forEach(name => { dom[name] = nextProps[name]; }); // 設置屬性 Object.keys(nextProps).filter(isAttribute).forEach(name => { dom[name] = nextProps[name]; }); // 添加事件監聽 Object.keys(nextProps).filter(isEvent).forEach(name => { const eventType = name.toLowerCase().substring(2); dom.addEventListener(eventType, nextProps[name]); }); }
updateDomProperties方法會移除所有舊的屬性,然后再添加新屬性。如果屬性沒有變化的話依然會進行移除和添加操作,這一定程度上有些浪費,但我們先這樣放著,后面再處理。
Reusing DOM nodes前面說過,一致性校驗算法需要盡可能多的去重用已經創建的節點。因為目前元素的type都是代表HTML中標簽名的字符串,所以如果同一位置前后兩次渲染的元素的類型一樣則表示兩者為同一類元素,對應的已經渲染到頁面上的dom節點就可以被重用。下面我們在reconcile中增加判斷前后兩次渲染的元素類型是否相同的功能,相同的話執行更新操作,否則是新建或者替換。
function reconcile(parentDom, instance, element) { if (instance == null) { // 創建實例 const newInstance = instantiate(element); parentDom.appendChild(newInstance.dom); return newInstance; } else if (instance.element.type === element.type) { // 和老的實例進行類型比較 // 更新 updateDomProperties(instance.dom, instance.element.props, element.props); instance.element = element; return instance; } else { // 如果不相等的話直接替換 const newInstance = instantiate(element); parentDom.replaceChild(newInstance.dom, instance.dom); return newInstance; } }Children Reconciliation
現在校驗過程還沒有對子元素進行處理。針對子元素的校驗是React中的一個關鍵部分,這一過程需要元素的一個額外屬性key來完成,如果某個元素在新舊虛擬DOM上的key值相同,則表示該元素沒有發生變化,直接重用即可。在當前版本的代碼中我們會遍歷instance.childInstances和element.props.children,并對同一位置的實例和元素進行比較,通過這種方式完成對子元素的一致性校驗。這種方法的缺點就是,如果子元素只是調換了位置,那么對應的DOM節點將沒法重用。
我們把同一實例上一次的instance.childInstances和這次對應元素的element.props.children進行遞歸比較,并且保存每次reconcile返回的結果以便更新childInstances。
function reconcile(parentDom, instance, element){ if(instance == null){ const newInstance = instantiate(element); parentDom.appendChild(newInstance.dom); return newInstance; } else if(instance.element.type === element.type){ updateDomProperties(instance.dom, instance.element.props, element.props); instance.childInstances = reconcileChildren(instance, element); instance.element = element; return instance; } else { const newInstance = instantiate(element); parentDom.replaceChild(newInstance.dom, instance.dom); return newInstance; } } function reconcileChildren(instance, element){ const dom = instance.dom; const childInstances = instance.childInstances; const nextChildElements = element.props.children || []; const newChildInstances = []; const count = Math.max(childInstances.length, nextChildElements.length); for(let i = 0; i< count; i++){ const childInstance = childInstances[i]; const childElement = nextChildElements[i];//上面一行和這一行都容易出現空指針,稍后處理 const newChildInstance = reconcile(dom, childInstance, childElement); newChildInstances.push(newChildInstance); } return newChildInstances; }Removing DOM nodes
如果nextChildElements數量多于childInstances,那么對子元素進行一致性校驗時就容易出現undefined與剩下的子元素進行比較的情況。不過這不是什么大問題,因為在reconcile中的if(instance == null)會處理這種情況,并且會根據多出來的元素創建新的實例。如果childInstances的數量多于nextChildElement,那么reconcile就會收到一個undefined作為其element參數,然后在嘗試獲取element.type時就會拋出錯誤。
出現這個錯誤是因為我們沒有考慮DOM節點需要移除的情況。所以接下來我們要做兩件事情,一個是在reconcile中增加增加element === null的校驗,一個是在reconcileChildren中過濾掉值為null的childInstances元素。
function reconcile(parentDom, instance, element){ if(instance == null){ const newInstance = instantiate(element); parentDom.appendChild(newInstance.dom); return Instance; } else if(element == null){ parentDom.removeChild(instance.dom); return null; // 注意這地方返回null了 } else if(instance.element.type === element.type){ updateDomProperties(instance.dom, instance.element.props, element.props); instance.childInstances = reconcileChildren(instance, element); instance.element = element; return instance; } else { const newInstance = instantiate(element); parentDom.replaceChild(newInstance.dom, instance.dom); return newInstance; } } function reconcileChildren(instance, element){ const dom = instance.dom; const childInstances = instance.childInstances; const nextChildElements = element.props.children || []; const newChildInstances = []; const count = Math.max(childInstances.length, nextChildElements.length); for(let i = 0; i < count; i++){ const childInstance = childInstances[i]; const childElement = nextChildElements[i]; const newChildInstances = reconcile(dom, childInstance, childElement); newChildInstances.push(newChildInstance); } return newChildInstances.filter(instance => instance != null) }Summary
這一節,我們為Didact增加了更新DOM的功能。我們通過重用節點,避免了頻繁的創建和移除DOM節點,提高了Didact的工作效率。重用節點還有一定的好處,比如保存了DOM的位置或者焦點等一些內部狀態信息。
目前我們是在根元素上調用render方法的,每次有變化時也是針對整棵元素樹進行的一致性校驗。下一節我們將介紹組件。有了組件我們就可以只針對有變化的那一部分子樹進行一致性校驗。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/99871.html
摘要:翻譯自截止目前我們已經可以使用來創建并渲染頁面。是由子元素對應實例組成的數組。出現這個錯誤是因為我們沒有考慮節點需要移除的情況。注意這地方返回了這一節,我們為增加了更新的功能。我們通過重用節點,避免了頻繁的創建和移除節點,提高了的工作效率。 翻譯自:https://engineering.hexacta.c... 截止目前我們已經可以使用JSX來創建并渲染頁面DOM。在這一節我們將會把...
摘要:它的主體特征是增量渲染能夠將渲染工作分割成塊,并將其分散到多個幀中。實際上,這樣做可能會造成浪費,導致幀丟失并降低用戶體驗。當一個函數被執行時,一個新的堆棧框架被添加到堆棧中。該堆??虮硎居稍摵瘮祱绦械墓ぷ?。 原文 react-fiber-architecture 介紹 React Fibre是React核心算法正在進行的重新實現。它是React團隊兩年多的研究成果。 React ...
摘要:事件行為在瀏覽器中保持一次,并且符合標準。主要是進行修復。事件已經在移動上支持。阻止已經在上存在的事件錯誤處理。然后對應的將會被打包送往客戶端。在中棄用,現在正式刪除。是運行于一個嚴格的安全策略下成為可能。增加警告提示非生產環境。 ??寫在開頭 閱讀React官網的 RECENT POSTS的個人翻譯/摘要(部分)。 英文片段為官網原文片段。 原文地址 ??為什么要使用React ...
閱讀 3793·2021-11-12 10:34
閱讀 2812·2021-09-22 15:14
閱讀 778·2019-08-30 15:53
閱讀 3196·2019-08-30 12:53
閱讀 1280·2019-08-29 18:32
閱讀 2761·2019-08-29 16:41
閱讀 1056·2019-08-26 13:40
閱讀 1795·2019-08-23 18:07