在傳統的開發模式中,每次須要進行頁面更新的時候都須要咱們手動的更新DOM:html
在前端開發中,最應該避免的就是DOM的更新,由於DOM更新是極其耗費性能的,有過操做DOM經歷的都應該知道,修改DOM的代碼也很是冗長,也會致使項目代碼閱讀困難。在React中,把真是得DOM轉換成JavaScript對象樹,這就是咱們說的虛擬DOM,它並非真正的DOM,只是存有渲染真實DOM須要的屬性的對象。前端
雖然虛擬DOM會提高必定得性能可是並不明顯,由於每次須要更新的時候Virtual DOM須要比較兩次的DOM有什麼不一樣,而後批量更新,這也是須要資源的。node
Virtual真實的好處實際上是,他能夠實現跨平臺,咱們所熟知的react-native就是基於VirtualDOM來實現的。react
如今咱們根據源碼來分析一下Virtual DOM的構建過程。json
JSX和React.createElementreact-native
在看源碼以前,如今回顧一下React中建立組件的兩種方式。緩存
1.JSX安全
function App() {
return (
<div>Hello React</div>
);
}
複製代碼
2.React.createElementbash
const App = React.createElement('div', null, 'Hello React');
複製代碼
這裏多說一句其實JSX只不過是React.createElement的語法糖,在編譯的時候babel會將JSX轉換成爲使用React.createElement的形式,由於JSX語法更加符合咱們平常開發的習慣,因此咱們在寫React的時候更多的是使用JSX語法進行編寫。babel
下面粘貼一段React.createElement的源碼來分析:
ReactElement.createElement = function(type, config, children) {
//初始化參數
var propName;
var props = {};
var key = null;
var ref = null;
var self = null;
var source = null;
if (config != null) {
// 若是存在config,則提取裏面的內容
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 將新添加的元素更新到新的props中
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
//若是隻有一個children參數,那麼指直接賦值給children
//不然合併處理children
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
// 若是某個prop爲空,且存在默認的prop,則將默認的prop賦值給props
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
//返回一個ReactElement實例對象,這個能夠理解就是咱們說的虛擬DOM
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
};
複製代碼
看到這裏咱們不由好奇上述代碼中返回的ReactElement究竟是個什麼東西呢?其實ReactElement就只是咱們常說的虛擬DOM,ReactElement主要包含了這個DOM節點的類型(type)、屬性(props)和子節點(children)。ReactElement只是包含了DOM節點的數據,尚未注入對應的一些方法來完成React框架的功能。
如今來看一下ReactElement的源碼部分:
var ReactElement = function (type, key, ref, self, source, owner, props) {
var element = {
// react中防止XSS注入的變量,也是標誌這個是react元素的變量,稍後會講
$$typeof: REACT_ELEMENT_TYPE,
// 構建屬於這個元素的屬性值
type: type,
key: key,
ref: ref,
props: props,
// 記錄一下建立這個元素的組件
_owner: owner,
};
return element;
};
複製代碼
上述代碼能夠看出來,ReactElement其實就是裝有各類屬性的一個大對象而已。
首先咱們如今控制檯打印一下react.createElement的結果:
WHAT???這個變量是什麼???
其實$$typeof是爲了安全問題引入的變量,什麼安全問題呢?那就是XSS
咱們都知道React.createElement方法的第三個參數是容許用戶輸入自定義組件的,那麼設想一下,若是前端容許用戶輸入下面一段代碼:
var input = "{"type": "div", "props": {"dangerouslySetInnerHTML": {"__html": "<script>alert('hey')</script>"}}}"" //而後咱們開始用輸入的值建立ReactElement,就變成了下面這個樣子 React.createElement('div', null, input); 複製代碼
至此XSS注入就達成目的啦。
那麼$$typeof這個變量是怎麼作到安全認證的呢???
var REACT_ELEMENT_TYPE =
(typeof Symbol === 'function' && Symbol.for && Symbol.for('react.element')) ||
0xeac7;
ReactElement.isValidElement = function (object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
};
複製代碼
首先$$typeof是Symbol類型的變量,是沒法經過json對象轉成字符串,因此就若是隻是簡單的json拷貝,是沒有辦法經過ReactElement.isValidElement的驗證的,ReactElement.isValidElement會將不帶有$$typeof變量的元素所有丟掉不用。
如今經過源碼來看一下react中從定義完組件以後render到頁面的過程。
當咱們想要將一個組件渲染到頁面上須要調用ReactDOM.render(element,container,[callback])方法,如今咱們就從這個方法入手一步一步來看源碼:
var ReactDOM = {
findDOMNode: findDOMNode,
render: ReactMount.render,
unmountComponentAtNode: ReactMount.unmountComponentAtNode,
version: ReactVersion
};
複製代碼
從上面代碼咱們能夠看到,咱們常常調用的ReactDOM.render,實際上是在調用ReactMount的render方法。因此咱們如今來看ReactMount中的render方法都作了些什麼。
/src/renderers/dom/client/ReactMount.js
render: function (nextElement, container, callback) {
return ReactMount._renderSubtreeIntoContainer(
null,
nextElement,
container,
callback,
);
}
複製代碼
如今咱們終於找到了源頭,那就是_renderSubtreeIntoContainer方法,咱們在來看一下它是怎麼樣定義的,能夠根據下面代碼中的註釋一步一步的來看:
_renderSubtreeIntoContainer: function (
parentComponent,
nextElement,
container,
callback,
) {
// 檢驗傳入的callback是否符合標準,若是不符合,validateCallback會throw出
//一個錯誤(內部調用了node_modules/fbjs/lib/invariant有invariant方法)
ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');
// 此處的TopLevelWrapper,只不過是將你傳進來的type,進行一層包裹,並賦值ID,並會在TopLevelWrapper.render方法中返回你傳入的值
// 具體看源碼,,因此個這東西只是一個包裹層
var nextWrappedElement = React.createElement(TopLevelWrapper, {
child: nextElement,
});
//判斷以前是否渲染過此元素,若是有返回此元素,若是沒有返回null
var prevComponent = getTopLevelWrapperInContainer(container);
if (prevComponent) {
var prevWrappedElement = prevComponent._currentElement;
var prevElement = prevWrappedElement.props.child;
// 判斷是否須要更新組件
if (shouldUpdateReactComponent(prevElement, nextElement)) {
var publicInst = prevComponent._renderedComponent.getPublicInstance();
var updatedCallback =
callback &&
function () {
callback.call(publicInst);
};
// 若是須要更新則調用組件更新方法,直接返回更新後的組件
ReactMount._updateRootComponent(
prevComponent,
nextWrappedElement,
nextContext,
container,
updatedCallback,
);
return publicInst;
} else {
// 不須要更新組件,那就把以前的組件卸載掉
ReactMount.unmountComponentAtNode(container);
}
}
// 返回當前容器的DOM節點,若是沒有container返回null
var reactRootElement = getReactRootElementInContainer(container);
// 返回上面reactRootElement的data-reactid
var containerHasReactMarkup =reactRootElement && !!internalGetID(reactRootElement);
// 判斷當前容器是否是有身爲react元素的子元素
var containerHasNonRootReactChild = hasNonRootReactChild(container);
// 獲得是否應該重複使用的標記變量
var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild;
// 將一個新的組件渲染到真是得DOM上
var component = ReactMount._renderNewRootComponent(
nextWrappedElement,
container,
shouldReuseMarkup,
nextContext,
)._renderedComponent.getPublicInstance();
// 若是有callback函數那就執行這個回調函數,而且將其this只想component
if (callback) {
callback.call(component);
}
// 返回組件
return component;
},
複製代碼
根據上面的註釋能夠很容易理解上面的代碼,如今咱們總結一下_renderSubtreeIntoContainer方法的執行過程:
1.校驗傳入callback的格式是否符合規範
2.用TopLevelWrapper包裹層(帶有reactID)包裹傳入的type,這裏說明一下,react.createElement這個方法的type值能夠有三種分別是,原生標籤的標籤名字符串('div'、'span')、react component 、react fragment
3.判斷是否渲染過這次準備渲染的元素,若是渲染過,則判斷是否須要更新。
3.1 若是須要更新則調用更新方法,而且直接將更新後的組件返回
3.2 若是不須要更新,則卸載老組件
4.若是沒渲染過,則處理shouldReuseMarkup變量
5.調用ReactMount._renderNewRootComponent將組將更新到DOM(此函數後面會分析)
6.返回組件
複製代碼
上面說到其實在_renderSubtreeIntoContainer方法中,最後使用了ReactMount._renderNewRootComponent進行進行組件的渲染,接下來咱們看一下該方法的源碼:
_renderNewRootComponent: function (
nextElement,
container,
shouldReuseMarkup,
context,
) {
// 監聽window上面的滾動事件,緩存滾動變量,保證在滾動的時候頁面不會觸發重排
ReactBrowserEventEmitter.ensureScrollValueMonitoring();
//獲取組件實例
var componentInstance = instantiateReactComponent(nextElement, false);
// 批處理,初始化render的過程是異步的,可是在render的時候componentWillMount或者componentDidMount生命中其中
// 可能會執行更新變量的操做,這是react會將這些操做經過當前批次策略,統一處理。
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode, // *
componentInstance,
container,
shouldReuseMarkup,
context,
);
var wrapperID = componentInstance._instance.rootID;
instancesByReactRootID[wrapperID] = componentInstance;
// 返回實例
return componentInstance;
}
複製代碼
仍是先來總結一下上面代碼的過程:
1.監聽滾動事件,緩存變量,避免滾動帶來的重排
2.初始化組件實例
3.批量執行更新操做
複製代碼
在上面代碼執行過程的2中調用instantiateReactComponent建立了,組件的實例,其實組件類型有四種,具體看下圖:
在這裏咱們仍是看一下它的具體實現,而後分析一下過程:function instantiateReactComponent(node, shouldHaveDebugID) {
var instance;
if (node === null || node === false) {
// 空組件
instance = ReactEmptyComponent.create(instantiateReactComponent);
} else if (typeof node === 'object') {
var element = node;
if (typeof element.type === 'string') {
// 原生DOM
instance = ReactHostComponent.createInternalComponent(element);
} else if (isInternalComponentType(element.type)) {
instance = new element.type(element);
} else {
// react組件
instance = new ReactCompositeComponentWrapper(element);
}
} else if (typeof node === 'string' || typeof node === 'number') {
// 文本字符串
instance = ReactHostComponent.createInstanceForText(node);
} else {
}
return instance;
}
1.node爲空時初始化空組件ReactEmptyComponent.create(instantiateReactComponent)
2.node類型是對象時,便是DOM標籤或者自定義組件,那麼若是element的類型是字符串,則初始化DOM標籤組件ReactNativeComponent.createInternalComponent,不然初始化自定義組件ReactCompositeComponentWrapper
3.當node是字符串或者數字時,初始化文本組件ReactNativeComponent.createInstanceForText
4.其餘狀況不處理
複製代碼
在_renderNewRootComponent代碼中有一個方法後面我是打了星號的,batchedUpdate方法的第一個參數實際上是個callback,這裏也就是batchedMountComponentIntoNode,從方法名就能夠很容易看出來他是一個批次裝載組件的方法,他是定義在ReactMount上面的,來看一下他的具體實現吧。
function batchedMountComponentIntoNode(
componentInstance,
container,
shouldReuseMarkup,
context,
) {
// 在batchedMountComponentIntoNode中,使用transaction.perform調用mountComponentIntoNode讓其基於事務機制進行調用
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
!shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement,
);
transaction.perform(
mountComponentIntoNode,
null,
componentInstance,
container,
transaction,
shouldReuseMarkup,
context,
);
ReactUpdates.ReactReconcileTransaction.release(transaction);
}
複製代碼
事務機制之後再進行分析,這裏就直接來看mountComponentIntoNode是如何將組件渲染成DOM節點的吧。
mountComponentIntoNode這個函數主要就是裝載組件,而且將其插入到DOM中,話很少說,直接上源碼,而後根據源碼一步步的分析:
/**
* Mounts this component and inserts it into the DOM.
*
* @param {ReactComponent} componentInstance The instance to mount.
* @param {DOMElement} container DOM element to mount into.
* @param {ReactReconcileTransaction} transaction
* @param {boolean} shouldReuseMarkup If true, do not insert markup
*/
function mountComponentIntoNode(
wrapperInstance,
container,
transaction,
shouldReuseMarkup,
context,
) {
var markup = ReactReconciler.mountComponent(
wrapperInstance,
transaction,
null,
ReactDOMContainerInfo(wrapperInstance, container),
context,
);
wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
ReactMount._mountImageIntoNode(
markup,
container,
wrapperInstance,
shouldReuseMarkup,
transaction,
);
}
複製代碼
能夠看到mountComponentIntoNode方法首先調用了ReactReconciler.mountComponent方法,而在ReactReconciler.mountComponent方法中實際上是調用了上面四種react組件的mountComponent方法,前面的就不說了,咱們直接來看一下四種組件中的mountComponent方法都幹了什麼吧。
/src/renderers/dom/shared/ReactDOMComponent.js
mountComponent: function (
transaction,
hostParent,
hostContainerInfo,
context,
) {
var props = this._currentElement.props;
switch (this._tag) {
case 'audio':
case 'form':
case 'iframe':
case 'img':
case 'link':
case 'object':
case 'source':
case 'video':
....
// 建立容器
var mountImage;
var ownerDocument = hostContainerInfo._ownerDocument;
var el;
if (this._tag === 'script') {
var div = ownerDocument.createElement('div');
var type = this._currentElement.type;
div.innerHTML = `<${type}></${type}>`;
el = div.removeChild(div.firstChild);
} else if (props.is) {
el = ownerDocument.createElement(this._currentElement.type, props.is);
} else {
el = ownerDocument.createElement(this._currentElement.type);
}
}
// 更新props,第一個參數是上次的props,第二個參數是最新的props,若是上一次的props爲空那麼就是新建狀態
this._updateDOMProperties(null, props, transaction);
// 生成DOMLazyTree對象
var lazyTree = DOMLazyTree(el);
// 處理孩子節點
this._createInitialChildren(transaction, props, context, lazyTree);
mountImage = lazyTree;
// 返回容器
return mountImage;
}
複製代碼
總結一下上述代碼的執行過程,在這裏我只截取了初次渲染時候執行的代碼: 1.對特殊的標籤進行處理,而且調用方法給出相應警告 2.建立DOM節點 3.調用_updateDOMProperties方法來處理props 4.生成DOMLazyTree 5.經過DOMLazyTree調用_createInitialChildren處理孩子節點。而後返回DOM節點
下面咱們來看一下這個DOMLazyTree方法都幹了些什麼,仍是上源碼:
function queueChild(parentTree, childTree) {
if (enableLazy) {
parentTree.children.push(childTree);
} else {
parentTree.node.appendChild(childTree.node);
}
}
function queueHTML(tree, html) {
if (enableLazy) {
tree.html = html;
} else {
setInnerHTML(tree.node, html);
}
}
function queueText(tree, text) {
if (enableLazy) {
tree.text = text;
} else {
setTextContent(tree.node, text);
}
}
function toString() {
return this.node.nodeName;
}
function DOMLazyTree(node) {
return {
node: node,
children: [],
html: null,
text: null,
toString,
};
}
DOMLazyTree.queueChild = queueChild;
DOMLazyTree.queueHTML = queueHTML;
DOMLazyTree.queueText = queueText;
複製代碼
從上述代碼能夠看到DOMLazyTree其實就是一個用來包裹節點信息的對象,裏面有孩子節點,html節點,文本節點,而且提供了將這些節點插入到真是DOM中的方法,如今咱們來看一下在_createInitialChildren方法中它是如何來使用這個lazyTree對象的:
_createInitialChildren: function (transaction, props, context, lazyTree) {
var innerHTML = props.dangerouslySetInnerHTML;
if (innerHTML != null) {
if (innerHTML.__html != null) {
DOMLazyTree.queueHTML(lazyTree, innerHTML.__html);
}
} else {
var contentToUse = CONTENT_TYPES[typeof props.children]
? props.children
: null;
var childrenToUse = contentToUse != null ? null : props.children;
if (contentToUse != null) {
if (contentToUse !== '') {
DOMLazyTree.queueText(lazyTree, contentToUse);
}
} else if (childrenToUse != null) {
var mountImages = this.mountChildren(
childrenToUse,
transaction,
context,
);
for (var i = 0; i < mountImages.length; i++) {
DOMLazyTree.queueChild(lazyTree, mountImages[i]);
}
}
}
}
複製代碼
判斷當前節點的dangerouslySetInnerHTML屬性、孩子節點是否爲文本和其餘節點分別調用DOMLazyTree的queueHTML、queueText、queueChild.
在實例調用mountComponent時,在這裏額外的說一下這個函數的執行過程,ReactCompositeComponent也就是咱們說的react自定義組件,起主要的執行過程以下:
1.處理props、contex等變量,調用構造函數建立組件實例
2.判斷是否爲無狀態組件,處理state
3.調用performInitialMount生命週期,處理子節點,獲取markup。
4.調用componentDidMount生命週期
複製代碼
在performInitialMount函數中,首先調用了componentWillMount生命週期,因爲自定義的React組件並非一個真實的DOM,因此在函數中又調用了孩子節點的mountComponent。這也是一個遞歸的過程,當全部孩子節點渲染完成後,返回markup並調用componentDidMount.
在上述mountComponentIntoNode中最後一步是執行_mountImageIntoNode方法,在該方法中核心的渲染方法就是insertTreeBefore,咱們直接來看這個方法的源碼,而後進行分析:
var insertTreeBefore = function(
parentNode,
tree,
referenceNode,
) {
if (
tree.node.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE ||
(tree.node.nodeType === ELEMENT_NODE_TYPE &&
tree.node.nodeName.toLowerCase() === 'object' &&
(tree.node.namespaceURI == null ||
tree.node.namespaceURI === DOMNamespaces.html))
) {
insertTreeChildren(tree);
parentNode.insertBefore(tree.node, referenceNode);
} else {
parentNode.insertBefore(tree.node, referenceNode);
insertTreeChildren(tree);
}
}
function insertTreeChildren(tree) {
if (!enableLazy) {
return;
}
var node = tree.node;
var children = tree.children;
if (children.length) {
for (var i = 0; i < children.length; i++) {
insertTreeBefore(node, children[i], null);
}
} else if (tree.html != null) {
setInnerHTML(node, tree.html);
} else if (tree.text != null) {
setTextContent(node, tree.text);
}
}
複製代碼
1.該方法首先就是判斷當前節點是否是fragment節點或者Object插件 2.若是知足條件1,首先調用insertTreeChildren將此節點的孩子節點渲染到當前節點上,再將渲染完的節點插入到html 3.若是不知足1,是其餘節點,先將節點插入到插入到html,再調用insertTreeChildren將孩子節點插入到html
在此過程當中已經一次調用了setInnerHTML或setTextContent來分別渲染html節點和文本節點。
上述文章就是react的初次渲染過程分析,若是有哪些地方寫的不對,歡迎在評論中討論。本文代碼採用的react15中的代碼,和react最新版本代碼會有一些的出入。