react 渲染器瞭解一下?

簡介

今天咱們來寫一個本身的renderer,也就是react的渲染器。開始以前,先來了解一下react的三個核心。javascript

  • react 暴露了幾個方法,主要就是定義component,並不包含怎麼處理更新的邏輯。
  • renderer 負責處理視圖的更新
  • reconciler 16版本以後的react從stack reconciler重寫爲fiber reconciler,主要做用就是去遍歷節點,找出須要更新的節點,而後交由renderer去更新視圖

開始寫

先create-react-app建一個項目,而後安裝react-reconciler,修改index.js文件,改成用咱們本身寫的renderer來渲染。html

先創建一個文件,就叫renderer吧,怎麼寫呢,看下react-reconciler的readme.md以下:java

import Reconciler from "react-reconciler"

const hostConfig = {
    // ... 這裏寫入須要使用的函數
};
const MyReconcilerInstance = Reconciler(hostConfig);
const MyCustomRenderer = {
    render(ele, containerDom, callback){
        const container = MyReconcilerInstance.createContainer(containerDom, false);
        MyReconcilerInstance.updateContainer(
            ele,
            container,
            null,
            callback
        )
    }
};
export default MyCustomRenderer

複製代碼

而後hostConfig怎麼寫呢,官方已經給出了完整的列表,不過咱們不須要寫那麼多,先寫出來須要的,以下,先把全部的函數都打上log,看一下調用順序:node

// 這些是渲染須要的
const hostConfig = {
    getPublicInstance(...args) {
        console.log('getPublicInstance', ...args);
    },
    getChildHostContext(...args) {
        console.log('getChildHostContext', ...args);
    },
    getRootHostContext(...args) {
        console.log('getRootHostContext', ...args);
    },
    appendChildToContainer(...args) { 
        console.log('appendChildToContainer', ...args);
    },
    prepareForCommit(...args) {
        console.log('prepareForCommit', ...args)
    },
    resetAfterCommit(...args) {
        console.log('resetAfterCommit', ...args)
    },
    createInstance(...args) {
        console.log('createInstance', ...args)
    },
    appendInitialChild(...args) {
        console.log('appendInitialChild', ...args)
    },
    finalizeInitialChildren(...args) {
        console.log('prepareUpdate', ...args)
    },
    shouldSetTextContent(...args) {
        console.log('shouldSetTextContent', ...args)
    },
    shouldDeprioritizeSubtree(...args) {
        console.log('shouldDeprioritizeSubtree', ...args);
    },
    createTextInstance(...args) {
        console.log('createTextInstance', ...args);
    },
    scheduleDeferredCallback(...args) {
        console.log('scheduleDeferredCallback', ...args);
    },
    cancelDeferredCallback(...args) {
        console.log('cancelDeferredCallback', ...args);
    },
    shouldYield(...args) {
        console.log('shouldYield', ...args);
    },
    scheduleTimeout(...args) {
        console.log('scheduleTimeout', ...args);
    },
    cancelTimeout(...args) {
        console.log('cancelTimeout', ...args);
    },
    noTimeout(...args) {
        console.log('noTimeout', ...args);
    },
    now(...arg){
        console.log('now',...args);
    },
    isPrimaryRenderer(...args) {
        console.log('isPrimaryRenderer', ...args);
    },
    supportsMutation:true,
}
複製代碼

而後咱們修改App.js文件,簡單的寫一個計數器,大體以下:react

class App extends Component {
    state = {
        count: 1
    }
    
    increment = () => {
        this.setState((state) => {
            return { count: state.count + 1 }
        })
    }

    decrement = () => {
        this.setState((state) => {
            return { count: state.count - 1 }
        })
    }

    render() {
        const { count } = this.state;
        return (
            <div> <button onClick={this.decrement}> - </button> <span>{count}</span> <button onClick={this.increment}> + </button> </div>
        )
    }
}
複製代碼

打開瀏覽器看一下發現並無渲染出任何東西,打開console,這些函數的調用順序以下圖,好的,那咱們開始寫這些函數:git

初次渲染

  • now 這個函數是用來返回當前時間的,因此咱們就寫成Date.now 就能夠了。
  • getRootHostContext 這個函數能夠向下一級節點傳遞信息,因此咱們就簡單的返回一個空對象。
// rootContainerInstance 根節點 咱們這裏就是div#root
getRootHostContext(rootContainerInstance){
    return {}
}
複製代碼
  • getChildHostContext 這個函數用來從上一級獲取剛纔那個函數傳遞過來的上下文,同時向下傳遞,因此咱們就接着讓他返回一個空對象
/** * parentHostContext 從上一級節點傳遞過來的上下文 * type 當前節點的nodeType * rootContainerInstance 根節點 */ 
getChildHostContext(parentHostContext, type, rootContainerInstance){
    return {}
} 
複製代碼
  • shouldSetTextContent 這個函數是用來判斷是否須要去設置文字內容,若是是在react native中就始終返回false,由於文字也須要去單獨生成一個節點去渲染。這裏咱們寫簡單點,不去考慮dangerouslySetInnerHTML的狀況,就直接判斷children是否是字符串或者數字就能夠了
