史上最貼心React渲染器原理輔導

rxLirp

這個故事要從幾年前,Reactreact-dom 離婚提及,先插一嘴,第三者不是張三(純博人眼球)。css

React 剛流行起來時,並無 react-dom ,忽然有一天,React 發達了,想學時間管理,而後閱人無數,就這樣 React 成了王,而 react-dom 與之分開淪爲了妾,React 可謂妻妾成羣,咱們隨便舉幾個例子: React-NativeRemax 等。爲啥 React 如此無情?我攤牌了,編不下去了,就說好好寫文章他不香嗎?react

正兒八經的,咱們開始!android

相信你們對於跨端這個概念不陌生,什麼是跨端?就是讓你感受寫一套代碼能夠作幾我的的事,好比,我用 React 能夠寫Web 、能夠寫小程序 、能夠寫原生應用,這樣能極大下降成本,但其實,你的工做交給 React 去作了,咱們能夠對應一下:ios

  • web:react-dom
  • 小程序:remax
  • ios、android:react-native

這樣一捋是否是清晰了?咱們再看一張圖web

image-20200509180159813

到這裏,你是否明白了當初 Reactreact-dom 分包的用意了?React 這個包自己代碼量不多,他只作了規範和api定義,平臺相關內容放在了與宿主相關的包,不一樣環境有對應的包面對,最終展示給用戶的是單單用 React 就把不少事兒作了。面試

那按這樣說,咱們是否是也能夠定義本身的React渲染器?固然能夠,否則跟着這篇文章走,學完就會,會了還想學。npm

建立React項目

首先使用React腳手架建立一個demo項目小程序

安裝腳手架react-native

npm i -g create-react-appapi

建立項目

create-react-app react-custom-renderer

運行項目

yarn start

如今咱們能夠在vs code中進行編碼了

修改 App.js 文件源碼

import React from "react";
import "./App.css";

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  handleClick = () => {
    this.setState(({ count }) => ({ count: count + 1 }));
  };

  render() {
    const { handleClick } = this;
    const { count } = this.state;
    return (
      <div className="App"> <header className="App-header" onClick={handleClick}> <span>{count}</span> </header> </div>
    );
  }
}

export default App;

複製代碼

打開瀏覽器,能夠看到頁面,咱們點擊頁面試試,數字會逐漸增長。

image-20200509181231669

到這裏,簡單的React項目建立成功,接下來咱們準備自定義渲染器。

初識渲染器

打開src/index.js,不出意外,一應該看到了這行代碼:

import ReactDOM from 'react-dom';
複製代碼

還有這行

ReactDOM.render(
  <App />, document.getElementById('root') ); 複製代碼

如今咱們要使用本身的代碼替換掉 react-dom,建立 MyRenderer.js,而後修改 index.js中的內容

import MyRenderer from './MyRenderer'

MyRenderer.render(
  <App />, document.getElementById('root') ) 複製代碼

而後打開瀏覽器,會看到報錯信息,咱們按照報錯信息提示,完善 MyRenderer.js 的內容。首先文件中最基本的結構以下

import ReactReconciler from "react-reconciler";

const rootHostContext = {};
const childHostContext = {};

const hostConfig = {
  getRootHostContext: () => {
    return rootHostContext;
  },
  getChildHostContext: () => {
    return childHostContext;
  },
};

const ReactReconcilerInst = ReactReconciler(hostConfig);
export default {
  render: (reactElement, domElement, callback) => {
    if (!domElement._rootContainer) {
      domElement._rootContainer = ReactReconcilerInst.createContainer(
        domElement,
        false
      );
    }
    return ReactReconcilerInst.updateContainer(
      reactElement,
      domElement._rootContainer,
      null,
      callback
    );
  },
};

複製代碼

react-reconciler 源碼咱們曾講解過,咱們能夠把它當作一個調度器,負責建立與更新,然後在 scheduler中進行調度,咱們導出一個對象,其中有一個方法 render ,參數與 react-dom的render方法一致,這裏須要判斷一下,若是傳入的dom元素是根容器,則爲建立操做,不然是更新的操做,建立操做調用 react-reconciler 實例的 createContainer 方法,更新操做調用 react-reconciler實例的 updateContainer 方法。咱們再來看到更爲重要的概念——hostConfig

