本文適合0.5~3年的react開發人員的進階。html
講講廢話:前端
react的源碼,的確是比vue的難度要深一些,本文也是針對初中級,本意讓博友們瞭解整個react的執行過程。vue
上一篇,從mini源碼分析vue,也許深度比較通常,可是也花了好幾天的時間去彙總,明白一個博主的不易。這是筆者第一篇得到上百個贊,還有評論上的好評,還有臨時關注的幾十粉絲。這是我寫博客的動力。node
這周十分繁忙的條件下,仍是抽出時間彙總react。若是以爲不錯,仍是點個贊吧(三大步最喜歡了)react
如何還須要學習vue源碼以及vue相關知識點的朋友,建議移步:juejin.im/post/5f0326…git
首先咱們須要瞭解什麼是JSX。github
網絡大神的解釋:React 使用 JSX 來替代常規的 JavaScript。JSX 是一個看起來很像 XML 的 JavaScript 語法擴展。chrome
是的,JSX是一種js的語法擴展,表面上像HTML,本質上仍是經過babel轉換爲js執行。再通俗的一點的說,jsx就是一段js,只是寫成了html的樣子,而咱們讀取他的時候,jsx會自動轉換成vnode對象給咱們,這裏都由react-script的內置的babel幫助咱們完成。json
簡單舉個栗子:redux
return (
<div>
Hello Word
</div>
)
其實是:
return React.createElement(
"div",
null,
"Hello"
)
複製代碼
JSX本質上就是轉換爲React.createElement在React內部構建虛擬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'))
複製代碼
爲何呢?能夠這樣理解,在咱們上述的js文件中,咱們使用了jsx。可是jsx並不能給編譯,因此,報錯了。這時候,須要引入react,而react的做用,就是把jsx轉換爲「虛擬dom」對象。
JSX本質上就是轉換爲React.createElement在React內部構建虛擬Dom,最終渲染出頁面。而引入React,就是爲了時限這個過程。
理解好這一步,咱們再看ReactDOM。React將jsx轉換爲「虛擬dom」對象。咱們再利用ReactDom的虛擬dom經過render函數,轉換成dom。再經過插入到咱們的真是頁面中。
這就是整個mini 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源碼的輪子就出來了。
第三種,你還懶的話,直接把筆者的項目導下來算了。
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到頁面
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);
}
複製代碼
看到這裏,整個字符串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,
};
複製代碼
至此,本文mini簡單版本源碼結束,代碼將在文章最後段送出。 因本文定位初中級, 沒有涉及react全家桶。 下一篇,fiber,redux, hooks等概念或者源碼分析,將在新文章彙總出。如對你有用,關注期待後續文章。