摘要:而對比變化,找出需要更新部分的算法我們稱之為算法。整個系列大概會有四篇,我每周會更新一到兩篇,我會第一時間在上更新,有問題需要探討也請?jiān)谏匣貜?fù)我博客地址關(guān)注點(diǎn),訂閱點(diǎn)上一篇文章從零開始實(shí)現(xiàn)一個二組件和生命周期
前言
在上一篇文章,我們已經(jīng)實(shí)現(xiàn)了React的組件功能,從功能的角度來說已經(jīng)實(shí)現(xiàn)了React的核心功能了。
但是我們的實(shí)現(xiàn)方式有很大的問題:每次更新都重新渲染整個應(yīng)用或者整個組件,DOM操作十分昂貴,這樣性能損耗非常大。
為了減少DOM更新,我們需要找渲染前后真正變化的部分,只更新這一部分DOM。而對比變化,找出需要更新部分的算法我們稱之為diff算法。
對比策略在前面兩篇文章后,我們實(shí)現(xiàn)了一個render方法,它能將虛擬DOM渲染成真正的DOM,我們現(xiàn)在就需要改進(jìn)它,讓它不要再傻乎乎地重新渲染整個DOM樹,而是找出真正變化的部分。
這部分很多類React框架實(shí)現(xiàn)方式都不太一樣,有的框架會選擇保存上次渲染的虛擬DOM,然后對比虛擬DOM前后的變化,得到一系列更新的數(shù)據(jù),然后再將這些更新應(yīng)用到真正的DOM上。
但也有一些框架會選擇直接對比虛擬DOM和真實(shí)DOM,這樣就不需要額外保存上一次渲染的虛擬DOM,并且能夠一邊對比一邊更新,這也是我們選擇的方式。
不管是DOM還是虛擬DOM,它們的結(jié)構(gòu)都是一棵樹,完全對比兩棵樹變化的算法時間復(fù)雜度是O(n^3),但是考慮到我們很少會跨層級移動DOM,所以我們只需要對比同一層級的變化。
只需要對比同一顏色框內(nèi)的節(jié)點(diǎn)
總而言之,我們的diff算法有兩個原則:
對比當(dāng)前真實(shí)的DOM和虛擬DOM,在對比過程中直接更新真實(shí)DOM
只對比同一層級的變化
實(shí)現(xiàn)我們需要實(shí)現(xiàn)一個diff方法,它的作用是對比真實(shí)DOM和虛擬DOM,最后返回更新后的DOM
/** * @param {HTMLElement} dom 真實(shí)DOM * @param {vnode} vnode 虛擬DOM * @returns {HTMLElement} 更新后的DOM */ function diff( dom, vnode ) { // ... }
接下來就要實(shí)現(xiàn)這個方法。
在這之前先來回憶一下我們虛擬DOM的結(jié)構(gòu):
虛擬DOM的結(jié)構(gòu)可以分為三種,分別表示文本、原生DOM節(jié)點(diǎn)以及組件。
// 原生DOM節(jié)點(diǎn)的vnode { tag: "div", attrs: { className: "container" }, children: [] } // 文本節(jié)點(diǎn)的vnode "hello,world" // 組件的vnode { tag: ComponentConstrucotr, attrs: { className: "container" }, children: [] }對比文本節(jié)點(diǎn)
首先考慮最簡單的文本節(jié)點(diǎn),如果當(dāng)前的DOM就是文本節(jié)點(diǎn),則直接更新內(nèi)容,否則就新建一個文本節(jié)點(diǎn),并移除掉原來的DOM。
// diff text node if ( typeof vnode === "string" ) { // 如果當(dāng)前的DOM就是文本節(jié)點(diǎn),則直接更新內(nèi)容 if ( dom && dom.nodeType === 3 ) { // nodeType: https://developer.mozilla.org/zh-CN/docs/Web/API/Node/nodeType if ( dom.textContent !== vnode ) { dom.textContent = vnode; } // 如果DOM不是文本節(jié)點(diǎn),則新建一個文本節(jié)點(diǎn)DOM,并移除掉原來的 } else { out = document.createTextNode( vnode ); if ( dom && dom.parentNode ) { dom.parentNode.replaceChild( out, dom ); } } return out; }
文本節(jié)點(diǎn)十分簡單,它沒有屬性,也沒有子元素,所以這一步結(jié)束后就可以直接返回結(jié)果了。
對比非文本DOM節(jié)點(diǎn)如果vnode表示的是一個非文本的DOM節(jié)點(diǎn),那就要分幾種情況了:
如果真實(shí)DOM和虛擬DOM的類型不同,例如當(dāng)前真實(shí)DOM是一個div,而vnode的tag的值是"button",那么原來的div就沒有利用價(jià)值了,直接新建一個button元素,并將div的所有子節(jié)點(diǎn)移到button下,然后用replaceChild方法將div替換成button。
if ( !dom || dom.nodeName.toLowerCase() !== vnode.tag.toLowerCase() ) { out = document.createElement( vnode.tag ); if ( dom ) { [ ...dom.childNodes ].map( out.appendChild ); // 將原來的子節(jié)點(diǎn)移到新節(jié)點(diǎn)下 if ( dom.parentNode ) { dom.parentNode.replaceChild( out, dom ); // 移除掉原來的DOM對象 } } }
如果真實(shí)DOM和虛擬DOM是同一類型的,那我們暫時不需要做別的,只需要等待后面對比屬性和對比子節(jié)點(diǎn)。
對比屬性實(shí)際上diff算法不僅僅是找出節(jié)點(diǎn)類型的變化,它還要找出來節(jié)點(diǎn)的屬性以及事件監(jiān)聽的變化。我們將對比屬性多帶帶拿出來作為一個方法:
function diffAttributes( dom, vnode ) { const old = dom.attributes; // 當(dāng)前DOM的屬性 const attrs = vnode.attrs; // 虛擬DOM的屬性 // 如果原來的屬性不在新的屬性當(dāng)中,則將其移除掉(屬性值設(shè)為undefined) for ( let name in old ) { if ( !( name in attrs ) ) { setAttribute( dom, name, undefined ); } } // 更新新的屬性值 for ( let name in attrs ) { if ( old[ name ] !== attrs[ name ] ) { setAttribute( dom, name, attrs[ name ] ); } } }
setAttribute方法的實(shí)現(xiàn)參見第一篇文章
對比子節(jié)點(diǎn)節(jié)點(diǎn)本身對比完成了,接下來就是對比它的子節(jié)點(diǎn)。
這里會面臨一個問題,前面我們實(shí)現(xiàn)的不同diff方法,都是明確知道哪一個真實(shí)DOM和虛擬DOM對比,但是子節(jié)點(diǎn)是一個數(shù)組,它們可能改變了順序,或者數(shù)量有所變化,我們很難確定要和虛擬DOM對比的是哪一個。
為了簡化邏輯,我們可以讓用戶提供一些線索:給節(jié)點(diǎn)設(shè)一個key值,重新渲染時對比key值相同的節(jié)點(diǎn)。
// diff方法 if ( vnode.children && vnode.children.length > 0 || ( out.childNodes && out.childNodes.length > 0 ) ) { diffChildren( out, vnode.children ); }
function diffChildren( dom, vchildren ) { const domChildren = dom.childNodes; const children = []; const keyed = {}; // 將有key的節(jié)點(diǎn)和沒有key的節(jié)點(diǎn)分開 if ( domChildren.length > 0 ) { for ( let i = 0; i < domChildren.length; i++ ) { const child = domChildren[ i ]; const key = child.key; if ( key ) { keyedLen++; keyed[ key ] = child; } else { children.push( child ); } } } if ( vchildren && vchildren.length > 0 ) { let min = 0; let childrenLen = children.length; for ( let i = 0; i < vchildren.length; i++ ) { const vchild = vchildren[ i ]; const key = vchild.key; let child; // 如果有key,找到對應(yīng)key值的節(jié)點(diǎn) if ( key ) { if ( keyed[ key ] ) { child = keyed[ key ]; keyed[ key ] = undefined; } // 如果沒有key,則優(yōu)先找類型相同的節(jié)點(diǎn) } else if ( min < childrenLen ) { for ( let j = min; j < childrenLen; j++ ) { let c = children[ j ]; if ( c && isSameNodeType( c, vchild ) ) { child = c; children[ j ] = undefined; if ( j === childrenLen - 1 ) childrenLen--; if ( j === min ) min++; break; } } } // 對比 child = diff( child, vchild ); // 更新DOM const f = domChildren[ i ]; if ( child && child !== dom && child !== f ) { if ( !f ) { dom.appendChild(child); } else if ( child === f.nextSibling ) { removeNode( f ); } else { dom.insertBefore( child, f ); } } } } }對比組件
如果vnode是一個組件,我們也多帶帶拿出來作為一個方法:
function diffComponent( dom, vnode ) { let c = dom && dom._component; let oldDom = dom; // 如果組件類型沒有變化,則重新set props if ( c && c.constructor === vnode.tag ) { setComponentProps( c, vnode.attrs ); dom = c.base; // 如果組件類型變化,則移除掉原來組件,并渲染新的組件 } else { if ( c ) { unmountComponent( c ); oldDom = null; } c = createComponent( vnode.tag, vnode.attrs ); setComponentProps( c, vnode.attrs ); dom = c.base; if ( oldDom && dom !== oldDom ) { oldDom._component = null; removeNode( oldDom ); } } return dom; }
下面是相關(guān)的工具方法的實(shí)現(xiàn),和上一篇文章的實(shí)現(xiàn)相比,只需要修改renderComponent方法其中的一行。
function renderComponent( component ) { // ... // base = base = _render( renderer ); // 將_render改成diff base = diff( component.base, renderer ); // ... }
完整diff實(shí)現(xiàn)看這個文件
渲染現(xiàn)在我們實(shí)現(xiàn)了diff方法,我們嘗試渲染上一篇文章中定義的Counter組件,來感受一下有無diff方法的不同。
class Counter extends React.Component { constructor( props ) { super( props ); this.state = { num: 1 } } onClick() { this.setState( { num: this.state.num + 1 } ); } render() { return (不使用diff); } }count: { this.state.num }
使用上一篇文章的實(shí)現(xiàn),從chrome的調(diào)試工具中可以看到,閃爍的部分是每次更新的部分,每次點(diǎn)擊按鈕,都會重新渲染整個組件。
而實(shí)現(xiàn)了diff方法后,每次點(diǎn)擊按鈕,都只會重新渲染變化的部分。
在這篇文章中我們實(shí)現(xiàn)了diff算法,通過它做到了每次只更新需要更新的部分,極大地減少了DOM操作。React實(shí)現(xiàn)遠(yuǎn)比這個要復(fù)雜,特別是在React 16之后還引入了Fiber架構(gòu),但是主要的思想是一致的。
實(shí)現(xiàn)diff算法可以說性能有了很大的提升,但是在別的地方仍然后很多改進(jìn)的空間:每次調(diào)用setState后會立即調(diào)用renderComponent重新渲染組件,但現(xiàn)實(shí)情況是,我們可能會在極短的時間內(nèi)多次調(diào)用setState。
假設(shè)我們在上文的Counter組件中寫出了這種代碼
onClick() { for ( let i = 0; i < 100; i++ ) { this.setState( { num: this.state.num + 1 } ); } }
那以目前的實(shí)現(xiàn),每次點(diǎn)擊都會渲染100次組件,對性能肯定有很大的影響。
下一篇文章我們就要來改進(jìn)setState方法
這篇文章的代碼:https://github.com/hujiulong/...
從零開始實(shí)現(xiàn)React系列React是前端最受歡迎的框架之一,解讀其源碼的文章非常多,但是我想從另一個角度去解讀React:從零開始實(shí)現(xiàn)一個React,從API層面實(shí)現(xiàn)React的大部分功能,在這個過程中去探索為什么有虛擬DOM、diff、為什么setState這樣設(shè)計(jì)等問題。
整個系列大概會有四篇,我每周會更新一到兩篇,我會第一時間在github上更新,有問題需要探討也請?jiān)趃ithub上回復(fù)我~
博客地址: https://github.com/hujiulong/...上一篇文章
關(guān)注點(diǎn)star,訂閱點(diǎn)watch
從零開始實(shí)現(xiàn)一個React(二):組件和生命周期
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/94134.html
摘要:可實(shí)際上并不是創(chuàng)造的,將這個概念拿過來以后融會貫通慢慢地成為目前前端最炙手可熱的框架之一。則是將再抽象一層生成的簡化版對象,這個對象也擁有上的一些屬性,比如等,但它是完全脫離于瀏覽器而存在的。所以今天我要手把手教大家怎么從零開始實(shí)現(xiàn)。 假如你的項(xiàng)目使用了React,你知道怎么做性能優(yōu)化嗎?你知道為什么React讓你寫shouldComponentUpdate或者React.PureCo...
摘要:一個比較好的做法是利用的事件隊(duì)列機(jī)制。整個系列大概會有四篇左右,我每周會更新一到兩篇,我會第一時間在上更新,有問題需要探討也請?jiān)谏匣貜?fù)我博客地址關(guān)注點(diǎn),訂閱點(diǎn)上一篇文章從零開始實(shí)現(xiàn)一個三算法 前言 在上一篇文章中,我們實(shí)現(xiàn)了diff算法,性能有非常大的改進(jìn)。但是文章末尾也指出了一個問題:按照目前的實(shí)現(xiàn),每次調(diào)用setState都會觸發(fā)更新,如果組件內(nèi)執(zhí)行這樣一段代碼: for ( le...
摘要:想要自己實(shí)現(xiàn)一個簡易版框架,并不是非常難。為了防止出現(xiàn)這種情況,我們需要改變整體的策略。上面這段話,說的就是版本和架構(gòu)的區(qū)別。 showImg(https://segmentfault.com/img/bVbwfRh); 想要自己實(shí)現(xiàn)一個React簡易版框架,并不是非常難。但是你需要先了解下面這些知識點(diǎn)如果你能閱讀以下的文章,那么會更輕松的閱讀本文章: 優(yōu)化你的超大型React應(yīng)用 ...
閱讀 1772·2021-10-11 10:57
閱讀 2356·2021-10-08 10:14
閱讀 3399·2019-08-29 17:26
閱讀 3356·2019-08-28 17:54
閱讀 3029·2019-08-26 13:38
閱讀 2903·2019-08-26 12:19
閱讀 3613·2019-08-23 18:05
閱讀 1282·2019-08-23 17:04