首頁 > 軟體

React執行機制超詳細講解

2022-11-05 14:00:14

適合人群

本文適合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,
};

到此這篇關於React執行機制超詳細講解的文章就介紹到這了,更多相關React執行機制內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com