網站首頁 編程語言 正文
適合人群
本文適合0.5~3年的react開發人員的進階。
講講廢話:
react的源碼,的確是比vue的難度要深一些,本文也是針對初中級,本意了解整個react的執行過程。
寫源碼之前的必備知識點
JSX
首先我們需要了解什么是JSX。
網絡大神的解釋:React 使用 JSX 來替代常規的 JavaScript。JSX 是一個看起來很像 XML 的 JavaScript 語法擴展。
是的,JSX是一種js的語法擴展,表面上像HTML,本質上還是通過babel轉換為js執行。再通俗的一點的說,jsx就是一段js,只是寫成了html的樣子,而我們讀取他的時候,jsx會自動轉換成vnode對象給我們,這里都由react-script的內置的babel幫助我們完成。
簡單舉個栗子:
return (
<div>
Hello Word </div>
)
實際上是:
return React.createElement(
"div",
null,
"Hello"
)
JSX本質上就是轉換為React.createElement在React內部構建虛擬Dom,最終渲染出頁面。
虛擬Dom
這里說明一下react的虛擬dom。react的虛擬dom跟vue的大為不同。vue的虛擬dom是為了是提高渲染效率,而react的虛擬dom是一定需要。很好理解,vue的template本身就是html,可以直接顯示。而jsx是js,需要轉換成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繼續講解。
原理簡介
我們寫一個react的最簡單的源碼:
import React from 'react'
import ReactDOM from 'react-dom'
function App(props){
return <div>你好</div>
</div>
}
ReactDOM.render(<App/>, document.getElementById('root'))
React負責邏輯控制,數據 -> VDOM
首先,我們可以看到每一個js文件中,都一定會引入import React from ‘react’。但是我們的代碼里邊,根本沒有用到React。但是你不引入他就報錯了。
為什么呢?可以這樣理解,在我們上述的js文件中,我們使用了jsx。但是jsx并不能給編譯,所以,報錯了。這時候,需要引入react,而react的作用,就是把jsx轉換為“虛擬dom”對象。
JSX本質上就是轉換為React.createElement在React內部構建虛擬Dom,最終渲染出頁面。而引入React,就是為了時限這個過程。
ReactDom渲染實際DOM,VDOM -> DOM
理解好這一步,我們再看ReactDOM。React將jsx轉換為“虛擬dom”對象。我們再利用ReactDom的虛擬dom通過render函數,轉換成dom。再通過插入到我們的真是頁面中。
這就是整個mini react的一個簡述過程。相關參考視頻講解:進入學習
手寫react過程
基本架子的搭建
react的功能化問題,暫時不考慮。例如,啟動react,怎么去識別JSX,實現熱更新服務等等,我們的重點在于react自身。我們借用一下一下react-scripts插件。
有幾種種方式創建我們的基本架子:
利用 create-react-app zwz_react_origin快速搭建,然后刪除原本的react,react-dom等文件。(zwz_react_origin是我的項目名稱)
第二種,復制下邊代碼。新建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,他已經識別到obj是jsx。該jsx卻不能解析成虛擬dom, 此時我們的頁面就會報錯。通過資料的查閱,或者是源碼的跟蹤,我們可以知道,實際上,識別到jsx之后,會調用頁面中的createElement轉換為虛擬dom。
我們import React,看看打印出來什么?
+ import React from "react";
let obj = (
<div>
<div className="class_0">你好</div>
</div>
);
console.log(`obj:${ JSON.stringify( obj) }`);
結果:
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":{}}
由上邊結論可以知道, babel會識別到我們的jsx,通過createElement并將其dom(html語法)轉換為虛擬dom。從上述的過程,我們可以看到虛擬dom的組成,由type,key,ref,props組成。我們來模擬react的源碼。
此時我們已經知道react中的createElement的作用是什么,我們可以嘗試著自己來寫一個createElement(新建react.js引入并手寫下邊代碼):
function createElement() {
console.log("createElement", arguments);
}
export default {
createElement,
};
此時的打印結果:
我們可以看出對象傳遞的時候,dom的格式,先傳入type, 然后props屬性,我們根據原本react模擬一下這個對象轉換的打印:
function createElement(type, props, ...children) {
props.children = children;
return {
type,
props,
};
}
這樣,我們已經把最簡版的一個react實現,我們下邊繼續看看如何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到對應的元素,整個簡版react的就已經完成,頁面就會完成渲染。首先,jsx我們已經知道是一個vnode,而第二個元素即是渲染上頁面的元素,假設我們的元素是一個html原生標簽div。
我們新建一個reactDom.js引入。
function render(vnode, container) {
mount(vnode, container);
}
function mount(vnode, container){
const { type, props } = vnode;
const node = document.createElement(type);//創建一個真實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渲染已經完成。我們優化一下。
首先,這個過程中, className="class_0"消失了。我們想辦法渲染上頁面。此時,虛擬dom的對象,沒有辦法,區分,哪些元素分別帶有什么屬性,我們在轉義的時候優化一下mount。
function mount(vnode, container){
const { type, props } = vnode;
const node = document.createElement(type);//創建一個真實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]);
}
});
// +結束
container.appendChild(node);
}
ReactDom.Component
看到這里,整個字符串render到頁面渲染的過程已完成。此時入口文件已經解決了。對于原始標簽div, h1已經兼容。但是對于自定義標簽呢?或者怎么完成組件化呢。
我們先看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其實跟原本組件不一樣的地方,在于他是個函數,而原本的jsx,是一個字符串。我們可以根據這個特點,將函數轉換為字符串,那么Function組件即跟普通標簽同一性質。
我們寫一個方法:
mountFunc(vnode, container);
function mountFunc(vnode, container) {
const { type, props } = vnode;
const node = new type(props);
mount(node, container);
}
此時type即是函數體內容,我們只需要實例化一下,即可跟拿到對應的字符串,即是普通的vnode。再利用我們原來的vnode轉換方法,即可實現。
按照這個思路,如果我們不考慮生命周期等相對復雜的東西。我們也相對簡單,只需拿到類中的render函數即可。
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統一封裝了,setState,forceUpdate方法,記錄了props,state,refs等。我們模擬一份簡版為栗子:
class Component {
static isReactComponent = true;
constructor(props) {
this.props = props;
this.state = {};
}
setState = () => {};
}
再添加一個標識,isReactComponent表示是函數數組件化。這樣的話,我們就可以區分出:普通標簽,函數組件標簽,類組件標簽。
我們可以重構一下createElement方法,多定義一個vtype屬性,分別表示
- 普通標簽
- 函數組件標簽
- 類組件標簽
根據上述標記,我們可改造為:
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); //處理原生標簽
}
if (vtype === 2) {
//處理class組件
mountClass(vnode, container);
}
if (vtype === 3) {
//處理函數組件
mountFunc(vnode, container);
}
}
至此,我們已經完成一個簡單可組件化的react源碼。不過,此時有個bug,就是文本元素的時候異常,因為文本元素不帶標簽。我們優化一下。
function mount(vnode, container) {
const { vtype } = vnode;
if (!vtype) {
mountTextNode(vnode, container); //處理文本節點
}
//vtype === 1
//vtype === 2
// ....
}
//處理文本節點
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); //處理文本節點
}
if (vtype === 1) {
mountHtml(vnode, container); //處理原生標簽
}
if (vtype === 3) {
//處理函數組件
mountFunc(vnode, container);
}
if (vtype === 2) {
//處理class組件
mountClass(vnode, container);
}
}
//處理文本節點
function mountTextNode(vnode, container) {
const node = document.createTextNode(vnode);
container.appendChild(node);
}
//處理原生標簽
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,
};
原文鏈接:https://blog.csdn.net/It_kc/article/details/127582559
相關推薦
- 2022-04-12 C++中的異常實例詳解_C 語言
- 2022-06-27 Python使用re模塊實現okenizer(表達式分詞器)_python
- 2022-02-18 matplotlib的legend參數與設置
- 2022-08-25 高級消息隊列協議AMQP簡介_其它綜合
- 2022-03-16 Docker安裝Nginx問題及錯誤分析_docker
- 2022-08-28 使用Freemarker生成靜態頁面
- 2022-06-28 C#遞歸算法和排列算法_C#教程
- 2022-12-10 Android入門之計時器Chronometer的使用教程_Android
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支