React
的虛擬DOM
和Diff
算法是React
的很是重要的核心特性,這部分源碼也很是複雜,理解這部分知識的原理對更深刻的掌握React
是很是必要的。html
原本想將虛擬DOM
和Diff
算法放到一篇文章,寫完虛擬DOM
發現文章已經很長了,因此本篇只分析虛擬DOM
。node
本篇文章從源碼出發,分析虛擬DOM
的核心渲染原理(首次渲染),以及React
對它作的性能優化點。react
說實話React
源碼真的很難讀😅,若是本篇文章幫助到了你,那麼請給個贊👍支持一下吧。git
React
React
組件爲什麼必須大寫React
如何防止XSS
React
的Diff
算法和其餘的Diff
算法有何區別key
在React
中的做用React
組件若是你對上面幾個問題還存在疑問,說明你對React
的虛擬DOM
以及Diff
算法實現原理還有所欠缺,那麼請好好閱讀本篇文章吧。github
首先咱們來看看到底什麼是虛擬DOM
:web
在原生的JavaScript
程序中,咱們直接對DOM
進行建立和更改,而DOM
元素經過咱們監聽的事件和咱們的應用程序進行通信。算法
而React
會先將你的代碼轉換成一個JavaScript
對象,而後這個JavaScript
對象再轉換成真實DOM
。這個JavaScript
對象就是所謂的虛擬DOM
。express
好比下面一段html
代碼:api
<div class="title">
<span>Hello ConardLi</span>
<ul>
<li>蘋果</li>
<li>橘子</li>
</ul>
</div>
複製代碼
在React
可能存儲爲這樣的JS
代碼:數組
const VitrualDom = {
type: 'div',
props: { class: 'title' },
children: [
{
type: 'span',
children: 'Hello ConardLi'
},
{
type: 'ul',
children: [
{ type: 'li', children: '蘋果' },
{ type: 'li', children: '橘子' }
]
}
]
}
複製代碼
當咱們須要建立或更新元素時,React
首先會讓這個VitrualDom
對象進行建立和更改,而後再將VitrualDom
對象渲染成真實DOM
;
當咱們須要對DOM
進行事件監聽時,首先對VitrualDom
進行事件監聽,VitrualDom
會代理原生的DOM
事件從而作出響應。
React
爲什麼採用VitrualDom
這種方案呢?
使用JavaScript
,咱們在編寫應用程序時的關注點在於如何更新DOM
。
使用React
,你只須要告訴React
你想讓視圖處於什麼狀態,React
則經過VitrualDom
確保DOM
與該狀態相匹配。你沒必要本身去完成屬性操做、事件處理、DOM
更新,React
會替你完成這一切。
這讓咱們更關注咱們的業務邏輯而非DOM
操做,這一點便可大大提高咱們的開發效率。
不少文章說VitrualDom
能夠提高性能,這一說法其實是很片面的。
直接操做DOM
是很是耗費性能的,這一點毋庸置疑。可是React
使用VitrualDom
也是沒法避免操做DOM
的。
若是是首次渲染,VitrualDom
不具備任何優點,甚至它要進行更多的計算,消耗更多的內存。
VitrualDom
的優點在於React
的Diff
算法和批處理策略,React
在頁面更新以前,提早計算好了如何進行更新和渲染DOM
。實際上,這個計算過程咱們在直接操做DOM
時,也是能夠本身判斷和實現的,可是必定會耗費很是多的精力和時間,並且每每咱們本身作的是不如React
好的。因此,在這個過程當中React
幫助咱們"提高了性能"。
因此,我更傾向於說,VitrualDom
幫助咱們提升了開發效率,在重複渲染時它幫助咱們計算如何更高效的更新,而不是它比DOM
操做更快。
若是您對本部分的分析有什麼不一樣看法,歡迎在評論區拍磚。
React
基於VitrualDom
本身實現了一套本身的事件機制,本身模擬了事件冒泡和捕獲的過程,採用了事件代理,批量更新等方法,抹平了各個瀏覽器的事件兼容性問題。
VitrualDom
爲React
帶來了跨平臺渲染的能力。以React Native
爲例子。React
根據VitrualDom
畫出相應平臺的ui
層,只不過不一樣平臺畫的姿式不一樣而已。
若是你不想看繁雜的源碼,或者如今沒有足夠時間,能夠跳過這一章,直接👇虛擬DOM原理、特性總結
在上面的圖上咱們繼續進行擴展,按照圖中的流程,咱們依次來分析虛擬DOM
的實現原理。
咱們在實現一個React
組件時能夠選擇兩種編碼方式,第一種是使用JSX
編寫:
class Hello extends Component {
render() {
return <div>Hello ConardLi</div>;
}
}
複製代碼
第二種是直接使用React.createElement
編寫:
class Hello extends Component {
render() {
return React.createElement('div', null, `Hello ConardLi`);
}
}
複製代碼
實際上,上面兩種寫法是等價的,JSX
只是爲 React.createElement(component, props, ...children)
方法提供的語法糖。也就是說全部的JSX
代碼最後都會轉換成React.createElement(...)
,Babel
幫助咱們完成了這個轉換的過程。
以下面的JSX
<div>
<img src="avatar.png" className="profile" /> <Hello /> </div>;
複製代碼
將會被Babel
轉換爲
React.createElement("div", null, React.createElement("img", {
src: "avatar.png",
className: "profile"
}), React.createElement(Hello, null));
複製代碼
注意,babel
在編譯時會判斷JSX
中組件的首字母,當首字母爲小寫時,其被認定爲原生DOM
標籤,createElement
的第一個變量被編譯爲字符串;當首字母爲大寫時,其被認定爲自定義組件,createElement
的第一個變量被編譯爲對象;
另外,因爲JSX
提早要被Babel
編譯,因此JSX
是不能在運行時動態選擇類型的,好比下面的代碼:
function Story(props) {
// Wrong! JSX type can't be an expression.
return <components[props.storyType] story={props.story} />; } 複製代碼
須要變成下面的寫法:
function Story(props) {
// Correct! JSX type can be a capitalized variable.
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />; } 複製代碼
因此,使用JSX
你須要安裝Babel
插件babel-plugin-transform-react-jsx
{
"plugins": [
["transform-react-jsx", {
"pragma": "React.createElement"
}]
]
}
複製代碼
下面咱們來看看虛擬DOM
的真實模樣,將下面的JSX
代碼在控制檯打印出來:
<div className="title">
<span>Hello ConardLi</span>
<ul>
<li>蘋果</li>
<li>橘子</li>
</ul>
</div>
複製代碼
這個結構和咱們上面本身描繪的結構很像,那麼React
是如何將咱們的代碼轉換成這個結構的呢,下面咱們來看看createElement
函數的具體實現(文中的源碼通過精簡)。
createElement
函數內部作的操做很簡單,將props
和子元素進行處理後返回一個ReactElement
對象,下面咱們來逐一分析:
(1).處理props:
ref
、key
從config
中取出並賦值self
、source
從config
中取出並賦值props
後面的文章會詳細介紹這些特殊屬性的做用。
(2).獲取子元素
props.children
props.children
(3).處理默認props
defaultProps
定義的默認props
進行賦值ReactElement
ReactElement
將傳入的幾個屬性進行組合,並返回。
type
:元素的類型,能夠是原生html類型(字符串),或者自定義組件(函數或class
)key
:組件的惟一標識,用於Diff
算法,下面會詳細介紹ref
:用於訪問原生dom
節點props
:傳入組件的props
owner
:當前正在構建的Component
所屬的Component
$$typeof
:一個咱們不常見到的屬性,它被賦值爲REACT_ELEMENT_TYPE
:
var REACT_ELEMENT_TYPE =
(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
0xeac7;
複製代碼
可見,$$typeof
是一個Symbol
類型的變量,這個變量能夠防止XSS
。
若是你的服務器有一個漏洞,容許用戶存儲任意JSON
對象, 而客戶端代碼須要一個字符串,這可能會成爲一個問題:
// JSON
let expectedTextButGotJSON = {
type: 'div',
props: {
dangerouslySetInnerHTML: {
__html: '/* put your exploit here */'
},
},
};
let message = { text: expectedTextButGotJSON };
<p> {message.text} </p>
複製代碼
JSON
中不能存儲Symbol
類型的變量。
ReactElement.isValidElement
函數用來判斷一個React
組件是不是有效的,下面是它的具體實現。
ReactElement.isValidElement = function (object) {
return typeof object === 'object' && object !== null && object.$$typeof === REACT_ELEMENT_TYPE;
};
複製代碼
可見React
渲染時會把沒有$$typeof
標識,以及規則校驗不經過的組件過濾掉。
當你的環境不支持Symbol
時,$$typeof
被賦值爲0xeac7
,至於爲何,React
開發者給出了答案:
0xeac7
看起來有點像React
。
self
、source
只有在非生產環境纔會被加入對象中。
self
指定當前位於哪一個組件實例。_source
指定調試代碼來自的文件(fileName
)和代碼行數(lineNumber
)。上面咱們分析了代碼轉換成了虛擬DOM
的過程,下面來看一下React
如何將虛擬DOM
轉換成真實DOM
。
本部分邏輯較複雜,咱們先用流程圖梳理一下整個過程,整個過程大概可分爲四步:
過程1:初始參數處理
在編寫好咱們的React
組件後,咱們須要調用ReactDOM.render(element, container[, callback])
將組件進行渲染。
render
函數內部實際調用了_renderSubtreeIntoContainer
,咱們來看看它的具體實現:
render: function (nextElement, container, callback) {
return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
},
複製代碼
TopLevelWrapper
進行包裹TopLevelWrapper
只一個空殼,它爲你須要掛載的組件提供了一個rootID
屬性,並在render
函數中返回該組件。
TopLevelWrapper.prototype.render = function () {
return this.props.child;
};
複製代碼
ReactDOM.render
函數的第一個參數能夠是原生DOM
也能夠是React
組件,包裹一層TopLevelWrapper
能夠在後面的渲染中將它們進行統一處理,而不用關心是否原生。
shouldReuseMarkup
變量,該變量表示是否須要從新標記元素_renderNewRootComponent
,渲染完成後調用callback
。在_renderNewRootComponent
中調用instantiateReactComponent
對咱們傳入的組件進行分類包裝:
根據組件的類型,React
根據原組件建立了下面四大類組件,對組件進行分類渲染:
ReactDOMEmptyComponent
:空組件ReactDOMTextComponent
:文本ReactDOMComponent
:原生DOM
ReactCompositeComponent
:自定義React
組件他們都具有如下三個方法:
construct
:用來接收ReactElement
進行初始化。mountComponent
:用來生成ReactElement
對應的真實DOM
或DOMLazyTree
。unmountComponent
:卸載DOM
節點,解綁事件。具體是如何渲染咱們在過程3中進行分析。
過程2:批處理、事務調用
在_renderNewRootComponent
中使用ReactUpdates.batchedUpdates
調用batchedMountComponentIntoNode
進行批處理。
ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);
複製代碼
在batchedMountComponentIntoNode
中,使用transaction.perform
調用mountComponentIntoNode
讓其基於事務機制進行調用。
transaction.perform(mountComponentIntoNode, null, componentInstance, container, transaction, shouldReuseMarkup, context);
複製代碼
關於批處理事務,在我前面的分析setState執行機制中有更多介紹。
過程3:生成html
在mountComponentIntoNode
函數中調用ReactReconciler.mountComponent
生成原生DOM
節點。
mountComponent
內部其實是調用了過程1生成的四種對象的mountComponent
方法。首先來看一下ReactDOMComponent
:
DOM
標籤、props
進行處理。DOM
節點。_updateDOMProperties
將props
插入到DOM
節點,_updateDOMProperties
也可用於props Diff
,第一個參數爲上次渲染的props
,第二個參數爲當前props
,若第一個參數爲空,則爲首次建立。DOMLazyTree
對象並調用_createInitialChildren
將孩子節點渲染到上面。那麼爲何不直接生成一個DOM
節點而是要建立一個DOMLazyTree
呢?咱們先來看看_createInitialChildren
作了什麼:
判斷當前節點的dangerouslySetInnerHTML
屬性、孩子節點是否爲文本和其餘節點分別調用DOMLazyTree
的queueHTML
、queueText
、queueChild
。
能夠發現:DOMLazyTree
其實是一個包裹對象,node
屬性中存儲了真實的DOM
節點,children
、html
、text
分別存儲孩子、html節點和文本節點。
它提供了幾個方法用於插入孩子、html
以及文本節點,這些插入都是有條件限制的,當enableLazy
屬性爲true
時,這些孩子、html
以及文本節點會被插入到DOMLazyTree
對象中,當其爲false
時會插入到真實DOM
節點中。
var enableLazy = typeof document !== 'undefined' &&
typeof document.documentMode === 'number' ||
typeof navigator !== 'undefined' &&
typeof navigator.userAgent === 'string' &&
/\bEdge\/\d/.test(navigator.userAgent);
複製代碼
可見:enableLazy
是一個變量,當前瀏覽器是IE
或Edge
時爲true
。
在IE(8-11)
和Edge
瀏覽器中,一個一個插入無子孫的節點,效率要遠高於插入一整個序列化完整的節點樹。
因此lazyTree
主要解決的是在IE(8-11)
和Edge
瀏覽器中插入節點的效率問題,在後面的過程4咱們會分析到:若當前是IE
或Edge
,則須要遞歸插入DOMLazyTree
中緩存的子節點,其餘瀏覽器只須要插入一次當前節點,由於他們的孩子已經被渲染好了,而不用擔憂效率問題。
下面來看一下ReactCompositeComponent
,因爲代碼很是多這裏就再也不貼這個模塊的代碼,其內部主要作了如下幾步:
props
、contex
等變量,調用構造函數建立組件實例state
performInitialMount
生命週期,處理子節點,獲取markup
。componentDidMount
生命週期在performInitialMount
函數中,首先調用了componentWillMount
生命週期,因爲自定義的React
組件並非一個真實的DOM,因此在函數中又調用了孩子節點的mountComponent
。這也是一個遞歸的過程,當全部孩子節點渲染完成後,返回markup
並調用componentDidMount
。
過程4:渲染html
在mountComponentIntoNode
函數中調用將上一步生成的markup
插入container
容器。
在首次渲染時,_mountImageIntoNode
會清空container
的子節點後調用DOMLazyTree.insertTreeBefore
:
判斷是否爲fragment
節點或者<object>
插件:
若是是以上兩種,首先調用insertTreeChildren
將此節點的孩子節點渲染到當前節點上,再將渲染完的節點插入到html
若是是其餘節點,先將節點插入到插入到html
,再調用insertTreeChildren
將孩子節點插入到html
。
若當前不是IE
或Edge
,則不須要再遞歸插入子節點,只須要插入一次當前節點。
IE
或bEdge
時return
children
不爲空,遞歸insertTreeBefore
進行插入有關虛擬DOM
的事件機制,我曾專門寫過一篇文章,有興趣能夠👇【React深刻】React事件機制
使用React.createElement
或JSX
編寫React
組件,實際上全部的JSX
代碼最後都會轉換成React.createElement(...)
,Babel
幫助咱們完成了這個轉換的過程。
createElement
函數對key
和ref
等特殊的props
進行處理,並獲取defaultProps
對默認props
進行賦值,而且對傳入的孩子節點進行處理,最終構形成一個ReactElement
對象(所謂的虛擬DOM
)。
ReactDOM.render
將生成好的虛擬DOM
渲染到指定容器上,其中採用了批處理、事務等機制而且對特定瀏覽器進行了性能優化,最終轉換爲真實DOM
。
即ReactElement
element對象,咱們的組件最終會被渲染成下面的結構:
type
:元素的類型,能夠是原生html類型(字符串),或者自定義組件(函數或class
)key
:組件的惟一標識,用於Diff
算法,下面會詳細介紹ref
:用於訪問原生dom
節點props
:傳入組件的props
,chidren
是props
中的一個屬性,它存儲了當前組件的孩子節點,能夠是數組(多個孩子節點)或對象(只有一個孩子節點)owner
:當前正在構建的Component
所屬的Component
self
:(非生產環境)指定當前位於哪一個組件實例_source
:(非生產環境)指定調試代碼來自的文件(fileName
)和代碼行數(lineNumber
)ReactElement
對象還有一個$$typeof
屬性,它是一個Symbol
類型的變量Symbol.for('react.element')
,當環境不支持Symbol
時,$$typeof
被賦值爲0xeac7
。
這個變量能夠防止XSS
。若是你的服務器有一個漏洞,容許用戶存儲任意JSON
對象, 而客戶端代碼須要一個字符串,這可能爲你的應用程序帶來風險。JSON
中不能存儲Symbol
類型的變量,而React
渲染時會把沒有$$typeof
標識的組件過濾掉。
React
在渲染虛擬DOM
時應用了批處理以及事務機制,以提升渲染性能。
關於批處理以及事務機制,在我以前的文章【React深刻】setState的執行機制中有詳細介紹。
在IE(8-11)
和Edge
瀏覽器中,一個一個插入無子孫的節點,效率要遠高於插入一整個序列化完整的節點樹。
React
經過lazyTree
,在IE(8-11)
和Edge
中進行單個節點依次渲染節點,而在其餘瀏覽器中則首先將整個大的DOM
結構構建好,而後再總體插入容器。
而且,在單獨渲染節點時,React
還考慮了fragment
等特殊節點,這些節點則不會一個一個插入渲染。
React
本身實現了一套事件機制,其將全部綁定在虛擬DOM
上的事件映射到真正的DOM
事件,並將全部的事件都代理到document
上,本身模擬了事件冒泡和捕獲的過程,而且進行統一的事件分發。
React
本身構造了合成事件對象SyntheticEvent
,這是一個跨瀏覽器原生事件包裝器。 它具備與瀏覽器原生事件相同的接口,包括stopPropagation()
和preventDefault()
等等,在全部瀏覽器中他們工做方式都相同。這抹平了各個瀏覽器的事件兼容性問題。
上面只分析虛擬DOM
首次渲染的原理和過程,固然這並不包括虛擬 DOM
進行 Diff
的過程,下一篇文章咱們再來詳細探討。
關於開篇提的幾個問題,咱們在下篇文章中進行統一回答。
本文源碼中的版本爲React
15版本,相對16
版本會有一些出入,關於16
版本的改動,後面的文章會單獨分析。
文中若有錯誤,歡迎在評論區指正,或者您對文章的排版,閱讀體驗有什麼好的建議,歡迎在評論區指出,謝謝閱讀。
想閱讀更多優質文章、下載文章中思惟導圖源文件、閱讀文中demo
源碼、可關注個人github博客,你的star✨、點贊和關注是我持續創做的動力!
推薦關注個人微信公衆號【code祕密花園】,天天推送高質量文章,咱們一塊兒交流成長。