React運行機制超詳細(xì)講解
適合人群
本文適合0.5~3年的react開發(fā)人員的進階。
講講廢話:
react的源碼,的確是比vue的難度要深一些,本文也是針對初中級,本意了解整個react的執(zhí)行過程。
寫源碼之前的必備知識點
JSX
首先我們需要了解什么是JSX。
網(wǎng)絡(luò)大神的解釋:React 使用 JSX 來替代常規(guī)的 JavaScript。JSX 是一個看起來很像 XML 的 JavaScript 語法擴展。
是的,JSX是一種js的語法擴展,表面上像HTML,本質(zhì)上還是通過babel轉(zhuǎn)換為js執(zhí)行。再通俗的一點的說,jsx就是一段js,只是寫成了html的樣子,而我們讀取他的時候,jsx會自動轉(zhuǎn)換成vnode對象給我們,這里都由react-script的內(nèi)置的babel幫助我們完成。
簡單舉個栗子:
return ( <div> Hello Word </div> ) 實際上是: return React.createElement( "div", null, "Hello" )
JSX本質(zhì)上就是轉(zhuǎn)換為React.createElement在React內(nèi)部構(gòu)建虛擬Dom,最終渲染出頁面。
虛擬Dom
這里說明一下react的虛擬dom。react的虛擬dom跟vue的大為不同。vue的虛擬dom是為了是提高渲染效率,而react的虛擬dom是一定需要。很好理解,vue的template本身就是html,可以直接顯示。而jsx是js,需要轉(zhuǎn)換成html,所以用到虛擬dom。
我們描述一下react的最簡版的vnode:
function createElement(type, props, ...children) { props.children = children; return { type, props, children, }; }
這里的vnode也很好理解,
type表示類型,如div,span,
props表示屬性,如{id: 1, style:{color:red}},
children表示子元素
下邊會在createElement繼續(xù)講解。
原理簡介
我們寫一個react的最簡單的源碼:
import React from 'react' import ReactDOM from 'react-dom' function App(props){ return <div>你好</div> </div> } ReactDOM.render(<App/>, document.getElementById('root'))
React負(fù)責(zé)邏輯控制,數(shù)據(jù) -> VDOM
首先,我們可以看到每一個js文件中,都一定會引入import React from ‘react’。但是我們的代碼里邊,根本沒有用到React。但是你不引入他就報錯了。
為什么呢?可以這樣理解,在我們上述的js文件中,我們使用了jsx。但是jsx并不能給編譯,所以,報錯了。這時候,需要引入react,而react的作用,就是把jsx轉(zhuǎn)換為“虛擬dom”對象。
JSX本質(zhì)上就是轉(zhuǎn)換為React.createElement在React內(nèi)部構(gòu)建虛擬Dom,最終渲染出頁面。而引入React,就是為了時限這個過程。
ReactDom渲染實際DOM,VDOM -> DOM
理解好這一步,我們再看ReactDOM。React將jsx轉(zhuǎn)換為“虛擬dom”對象。我們再利用ReactDom的虛擬dom通過render函數(shù),轉(zhuǎn)換成dom。再通過插入到我們的真是頁面中。
這就是整個mini react的一個簡述過程。相關(guān)參考視頻講解:進入學(xué)習(xí)
手寫react過程
基本架子的搭建
react的功能化問題,暫時不考慮。例如,啟動react,怎么去識別JSX,實現(xiàn)熱更新服務(wù)等等,我們的重點在于react自身。我們借用一下一下react-scripts插件。
有幾種種方式創(chuàng)建我們的基本架子:
利用 create-react-app zwz_react_origin快速搭建,然后刪除原本的react,react-dom等文件。(zwz_react_origin是我的項目名稱)
第二種,復(fù)制下邊代碼。新建package.json
{ "name": "zwz_react_origin", "scripts": { "start": "react-scripts start" }, "version": "0.1.0", "private": true, "dependencies": { "react-scripts": "3.4.1" }, }
然后新建public下邊的index.html
<!DOCTYPE html> <html lang="en"> <head> </head> <body> <div id="root"></div> </body> </html>
再新建src下邊的index.js
這時候react-scripts會快速的幫我們定為到index.html以及引入index.js
import React from "react"; import ReactDOM from "react-dom"; let jsx = ( <div> <div className="">react啟動成功</div> </div> ); ReactDOM.render(jsx, document.getElementById("root"));
這樣,一個可以寫react源碼的輪子就出來了。
React的源碼
let obj = ( <div> <div className="class_0">你好</div> </div> ); console.log(`obj=${ JSON.stringify( obj) }`);
首先,我們上述代碼,如果我們不import React處理的話,我們可以打印出:
‘React’ must be in scope when using JSX react/react-in-jsx-scope
是的,編譯不下去,因為js文件再react-script,他已經(jīng)識別到obj是jsx。該jsx卻不能解析成虛擬dom, 此時我們的頁面就會報錯。通過資料的查閱,或者是源碼的跟蹤,我們可以知道,實際上,識別到j(luò)sx之后,會調(diào)用頁面中的createElement轉(zhuǎn)換為虛擬dom。
我們import React,看看打印出來什么?
+ import React from "react"; let obj = ( <div> <div className="class_0">你好</div> </div> ); console.log(`obj:${ JSON.stringify( obj) }`); 結(jié)果: jsx={"type":"div","key":null,"ref":null,"props":{"children":{"type":"div","key":null,"ref":null,"props":{"className":"class_0","children":"你好"},"_owner":null,"_store":{}}},"_owner":null,"_store":{}}
由上邊結(jié)論可以知道, babel會識別到我們的jsx,通過createElement并將其dom(html語法)轉(zhuǎn)換為虛擬dom。從上述的過程,我們可以看到虛擬dom的組成,由type,key,ref,props組成。我們來模擬react的源碼。
此時我們已經(jīng)知道react中的createElement的作用是什么,我們可以嘗試著自己來寫一個createElement(新建react.js引入并手寫下邊代碼):
function createElement() { console.log("createElement", arguments); } export default { createElement, };
此時的打印結(jié)果:
我們可以看出對象傳遞的時候,dom的格式,先傳入type, 然后props屬性,我們根據(jù)原本react模擬一下這個對象轉(zhuǎn)換的打印:
function createElement(type, props, ...children) { props.children = children; return { type, props, }; }
這樣,我們已經(jīng)把最簡版的一個react實現(xiàn),我們下邊繼續(xù)看看如何render到頁面
ReactDom.render
import React from "react"; + import ReactDOM from "react-dom"; let jsx = ( <div> <div className="class_0">你好</div> </div> ); // console.log(`jsx=${ JSON.stringify( jsx) }`); + ReactDOM.render(jsx, document.getElementById("root"));
如果此時,我們引入ReactDom,通過render到對應(yīng)的元素,整個簡版react的就已經(jīng)完成,頁面就會完成渲染。首先,jsx我們已經(jīng)知道是一個vnode,而第二個元素即是渲染上頁面的元素,假設(shè)我們的元素是一個html原生標(biāo)簽div。
我們新建一個reactDom.js引入。
function render(vnode, container) { mount(vnode, container); } function mount(vnode, container){ const { type, props } = vnode; const node = document.createElement(type);//創(chuàng)建一個真實dom const { children, ...rest } = props; children.map(item => {//子元素遞歸 if (Array.isArray(item)) { item.map(c => { mount(c, node); }); } else { mount(item, node); } }); container.appendChild(node); } //主頁: - import React from "react"; - import ReactDOM from "react-dom"; + import React from "./myReact/index.js"; + import ReactDOM from "./myReact/reactDom.js"; let jsx = ( <div> <div className="class_0">你好</div> </div> ); ReactDOM.render(jsx, document.getElementById("root"));
此時,我們可以看到頁面,我們自己寫的一個react渲染已經(jīng)完成。我們優(yōu)化一下。
首先,這個過程中, className="class_0"消失了。我們想辦法渲染上頁面。此時,虛擬dom的對象,沒有辦法,區(qū)分,哪些元素分別帶有什么屬性,我們在轉(zhuǎn)義的時候優(yōu)化一下mount。
function mount(vnode, container){ const { type, props } = vnode; const node = document.createElement(type);//創(chuàng)建一個真實dom const { children, ...rest } = props; children.map(item => {//子元素遞歸 if (Array.isArray(item)) { item.map(c => { mount(c, node); }); } else { mount(item, node); } }); // +開始 Object.keys(rest).map(item => { if (item === "className") { node.setAttribute("class", rest[item]); } if (item.slice(0, 2) === "on") { node.addEventListener("click", rest[item]); } }); // +結(jié)束 container.appendChild(node); }
ReactDom.Component
看到這里,整個字符串render到頁面渲染的過程已完成。此時入口文件已經(jīng)解決了。對于原始標(biāo)簽div, h1已經(jīng)兼容。但是對于自定義標(biāo)簽?zāi)??或者怎么完成組件化呢。
我們先看react16+的兩種組件化模式,一種是function組件化,一種是class組件化。
首先,我們先看看demo.
import React, { Component } from "react"; import ReactDOM from "react-dom"; class MyClassCmp extends React.Component { constructor(props) { super(props); } render() { return ( <div className="class_2" >MyClassCmp表示:{this.props.name}</div> ); } } function MyFuncCmp(props) { return <div className="class_1" >MyFuncCmp表示:{props.name}</div>; } let jsx = ( <div> <h1>你好</h1> <div className="class_0">前端小伙子</div> <MyFuncCmp /> <MyClassCmp /> </div> ); ReactDOM.render(jsx, document.getElementById("root"));
先看簡單點一些的Function組件。暫不考慮傳遞值等問題,Function其實跟原本組件不一樣的地方,在于他是個函數(shù),而原本的jsx,是一個字符串。我們可以根據(jù)這個特點,將函數(shù)轉(zhuǎn)換為字符串,那么Function組件即跟普通標(biāo)簽同一性質(zhì)。
我們寫一個方法:
mountFunc(vnode, container); function mountFunc(vnode, container) { const { type, props } = vnode; const node = new type(props); mount(node, container); }
此時type即是函數(shù)體內(nèi)容,我們只需要實例化一下,即可跟拿到對應(yīng)的字符串,即是普通的vnode。再利用我們原來的vnode轉(zhuǎn)換方法,即可實現(xiàn)。
按照這個思路,如果我們不考慮生命周期等相對復(fù)雜的東西。我們也相對簡單,只需拿到類中的render函數(shù)即可。
mountFunc(vnode, container); function mountClass(vnode, container) { const { type, props } = vnode; const node = new type(props); mount(node.render(), container); }
這里可能需注意,class組件,需要繼承React.Component。截圖一下react自帶的Component
可以看到,Component統(tǒng)一封裝了,setState,forceUpdate方法,記錄了props,state,refs等。我們模擬一份簡版為栗子:
class Component { static isReactComponent = true; constructor(props) { this.props = props; this.state = {}; } setState = () => {}; }
再添加一個標(biāo)識,isReactComponent表示是函數(shù)數(shù)組件化。這樣的話,我們就可以區(qū)分出:普通標(biāo)簽,函數(shù)組件標(biāo)簽,類組件標(biāo)簽。
我們可以重構(gòu)一下createElement方法,多定義一個vtype屬性,分別表示
- 普通標(biāo)簽
- 函數(shù)組件標(biāo)簽
- 類組件標(biāo)簽
根據(jù)上述標(biāo)記,我們可改造為:
function createElement(type, props, ...children) { props.children = children; let vtype; if (typeof type === "string") { vtype = 1; } if (typeof type === "function") { vtype = type.isReactComponent ? 2 : 3; } return { vtype, type, props, };
那么,我們處理時:
function mount(vnode, container) { const { vtype } = vnode; if (vtype === 1) { mountHtml(vnode, container); //處理原生標(biāo)簽 } if (vtype === 2) { //處理class組件 mountClass(vnode, container); } if (vtype === 3) { //處理函數(shù)組件 mountFunc(vnode, container); } }
至此,我們已經(jīng)完成一個簡單可組件化的react源碼。不過,此時有個bug,就是文本元素的時候異常,因為文本元素不帶標(biāo)簽。我們優(yōu)化一下。
function mount(vnode, container) { const { vtype } = vnode; if (!vtype) { mountTextNode(vnode, container); //處理文本節(jié)點 } //vtype === 1 //vtype === 2 // .... } //處理文本節(jié)點 function mountTextNode(vnode, container) { const node = document.createTextNode(vnode); container.appendChild(node); }
簡單源碼
package.json:
{ "name": "zwz_react_origin", "version": "0.1.0", "private": true, "dependencies": { "react": "^16.10.2", "react-dom": "^16.10.2", "react-scripts": "3.2.0" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": "react-app" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }}
index.js
import React from "./wzReact/"; import ReactDOM from "./wzReact/ReactDOM"; class MyClassCmp extends React.Component { constructor(props) { super(props); } render() { return ( <div className="class_2" >MyClassCmp表示:{this.props.name}</div> ); } } function MyFuncCmp(props) { return <div className="class_1" >MyFuncCmp表示:{props.name}</div>; } let jsx = ( <div> <h1>你好</h1> <div className="class_0">前端小伙子</div> <MyFuncCmp name="真帥" /> <MyClassCmp name="還有錢" /> </div> ); ReactDOM.render(jsx, document.getElementById("root"));
/wzReact/index.js
function createElement(type, props, ...children) { console.log("createElement", arguments); props.children = children; let vtype; if (typeof type === "string") { vtype = 1; } if (typeof type === "function") { vtype = type.isReactComponent ? 2 : 3; } return { vtype, type, props, }; } class Component { static isReactComponent = true; constructor(props) { this.props = props; this.state = {}; } setState = () => {}; } export default { Component, createElement, };
/wzReact/ReactDOM.js
function render(vnode, container) { console.log("render", vnode); //vnode-> node mount(vnode, container); // container.appendChild(node) } // vnode-> node function mount(vnode, container) { const { vtype } = vnode; if (!vtype) { mountTextNode(vnode, container); //處理文本節(jié)點 } if (vtype === 1) { mountHtml(vnode, container); //處理原生標(biāo)簽 } if (vtype === 3) { //處理函數(shù)組件 mountFunc(vnode, container); } if (vtype === 2) { //處理class組件 mountClass(vnode, container); } } //處理文本節(jié)點 function mountTextNode(vnode, container) { const node = document.createTextNode(vnode); container.appendChild(node); } //處理原生標(biāo)簽 function mountHtml(vnode, container) { const { type, props } = vnode; const node = document.createElement(type); const { children, ...rest } = props; children.map(item => { if (Array.isArray(item)) { item.map(c => { mount(c, node); }); } else { mount(item, node); } }); Object.keys(rest).map(item => { if (item === "className") { node.setAttribute("class", rest[item]); } if (item.slice(0, 2) === "on") { node.addEventListener("click", rest[item]); } }); container.appendChild(node); } function mountFunc(vnode, container) { const { type, props } = vnode; const node = new type(props); mount(node, container); } function mountClass(vnode, container) { const { type, props } = vnode; const cmp = new type(props); const node = cmp.render(); mount(node, container); } export default { render, };
到此這篇關(guān)于React運行機制超詳細(xì)講解的文章就介紹到這了,更多相關(guān)React運行機制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React Native第三方平臺分享的實例(Android,IOS雙平臺)
本篇文章主要介紹了React Native第三方平臺分享的實例(Android,IOS雙平臺),具有一定的參考價值,有興趣的可以了解一下2017-08-08react axios配置代理(proxy),如何解決本地開發(fā)時的跨域問題
這篇文章主要介紹了react axios配置代理(proxy),如何解決本地開發(fā)時的跨域問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07React實現(xiàn)下拉框的key,value的值同時傳送
這篇文章主要介紹了React實現(xiàn)下拉框的key,value的值同時傳送方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08React利用lazy+Suspense實現(xiàn)路由懶加載
這篇文章主要為大家詳細(xì)介紹了React如何利用lazy+Suspense實現(xiàn)路由懶加載,文中的示例代碼簡潔易懂,感興趣的小伙伴可以跟隨小編一起了解一下2023-06-06React中useCallback useMemo使用方法快速精通
在React函數(shù)組件中,當(dāng)組件中的props發(fā)生變化時,默認(rèn)情況下整個組件都會重新渲染。換句話說,如果組件中的任何值更新,整個組件將重新渲染,包括沒有更改values/props的函數(shù)/組件。在react中,我們可以通過memo,useMemo以及useCallback來防止子組件的rerender2023-02-02