從簡版mini源碼分析react。附送簡單版react源碼。(3K字+)

適合人羣

本文適合0.5~3年的react開發人員的進階。html

講講廢話:前端

react的源碼,的確是比vue的難度要深一些,本文也是針對初中級,本意讓博友們瞭解整個react的執行過程。vue

上一篇,從mini源碼分析vue,也許深度比較通常,可是也花了好幾天的時間去彙總,明白一個博主的不易。這是筆者第一篇得到上百個贊,還有評論上的好評,還有臨時關注的幾十粉絲。這是我寫博客的動力。node

這周十分繁忙的條件下,仍是抽出時間彙總react。若是以爲不錯,仍是點個贊吧(三大步最喜歡了)react

如何還須要學習vue源碼以及vue相關知識點的朋友,建議移步:juejin.im/post/5f0326…git

寫源碼以前的必備知識點

JSX

首先咱們須要瞭解什麼是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,最終渲染出頁面。

虛擬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過程

1)基本架子的搭建

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源碼的輪子就出來了。

  • 第三種,你還懶的話,直接把筆者的項目導下來算了。

    連接:github.com/zhuangweizh…

2) 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到頁面

3) 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);
}
複製代碼

4) 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屬性,分別表示

    1. 普通標籤
    1. 函數組件標籤
    1. 類組件標籤

根據上述標記,咱們可改造爲:

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等概念或者源碼分析,將在新文章彙總出。如對你有用,關注期待後續文章。

相關文章
相關標籤/搜索