這個故事要從幾年前,
React
和
react-dom
離婚說起,先插一嘴,第三者不是張三(純博人眼球)。
React
剛流行起來時,并沒有
react-dom
,突然有一天,
React
發達了,想學時間管理,然後閱人無數,就這樣
React
成了王,而
react-dom
與之分開淪為了妾,
React
可謂妻妾成群,我們随便舉幾個例子:
React-Native
、
Remax
等。為啥
React
如此無情?我攤牌了,編不下去了,就說好好寫文章他不香嗎?
正兒八經的,我們開始!
相信大家對于跨端這個概念不陌生,什麼是跨端?就是讓你感覺寫一套代碼可以做幾個人的事,比如,我用
React
可以寫Web 、可以寫小程式 、可以寫原生應用,這樣能極大降低成本,但其實,你的工作交給
React
去做了,我們可以對應一下:
•web:react-dom•小程式:remax•ios、android:react-native
這樣一捋是不是清晰了?我們再看一張圖
到這裡,你是否明白了當初
React
和
react-dom
分包的用意了?
React
這個包本身代碼量很少,他隻做了規範和api定義,平台相關内容放在了與宿主相關的包,不同環境有對應的包面對,最終展現給使用者的是單單用
React
就把很多事兒做了。
那按這樣說,我們是不是也可以定義自己的React渲染器?當然可以,不然跟着這篇文章走,學完就會,會了還想學。
建立React項目
首先使用React腳手架建立一個demo項目
安裝腳手架
npm i -g create-react-app
建立項目
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;
複制
打開浏覽器,可以看到頁面,我們點選頁面試試,數字會逐漸增加。
到這裡,簡單的
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
以上方法中,我們重點了解一下
createInstance
和
commitUpdate
, 其他方法我在最後通過代碼片段展示出來,供大家參考。(注:相關實作可能與實際使用有較大的差别,僅供借鑒學習)
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
來确定的,将一定間隔内收到的更新請求入隊并貼上相同時間,想想,如果其他條件都一樣的情況下,那這幾次更新都會等到同一個時間被執行,看似異步,實則将優先權讓給了更需要的任務。
小小拓展了一下,我們回來,更新來自于
setState
、
forceUpdate
,更新在經過系列排程之後,最終會送出更新,這個操作就是在 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
對象,那是不是在 createInstance 和 commitUpdate 方法中使用對應的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
我是合一,英雄再會。