React.js核心原理实现:首次渲染机制
2019獨角獸企業重金招聘Python工程師標準>>>
一、前言
? ? ?react.js和vue.js無疑是當下最火的js框架了,它們為組件式的開發傳統前端頁面、SPA頁面、前后端分離等帶來了有力的支持。react.js源碼實現較為復雜(據說FaceBook的react團隊目前正在全面重寫react實現),如果直接通過閱讀react源碼理解其原理還是比較有難度的,因此,通過把react核心概念自己手動實現一遍,那么既可以避開react源碼的復雜性又可以加深自己的理解。其實,react核心概念并不多:
- 虛擬dom對象(Virtual DOM)
- 虛擬dom差異化算法(diff algorithm)
- 單向數據流渲染(Data Flow)
- 組件生命周期
- 事件處理
? ? ?聲明:
- 本文假定你已經對reactjs有了一定的了解,如果沒有至少看下ruanyifeng老師的入門demo。
- jsx不在本文的討論范圍,所有的例子原理都是使用原生的javascript。
- 篇幅限制,服務器端的reactjs也不在本文討論范圍內(服務端渲染)。
- 為了演示方便,本文以jQuery作為基本工具庫。
- 為了更清晰的演示原理,本文會忽略很多細節的東西,千萬不要用于生產環境。
? ? ?所有實例源碼都托管在github。點這里里面有分步驟的例子,可以一邊看一邊運行例子。
二、入門實例
? ? ? 以向div渲染一個“hello world”文本開始:
<script type="text/javascript"> React.render('hello world',document.getElementById("container")) </script><div id="container"></div>/** 生成后的html為:<div id="container"><span data-reactid="0">hello world</span> </div>*/? ? ? 可以看到,上面最關鍵的代碼就是:React.render,下面看其實現:
// component類,用來表示文本在渲染,更新,刪除時應該做些什么事情 function ReactDOMTextComponent(text) {// 存下當前的字符串(該component綁定的element)this._currentElement = '' + text;// 用來標識當前component的Idthis._rootNodeID = null; }// component渲染時生成的dom結構 ReactDOMTextComponent.prototype.mountComponent = function(rootID) {this._rootNodeID = rootID;// 生成domreturn '<span data-reactid="' + rootID + '">' + this._currentElement + '</span>'; }// component工廠 用來根據element類型返回一個相應的component實例 function instantiateReactComponent(node){// 如果傳進來的node是字符串或者是一個數值if(typeof node === 'string' || typeof node === 'number'){// 就創建并返回一個文本componentreturn new ReactDOMTextComponent(node)} }React = {nextReactRootIndex:0,render:function(element,container){// 根據element返回一個componentvar componentInstance = instantiateReactComponent(element);// 渲染生成dom結構var markup = componentInstance.mountComponent(React.nextReactRootIndex++);// 插入到容器中$(container).html(markup);// 觸發完成mount的事件$(document).trigger('mountReady'); } }? ? ? 代碼主要分為三個部分:
1、React.render 作為渲染的入口
2、引入了component類的概念,ReactDOMTextComponent是一個component類定義,它是一個文本類型的component。component提供了在渲染,更新,刪除時應該對element做什么操作,由于目前只用到渲染,另外兩個可以先忽略。
3、instantiateReactComponent用來根據element的類型(現在只有一種string類型),返回一個component的實例。其實就是個類工廠。
? ? ?nextReactRootIndex作為每個component的標識id,不斷加1,確保唯一性。這樣我們以后可以通過這個標識找到這個元素。
? ?? 可以看到我們把邏輯分為幾個部分,主要的渲染邏輯放在了具體的componet類去定義(只有component自己最清楚如何渲染自己)。React.render負責調度整個流程,這里是調用instantiateReactComponent生成一個對應component類型的實例對象,然后調用此對象的mountComponent獲取生成的內容(dom結構)。最后插入到對應的container節點中。
? ? 插播:上面的代碼使用了javascript原型鏈,不熟悉原型鏈的可以看下面這張圖:
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
三、基本元素類型element
? ? reactjs最大的賣點就是它的虛擬dom概念,一般使用React.createElement來創建一個虛擬dom元素。虛擬dom元素分為兩種,一種是瀏覽器自帶的基本元素比如 div p input form 這種,一種是自定義的元素。理論上文本節點不算虛擬dom,但是reac.js為了保持渲染的一致性,在文本節點外面包了一層span標記,也給它配了個簡化版component(ReactDOMTextComponent)。
? ? 本節只討論基本元素(element)。
? ??在reactjs里,當我們希望在hello world外面包一層div,并且帶上一些屬性,甚至事件時我們可以這么寫:
//演示事件監聽怎么用 function hello(){alert('hello') }// 創建一個基本元素element var element = React.createElement('div',{id:'test',onclick:hello},'click me')React.render(element,document.getElementById("container"))/**生成的html為:<div data-reactid="0" id="test"><span data-reactid="0.0">click me</span> </div>點擊文字,會彈出hello的對話框*/? ? ?上面使用React.createElement創建了一個基本元素,下面來看看簡易版本React.createElement的實現:
//ReactElement就是虛擬dom的概念,具有一個type屬性代表當前的節點類型,還有節點的屬性props //比如對于div這樣的節點type就是div,props就是那些attributes //另外這里的key,可以用來標識這個element,用于優化以后的更新,這里可以先不管,知道有這么個東西就好了 function ReactElement(type,key,props){this.type = type;this.key = key;this.props = props; }React = {nextReactRootIndex:0,// createElement函數定義createElement:function(type,config,children){var props = {},propName;config = config || {}// 看有沒有key,用來標識element的類型,方便以后高效的更新,這里可以先不管var key = config.key || null;// 復制config里的內容到propsfor (propName in config) {if (config.hasOwnProperty(propName) && propName !== 'key') {props[propName] = config[propName];}}// 處理children,全部掛載到props的children屬性上// 支持兩種寫法,如果只有一個參數,直接賦值給children,否則做合并處理var childrenLength = arguments.length - 2;if (childrenLength === 1) {props.children = $.isArray(children) ? 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;}// 創建新的ReactElementreturn new ReactElement(type,key,props);},render:function(element,container){var componentInstance = instantiateReactComponent(element);var markup = componentInstance.mountComponent(React.nextReactRootIndex++);$(container).html(markup);//觸發完成mount的事件$(document).trigger('mountReady');} }? ? ?createElement只是做了簡單的參數修正,最終返回一個ReactElement實例對象也就是我們說的虛擬元素的實例。這里注意key的定義,主要是為了以后更新時優化效率,這邊可以先不管忽略。
? ? ?有了元素實例,得把他渲染出來,此時render接受的是一個ReactElement而不是文本,先改造下instantiateReactComponent:
function instantiateReactComponent(node){//文本節點的情況if(typeof node === 'string' || typeof node === 'number'){return new ReactDOMTextComponent(node);}//瀏覽器基本element,注意基本類型element的type一定是字符串,可以和自定義element時對比if(typeof node === 'object' && typeof node.type === 'string'){//注意這里,使用了一種新的componentreturn new ReactDOMComponent(node);} }? ? ? 這里增加了一個判斷,這樣當render的不是文本而是瀏覽器的基本元素時。就使用另外一種component(ReactDOMComponent)來處理它渲染時應該返回的內容。這里就體現了工廠方法instantiateReactComponent的好處了,不管來了什么類型的node,都可以負責生產出一個負責渲染的component實例。這樣render完全不需要做任何修改,只需要再做一種對應的component類型(這里是ReactDOMComponent)就行了。
? ? ? 所以重點我們來看看ReactDOMComponent的具體實現:
//component類,用來表示文本在渲染,更新,刪除時應該做些什么事情 function ReactDOMComponent(element){//存下當前的element對象引用this._currentElement = element;this._rootNodeID = null; }//component渲染時生成的dom結構 ReactDOMComponent.prototype.mountComponent = function(rootID){// 標識this._rootNodeID = rootID;// element屬性var props = this._currentElement.props;// 開始構造dom結構的開始和結束標簽var tagOpen = '<' + this._currentElement.type;var tagClose = '</' + this._currentElement.type + '>';// 加上reactid標識,reactid=_rootNodeIDtagOpen += ' data-reactid=' + this._rootNodeID;// 拼湊出屬性for (var propKey in props) {// 這里要做一下事件的監聽,就是從屬性props里面解析拿出on開頭的事件屬性的對應事件監聽if (/^on[A-Za-z]/.test(propKey)) {// 事件類型var eventType = propKey.replace('on', '');// 針對當前的節點添加事件代理,代理了reactid=_rootNodeID子節點的事件$(document).delegate('[data-reactid="' + this._rootNodeID + '"]', eventType + '.' + this._rootNodeID, props[propKey]);}// 對于children屬性以及事件屬性不需要進行字符串拼接// 事件會代理到全局。這邊不能拼到dom上不然會產生原生的事件監聽// children屬性會在下面遞歸處理if (props[propKey] && propKey != 'children' && !/^on[A-Za-z]/.test(propKey)) {tagOpen += ' ' + propKey + '=' + props[propKey];}}// 獲取子節點渲染出的內容var content = '';var children = props.children || [];// 用于保存所有的子節點的componet實例,以后會用到var childrenInstances = []; var that = this;$.each(children, function(key, child) {// 這里再次調用了instantiateReactComponent實例化子節點component類,拼接好返回var childComponentInstance = instantiateReactComponent(child);childComponentInstance._mountIndex = key;// 將子節點實例緩存到childrenInstanceschildrenInstances.push(childComponentInstance);// 子節點的rootId是父節點的rootId加上新的key也就是順序的值拼成的新值var curRootId = that._rootNodeID + '.' + key;// 得到子節點的渲染內容var childMarkup = childComponentInstance.mountComponent(curRootId);// 拼接在一起content += ' ' + childMarkup;})// 留給以后更新時用的這邊先不用管this._renderedChildren = childrenInstances;// 拼出整個html內容return tagOpen + '>' + content + tagClose; }? ? ??增加了虛擬dom reactElement的定義,增加了一個新的componet類ReactDOMComponent。這樣我們就實現了渲染瀏覽器基本元素的功能了。
? ? ? 對于虛擬dom的渲染邏輯,本質上是使用了遞歸,reactElement會遞歸渲染自己的子節點。可以看到我們通過instantiateReactComponent屏蔽了子節點的差異,只需要使用不同的componet類,這樣都能保證通過mountComponent最終拿到渲染后的內容。
? ? ? 另外這邊的事件也要說下,可以在傳遞props的時候傳入{onClick:function(){}}這樣的參數,這樣就會在當前元素上添加事件,代理到document。由于reactjs本身全是在寫js,所以監聽的函數的傳遞變得特別簡單。
? ? ? 這里很多東西沒有考慮,比如一些特殊的類型input select等等,再比如img不需要有對應的tagClose等。這里為了保持簡單就不再擴展了。另外reactjs的事件處理其實很復雜,實現了一套標準的w3c事件。這里偷懶直接使用jQuery的事件代理到document上了。
四、自定義類型element
? ? ? 本節來看看自定義類型element的渲染原理。
? ? ? 下面先來看看在React中如何定義一個自定義元素element:
// 自定義元素element var HelloMessage = React.createClass({// 初始狀態getInitialState: function() {return {type: 'say:'};},// 生命周期:將要掛載時調用componentWillMount: function() {console.log('我就要開始渲染了。。。')},// 生命周期:已經掛載之后調用componentDidMount: function() {console.log('我已經渲染好了。。。')},// 渲染并返回一個虛擬dom(包括element和text)render: function() {return React.createElement("div", null,this.state.type, "Hello ", this.props.name);} });React.render(React.createElement(HelloMessage, {name: "John"}), document.getElementById("container"));/** 結果為:html: <div data-reactid="0"><span data-reactid="0.0">say:</span><span data-reactid="0.1">Hello </span><span data-reactid="0.2">John</span> </div>console: 我就要開始渲染了。。。 我已經渲染好了。。。*/? ? ?可以看到,這里的createElement函數第一個參數的類型不再是字符串,而是一個class,
? ? ?React.createClass生成一個自定義標記類,帶有基本的生命周期:
- getInitialState :獲取最初的屬性值this.state
- componentWillmount :在組件準備渲染時調用
- componentDidMount :在組件渲染完成后調用
? ? ?下面就來看看React.createClass的實現吧:
// 定義ReactClass類,所有自定義的超級父類 var ReactClass = function(){ } // 留給子類去繼承覆蓋 ReactClass.prototype.render = function(){}React = {nextReactRootIndex:0,// 創建自定義類createClass:function(spec){// 生成一個子類var Constructor = function (props) {this.props = props;this.state = this.getInitialState ? this.getInitialState() : null;}// 原型繼承,繼承超級父類Constructor.prototype = new ReactClass();Constructor.prototype.constructor = Constructor;// 混入spec到原型$.extend(Constructor.prototype,spec);return Constructor;},createElement:function(type,config,children){...},render:function(element,container){...} }? ? ?可以看到createClass生成了一個繼承ReactClass的子類,在構造函數里調用this.getInitialState獲得最初的state。
為了演示方便,我們這邊的ReactClass相當簡單,實際上原始的代碼處理了很多東西,比如類的mixin的組合繼承支持,比如componentDidMount等可以定義多次,需要合并調用等等,有興趣的去翻源碼吧,不是本文的主要目的,這里就不詳細展開了。
? ? ? 我們這里只是返回了一個繼承類的定義,那么具體的componentWillmount,這些生命周期函數在哪里調用呢。
? ? ? 看看我們上面的兩種類型就知道,我們是時候為自定義元素也提供一個componet類了,在那個類里我們會實例化ReactClass,并且管理生命周期,還有父子組件依賴。
好,我們老規矩先改造instantiateReactComponent
function instantiateReactComponent(node){// 文本節點的情況if(typeof node === 'string' || typeof node === 'number'){return new ReactDOMTextComponent(node);}// 瀏覽器默認節點的情況if(typeof node === 'object' && typeof node.type === 'string'){//注意這里,使用了一種新的componentreturn new ReactDOMComponent(node);}// 自定義的元素節點,類型為構造函數if(typeof node === 'object' && typeof node.type === 'function'){// 注意這里,使用新的component,專門針對自定義元素return new ReactCompositeComponent(node);} }? ? 很簡單我們增加了一個判斷,使用新的component類形來處理自定義的節點。我們看下ReactCompositeComponent的具體實現:?
function ReactCompositeComponent(element){//存放元素element對象this._currentElement = element;//存放唯一標識this._rootNodeID = null;//存放對應的ReactClass的實例this._instance = null; }//用于返回當前自定義元素渲染時應該返回的內容 ReactCompositeComponent.prototype.mountComponent = function(rootID){this._rootNodeID = rootID;//拿到當前元素對應的屬性值var publicProps = this._currentElement.props;//拿到對應的ReactClassvar ReactClass = this._currentElement.type;// Initialize the public classvar inst = new ReactClass(publicProps);this._instance = inst;//保留對當前comonent的引用,下面更新會用到inst._reactInternalInstance = this;// 如果自定義元素設置了componentWillMount生命周期if (inst.componentWillMount) {inst.componentWillMount();//這里在原始的reactjs其實還有一層處理,就是 componentWillMount調用setstate,不會觸發rerender而是自動提前合并,這里為了保持簡單,就略去了}//調用ReactClass的實例的render方法,返回一個element或者一個文本節點var renderedElement = this._instance.render();//得到renderedElement對應的component類實例var renderedComponentInstance = instantiateReactComponent(renderedElement);this._renderedComponent = renderedComponentInstance; //存起來留作后用//拿到渲染之后的字符串內容,將當前的_rootNodeID傳給render出的節點var renderedMarkup = renderedComponentInstance.mountComponent(this._rootNodeID);//之前我們在React.render方法最后觸發了mountReady事件,所以這里可以監聽,在渲染完成后會觸發。$(document).on('mountReady', function() {//調用inst.componentDidMountinst.componentDidMount && inst.componentDidMount();});return renderedMarkup; }? ? ? 實現并不難,ReactClass的render一定是返回一個虛擬節點(包括element和text),這個時候我們使用instantiateReactComponent去得到實例,再使用mountComponent拿到結果作為當前自定義元素的結果。
? ? ? 應該說本身自定義元素不負責具體的內容,他更多的是負責生命周期。具體的內容是由它的render方法返回的虛擬節點來負責渲染的。
? ? ?本質上也是遞歸的去渲染內容的過程。同時因為這種遞歸的特性,父組件的componentWillMount一定在某個子組件的componentWillMount之前調用,而父組件的componentDidMount肯定在子組件之后,因為監聽mountReady事件,肯定是子組件先監聽的。
需要注意的是自定義元素并不會處理我們createElement時傳入的子節點,它只會處理自己render返回的節點作為自己的子節點。不過我們在render時可以使用this.props.children拿到那些傳入的子節點,可以自己處理。其實有點類似webcomponents里面的shadow dom的作用。
? ? ? 上面實現了三種類型的元素,其實我們發現本質上沒有太大的區別,都是有自己對應component類來處理自己的渲染過程。
? ??本文源碼實現以及參考內容來自于: ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?http://purplebamboo.github.io/2015/09/15/reactjs_source_analyze_part_one/
轉載于:https://my.oschina.net/fileoptions/blog/885316
總結
以上是生活随笔為你收集整理的React.js核心原理实现:首次渲染机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 201521123087 《Java程序
- 下一篇: angular js一factory,s