/* * type 當前節點的nodeType * props 要賦予當前節點的屬性 */
shouldSetTextContent(type, props) {
    return typeof props.children === 'string' || typeof props.children === 'number'
},
複製代碼
  • createInstance 這個函數就是要生成dom了。
/** * type 當前節點的nodeType * newProps 傳遞給當前節點的屬性 * rootContainerInstance 根節點 * currentHostContext 從上級節點傳遞下來的上下文 * workInProgress 當前這個dom節點對應的fiber節點 */ 
createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress) {
    const element = document.createElement(type);
    for (const key in newProps) {
        const val = newProps[key];
        if (key === 'className') {
            element.className = val
        } else if (key === 'style') {
            element.style = val
        } else if (key.startsWith('on')) {
            const eventType = key.slice(2).toLowerCase();
            element.addEventListener(eventType, val);
        } else if (key === 'children') {
            if (typeof val === 'string' || typeof val === 'number') {
                const textNode = document.createTextNode(val);
                element.appendChild(textNode)
            }
        } else {
            element.setAttribute(key, val)
        }
    }
    return element
},
複製代碼
  • finalizeInitialChildren 這個函數用來決定當前節點在commit階段是否沒法完成某些功能,須要在肯定dom節點已經掛載上以後,才能去完成這個功能,其實主要就是要判斷autofocus,因此咱們就簡單的判斷一下是否有這個屬性就能夠了
/** * domElement 當前已經生成的dom節點 * type nodeType * props 屬性 * rootContainerInstance 根節點 * hostContext 上下級傳遞過來的上下文 */ 
finalizeInitialChildren(domElement, type, props, rootContainerInstance, hostContext) {
    return !!props.autoFocus
},
複製代碼
  • appendInitialChild 這個就是用來將剛剛生成的節點掛載到上一層節點的函數。
/** * parentInstance 上一級節點 * child 子節點 */ 
appendInitialChild(parentInstance, child) {
    parentInstance.appendChild(child);
}
複製代碼
  • prepareForCommit 這個函數調用的時候,咱們的dom節點已經生成了,即將掛載到根節點上,在這裏須要作一些準備工做,好比說禁止事件的觸發,統計須要autofocus的節點。咱們就不須要了,直接寫一個空函數就能夠了。
// rootContainerInstance 根節點
prepareFomCommit(rootContainerInstance){}
複製代碼
  • appendChildToContainer 這個就是將生成的節點插入根節點的函數了。
// container 咱們的根節點
// child 已經生成的節點
appendChildToContainer(container, child){
    container.appendChild(child)
}
複製代碼
  • resetAfterCommit 這個函數會在已經將真實的節點掛載後觸發,因此咱們仍是寫一個空函數。
resetAfterCommit(){}
複製代碼

好了,如今咱們的初次渲染已經大功告成了。github

而後我畫了一張比較醜陋的流程圖: 數組

更新

  • prepareUpdate 這個函數用來統計怎麼更新,返回一個數組表明須要更新,若是不須要更新就返回null。而後返回的數組會返回給當前dom節點對應的fiber節點,賦予fiber節點的updateQueue,同時將當前fiber節點標記成待更新狀態。
/** * domElement 當前遍歷的dom節點 * type nodeType * oldProps 舊的屬性 * newProps 新屬性 * rootContainerInstance 根節點 * hostContext 從上一級節點傳遞下來的上下文 */ 
prepareUpdate(domElement, type, oldProps, newProps, rootContainerInstance, hostContext) {
    console.log('prepareUpdate', [...arguments]);
    let updatePayload = null;
    for (const key in oldProps) {
        const lastProp = oldProps[key];
        const nextProp = newProps[key];
        if (key === 'children') {
            if (nextProp != lastProp && (typeof nextProp === 'number' || typeof nextProp === 'string')) {
                updatePayload = updatePayload || [];
                updatePayload.push(key, nextProp);
            }
        } else {
            // 其他暫不考慮
        }
    };
    return updatePayload
}
複製代碼
  • commitUpdate 這個函數就是已經遍歷完成,準備更新了。
/** * domElement 對應的dom節點 * updatePayload 咱們剛纔決定返回的更新 * type nodeType * oldProps 舊的屬性 * newProps 新屬性 * internalInstanceHandle 當前dom節點對應的fiber節點 */ 
commitUpdate(domElement, updatePayload, type, oldProps, newProps, internalInstanceHandle) {
    for (var i = 0; i < updatePayload.length; i += 2) {
        var propKey = updatePayload[i];
        var propValue = updatePayload[i + 1];
        if (propKey === 'style') {

        } else if (propKey === 'children') {
            domElement.textContent = propValue;
            // 其他狀況暫不考慮
        } else {

        }
    }
},
複製代碼

woola!更新也完成了。瀏覽器

一樣也畫了一張醜陋的流程圖:

最後

代碼有點多,各位看官辛苦!本文全部代碼都可在此處找到markdown

參考:

相關文章
相關標籤/搜索