Host宿主相關配置

Host——東家、宿主,見名知意,HostConfig是對於宿主相關的配置,這裏所說的宿主就是運行環境,是web、小程序、仍是原生APP。有了這個配置,react-reconciler在進行調度後,便能根據宿主環境,促成UI界面更新。

咱們繼續來到瀏覽器,跟隨報錯信息,完善咱們hostConfig的內容,我將其中核心的方法列舉以下,供你們參考學習。

  • getRootHostContext
  • getChildHostContext
  • shouldSetTextContent
  • prepareForCommit
  • resetAfterCommit
  • createTextInstance
  • createInstance
  • appendInitialChild
  • appendChild
  • finalizeInitialChildren
  • appendChildToContainer
  • prepareUpdate
  • commitUpdate
  • commitTextUpdate
  • removeChild

看到這些方法不由聯想到DOM相關操做方法,都是語義化命名,這裏不贅述各個方法的實際含義,一下咱們修改相關方法,從新讓項目跑起來,以助於你們理解渲染器的工做原理。

定義hostConfig

以上方法中,咱們重點理解一下 createInstancecommitUpdate, 其餘方法我在最後經過代碼片斷展現出來,供你們參考。(注:相關實現可能與實際使用有較大的差異,僅供借鑑學習)

createInstance

方法參數

  • type
  • newProps
  • rootContainerInstance
  • _currentHostContext
  • workInProgress

返回值

根據傳入type,建立dom元素,並處理props等,最終返回這個dom元素。本例咱們只考慮一下幾個props

  • children
  • onClick
  • className
  • style
  • 其餘

代碼實現

const hostConfig = {
  createInstance: (
    type,
    newProps,
    rootContainerInstance,
    _currentHostContext,
    workInProgress
  ) => {
    const domElement = document.createElement(type);
    Object.keys(newProps).forEach((propName) => {
      const propValue = newProps[propName];
      if (propName === "children") {
        if (typeof propValue === "string" || typeof propValue === "number") {
          domElement.textContent = propValue;
        }
      } else if (propName === "onClick") {
        domElement.addEventListener("click", propValue);
      } else if (propName === "className") {
        domElement.setAttribute("class", propValue);
      } else if (propName === "style") {
        const propValue = newProps[propName];
        const propValueKeys = Object.keys(propValue)
        const propValueStr = propValueKeys.map(k => `${k}: ${propValue[k]}`).join(';')
        domElement.setAttribute(propName, propValueStr);
      } else {
        const propValue = newProps[propName];
        domElement.setAttribute(propName, propValue);
      }
    });
    return domElement;
  },
}
複製代碼

是否是很眼熟?誰說原生JavaScript不重要,咱們能夠看到在框架的內部,仍是須要使用原生JavaScript去操做DOM,相關操做咱們就不深刻了。

commitUpdate

更新來自於哪裏?很容易想到 setState ,固然還有 forceUpdate ,好比老生常談的問題:兄嘚,setState 是同步仍是異步啊?啥時候同步啊?這就涉及到 fiber的內容了,其實調度是經過計算的 expirationTime來肯定的,將必定間隔內收到的更新請求入隊並貼上相同時間,想一想,若是其餘條件都同樣的狀況下,那這幾回更新都會等到同一個時間被執行,看似異步,實則將優先權讓給了更須要的任務。

小小拓展了一下,咱們回來,更新來自於 setStateforceUpdate,更新在通過系列調度以後,最終會提交更新,這個操做就是在 commitUpdate方法完成。

方法參數

  • domElement
  • updatePayload
  • type
  • oldProps
  • newProps

這裏的操做其實與上面介紹的createInstance有相似之處,不一樣點在於,上面的方法須要建立實例,而此處更新操做是將已經建立好的實例進行更新,好比內容的更新,屬性的更新等。

代碼實現

