React之虛擬DOM到真實DOM經歷了什麼

準備階段

建立項目

  • 經過create-react-app easy_react腳手架建立一個項目
  • 清理src目錄,這裏咱們只留下一個index.js便可
  • 修改index.js以下
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
const style = {
  color: 'red',
  background: 'yellow',
  fontSize:'20px'
}
ReactDOM.render(
  <h1 id={'gu_yan'} className={'gu-yan'} style={style}> Guyan </h1>, document.getElementById('root'));
複製代碼

啓動項目

  • 執行yarn start頁面效果以下

分析階段

  • index.js的代碼拷貝到babel中進行轉換,效果以下
  • 分析上圖
    • 比較代碼先後的變化,咱們發現變化以下
    /** * <h1 id={'gu_yan'} className={'gu-yan'} style={style}> * Guyan * </h1>, document.getElementById('root')); */
                ‖
                ‖ 
                ‖
                ↓
    React.createElement("h1", {
      id: 'gu_yan',
      className: 'gu-yan',
      style: style
    }, "Guyan")
    複製代碼
  • index.js文件,修改以下,發現頁面無變化,控制檯打印結果以下圖
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
const style = {
  color: 'red',
  background: 'yellow',
  fontSize: '20px'
};
const element = React.createElement("h1", {
  id: 'gu_yan',
  className: 'gu-yan',
  style: style
}, "Guyan")
console.log(element)
ReactDOM.render(element, document.getElementById('root'));
複製代碼

  • 如上圖可知React.createElement()執行返回的結果是一個對象,這個對象咱們就稱之爲虛擬DOM,到這裏咱們就能夠總結出虛擬DOM的由來了
    • 【1】在webpack打包的時候會調用babel-loader,將JSX語法轉義爲React.createElement(...)的形式
    • 【2】React.createElement執行返回一個對象,這對象就是虛擬DOM
  • 解析React.createElement的執行過程
    • 【1】收集屬性對象,處理特殊的屬性好比childrenkeyref;本文只舉例說明children
      • 【1-1】建立一個props對象
      • 【1-2】將傳進來的第二個參數對象中的每個屬性都掛在props上,值爲第二個參數對象中相對於的值
      • 【1-3】給props掛一個children屬性,值爲傳進來的第三個參數
        • 注:children這個值是一個字符串或者是一個數組。若是執行React.createElement的時候傳入的參數大於3,那麼children的值就是一個數組,其值爲除前兩個以外的全部屬性
    • 【2】將傳入的type和收集的屬性對象做爲參數傳入到ReactElement中執行(reactElement(type,props))
    • 【3】ReactElement執行建立一個newElement對象
    • 【4】給newElement掛一個$$typeof屬性,這裏咱們統一將其值賦爲Symbol(react.element)
    • 【5】給newElement掛一個type屬性,值爲傳進來的type
    • 【6】給newElement掛一個props屬性,值爲傳進來的props
    • 【7】返回newElement(虛擬DOM
  • 解析ReactDom.render的執行過程
    • 【1】判斷傳入的虛擬DOM的類型
      • 【1.1】普通文本類型,則第一個參數多是string或者number,則直接建立一個真實文本節點
      • 【1.2】普通html標籤組件,好比<div></div>,第一個參數對象的type屬性是一個string類型,建立一個真實的DOM節點並將第一個參數的props屬性中的一些普通屬性掛載到這個真實的DOM節點上,並對一些特殊的屬性好比children的進行處理,詳細見下文的實現階段
      • 【1.3】函數組件(function Component),第一個參數對象的type屬性是一個function類型,type執行傳入第一個參數的props
      • 【1.4】類組件(class Component),第一個參數對象的type屬性上有一個isReactComponent屬性,new type(props)建立實例,並讓實例的render方法執行
    • 【2】將建立的真實節點插入到父節點中

實現階段

react.js

// _react.js

    class Component {
        static isReactComponent = true;
        constructor(props){
            this.props = props;
        }
    }
    
    function ReactElement(type,props){
        const virtual_dom = {};
        virtual_dom.$$typeof = Symbol.for('react.element');
        virtual_dom.type = type;
        virtual_dom.props = props;
        return virtual_dom;
    }

    function createElement(type,config,children){
        const props = {};
        for (const propName in config){
            if (config.hasOwnProperty(propName)){
                props[propName] = config[propName];
            }
        }
        const childrenLength = arguments.length - 2;
        if (childrenLength === 1){
            props.children = children;
        }else if (childrenLength > 2){
            props.children = Array.from(arguments).slice(2);
        }
        return ReactElement(type,props);
    }
    
    export default {createElement,Component}
複製代碼

react-dom.js

// _react_dom.js
    
    function render(virtual_dom,parent_node){
        if (typeof virtual_dom ==='string' || typeof virtual_dom === 'number'){
        /** *處理直接渲染文本節點的狀況,好比ReactDOM.render('guYan', document.getElementById('root')); */
         return parent_node.appendChild(document.createTextNode(virtual_dom));
        }
        
        if (virtual_dom.type.isReactComponent){
         /** *處理直接渲染Class Component的狀況 */
            virtual_dom = new virtual_dom.type(virtual_dom.props).render();
            render(virtual_dom,parent_node);
        }
        
        if (typeof virtual_dom.type === 'function'){
         /** *處理直接渲染function Component的狀況 */
            virtual_dom = virtual_dom.type(virtual_dom.props);
            render(virtual_dom,parent_node);
        }
        let {type , props} = virtual_dom;
        let real_dom = document.createElement(type); // 根據type建立真實節點
        for (const propName in props){ // 對props進行處理
            if(props.hasOwnProperty(propName)){
                const prop =  props[propName];
                if(propName === 'className'){
                    real_dom.className = prop;
                }else if (propName === 'style'){
                    let cssText = Object.keys(prop).map(attr=>(`${attr.replace(/([A-Z])/g,function(){ return `-${arguments[1].toLowerCase()}` })}:${prop[attr]}`)).join(';');
                    real_dom.style.cssText = cssText;
                }else if (propName === 'children'){ 
                    /** *children多是字符串或者是數組,若是是字符串轉化成數組,而後遞歸調用render,須要注意的是 * 此時傳入的父節點是咱們建立的real_dom */
                    let childArr = Array.isArray(prop) ? prop : [prop];
                    childArr.forEach(child=>render(child,real_dom));
                }else {
                    real_dom.setAttribute(propName,prop);
                }
            }
        }
        return parent_node.appendChild(real_dom);
    }
複製代碼

寫在最後

  • 平常推薦使用函數組件
    • 1.使用簡單
    • 2.節約內存
    • 3.提升性能
  • 本文中的僅僅在表面層說明原理。並未深度剖析,如何錯誤還望指教。謝謝!
相關文章
相關標籤/搜索