const hostConfig = {
  commitUpdate(domElement, updatePayload, type, oldProps, newProps) {
    Object.keys(newProps).forEach((propName) => {
      const propValue = newProps[propName];
      if (propName === "children") {
        if (typeof propValue === "string" || typeof propValue === "number") {
          domElement.textContent = propValue;
        }
        // TODO 還要考慮數組的狀況
      } else if (propName === "style") {
        const propValue = newProps[propName];
        const propValueKeys = Object.keys(propValue)
        const propValueStr = propValueKeys.map(k => `${k}: ${propValue[k]}`).join(';')
        domElement.setAttribute(propName, propValueStr);
      } else {
        const propValue = newProps[propName];
        domElement.setAttribute(propName, propValue);
      }
    });
  },
}
複製代碼

兩個主要的方法介紹完了,你如今隱隱約約感覺到了 react 跨平臺的魅力了嗎?咱們能夠想象一下,假設 MyRenderer.render 方法傳入的第二個參數不是DOM對象,而是其餘平臺的 GUI 對象,那是否是在 createInstancecommitUpdate 方法中使用對應的GUI建立與更新api就能夠了呢?沒錯!

完整配置

const hostConfig = {
  getRootHostContext: () => {
    return rootHostContext;
  },
  getChildHostContext: () => {
    return childHostContext;
  },
  shouldSetTextContent: (type, props) => {
    return (
      typeof props.children === "string" || typeof props.children === "number"
    );
  },
  prepareForCommit: () => {},
  resetAfterCommit: () => {},
  createTextInstance: (text) => {
    return document.createTextNode(text);
  },
  createInstance: (
    type,
    newProps,
    rootContainerInstance,
    _currentHostContext,
    workInProgress
  ) => {
    const domElement = document.createElement(type);
    Object.keys(newProps).forEach((propName) => {
      const propValue = newProps[propName];
      if (propName === "children") {
        if (typeof propValue === "string" || typeof propValue === "number") {
          domElement.textContent = propValue;
        }
      } else if (propName === "onClick") {
        domElement.addEventListener("click", propValue);
      } else if (propName === "className") {
        domElement.setAttribute("class", propValue);
      } else if (propName === "style") {
        const propValue = newProps[propName];
        const propValueKeys = Object.keys(propValue)
        const propValueStr = propValueKeys.map(k => `${k}: ${propValue[k]}`).join(';')
        domElement.setAttribute(propName, propValueStr);
      } else {
        const propValue = newProps[propName];
        domElement.setAttribute(propName, propValue);
      }
    });
    return domElement;
  },
  appendInitialChild: (parent, child) => {
    parent.appendChild(child);
  },
  appendChild(parent, child) {
    parent.appendChild(child);
  },
  finalizeInitialChildren: (domElement, type, props) => {},
  supportsMutation: true,
  appendChildToContainer: (parent, child) => {
    parent.appendChild(child);
  },
  prepareUpdate(domElement, oldProps, newProps) {
    return true;
  },
  commitUpdate(domElement, updatePayload, type, oldProps, newProps) {
    Object.keys(newProps).forEach((propName) => {
      const propValue = newProps[propName];
      if (propName === "children") {
        if (typeof propValue === "string" || typeof propValue === "number") {
          domElement.textContent = propValue;
        }
        // TODO 還要考慮數組的狀況
      } else if (propName === "style") {
        const propValue = newProps[propName];
        const propValueKeys = Object.keys(propValue)
        const propValueStr = propValueKeys.map(k => `${k}: ${propValue[k]}`).join(';')
        domElement.setAttribute(propName, propValueStr);
      } else {
        const propValue = newProps[propName];
        domElement.setAttribute(propName, propValue);
      }
    });
  },
  commitTextUpdate(textInstance, oldText, newText) {
    textInstance.text = newText;
  },
  removeChild(parentInstance, child) {
    parentInstance.removeChild(child);
  },
};
複製代碼

來到瀏覽器,正常工做了,點擊頁面,計數增長。

以上就是本節的全部內容了,看罷你都明白了嗎?若是想看其餘框架原理,歡迎留言評論

  • 微信公衆號 《JavaScript全棧
  • 掘金 《合一大師
  • Bilibili 《合一大師
  • 微信:zxhy-heart

我是合一,英雄再會。

相關文章
相關標籤/搜索