面試官:說(shuō)說(shuō)React Jsx轉(zhuǎn)換成真實(shí)DOM過(guò)程?
本文轉(zhuǎn)載自微信公眾號(hào)「JS每日一題」,作者灰灰。轉(zhuǎn)載本文請(qǐng)聯(lián)系JS每日一題公眾號(hào)。
一、是什么
react通過(guò)將組件編寫(xiě)的JSX映射到屏幕,以及組件中的狀態(tài)發(fā)生了變化之后 React會(huì)將這些「變化」更新到屏幕上
在前面文章了解中,JSX通過(guò)babel最終轉(zhuǎn)化成React.createElement這種形式,例如:
- <div>
- <img src="avatar.png" className="profile" />
- <Hello />
- </div>
會(huì)被bebel轉(zhuǎn)化成如下:
- React.createElement(
- "div",
- null,
- React.createElement("img", {
- src: "avatar.png",
- className: "profile"
- }),
- React.createElement(Hello, null)
- );
在轉(zhuǎn)化過(guò)程中,babel在編譯時(shí)會(huì)判斷 JSX 中組件的首字母:
- 當(dāng)首字母為小寫(xiě)時(shí),其被認(rèn)定為原生 DOM 標(biāo)簽,createElement 的第一個(gè)變量被編譯為字符串
- 當(dāng)首字母為大寫(xiě)時(shí),其被認(rèn)定為自定義組件,createElement 的第一個(gè)變量被編譯為對(duì)象
最終都會(huì)通過(guò)RenderDOM.render(...)方法進(jìn)行掛載,如下:
- ReactDOM.render(<App />, document.getElementById("root"));
二、過(guò)程
在react中,節(jié)點(diǎn)大致可以分成四個(gè)類(lèi)別:
- 原生標(biāo)簽節(jié)點(diǎn)
- 文本節(jié)點(diǎn)
- 函數(shù)組件
- 類(lèi)組件
如下所示:
- class ClassComponent extends Component {
- static defaultProps = {
- color: "pink"
- };
- render() {
- return (
- <div className="border">
- <h3>ClassComponent</h3>
- <p className={this.props.color}>{this.props.name}</p>
- </div>
- );
- }
- }
- function FunctionComponent(props) {
- return (
- <div className="border">
- FunctionComponent
- <p>{props.name}</p>
- </div>
- );
- }
- const jsx = (
- <div className="border">
- <p>xx</p>
- <a href="https://www.xxx.com/">xxx</a>
- <FunctionComponent name="函數(shù)組件" />
- <ClassComponent name="類(lèi)組件" color="red" />
- </div>
- );
這些類(lèi)別最終都會(huì)被轉(zhuǎn)化成React.createElement這種形式
React.createElement其被調(diào)用時(shí)會(huì)傳?標(biāo)簽類(lèi)型type,標(biāo)簽屬性props及若干子元素children,作用是生成一個(gè)虛擬Dom對(duì)象,如下所示:
- function createElement(type, config, ...children) {
- if (config) {
- delete config.__self;
- delete config.__source;
- }
- // ! 源碼中做了詳細(xì)處理,⽐如過(guò)濾掉key、ref等
- const props = {
- ...config,
- children: children.map(child =>
- typeof child === "object" ? child : createTextNode(child)
- )
- };
- return {
- type,
- props
- };
- }
- function createTextNode(text) {
- return {
- type: TEXT,
- props: {
- children: [],
- nodeValue: text
- }
- };
- }
- export default {
- createElement
- };
createElement會(huì)根據(jù)傳入的節(jié)點(diǎn)信息進(jìn)行一個(gè)判斷:
- 如果是原生標(biāo)簽節(jié)點(diǎn), type 是字符串,如div、span
- 如果是文本節(jié)點(diǎn), type就沒(méi)有,這里是 TEXT
- 如果是函數(shù)組件,type 是函數(shù)名
- 如果是類(lèi)組件,type 是類(lèi)名
虛擬DOM會(huì)通過(guò)ReactDOM.render進(jìn)行渲染成真實(shí)DOM,使用方法如下:
- ReactDOM.render(element, container[, callback])
當(dāng)首次調(diào)用時(shí),容器節(jié)點(diǎn)里的所有 DOM 元素都會(huì)被替換,后續(xù)的調(diào)用則會(huì)使用 React 的 diff算法進(jìn)行高效的更新
如果提供了可選的回調(diào)函數(shù)callback,該回調(diào)將在組件被渲染或更新之后被執(zhí)行
render大致實(shí)現(xiàn)方法如下:
- function render(vnode, container) {
- console.log("vnode", vnode); // 虛擬DOM對(duì)象
- // vnode _> node
- const node = createNode(vnode, container);
- container.appendChild(node);
- }
- // 創(chuàng)建真實(shí)DOM節(jié)點(diǎn)
- function createNode(vnode, parentNode) {
- let node = null;
- const {type, props} = vnode;
- if (type === TEXT) {
- node = document.createTextNode("");
- } else if (typeof type === "string") {
- node = document.createElement(type);
- } else if (typeof type === "function") {
- node = type.isReactComponent
- ? updateClassComponent(vnode, parentNode)
- : updateFunctionComponent(vnode, parentNode);
- } else {
- node = document.createDocumentFragment();
- }
- reconcileChildren(props.children, node);
- updateNode(node, props);
- return node;
- }
- // 遍歷下子vnode,然后把子vnode->真實(shí)DOM節(jié)點(diǎn),再插入父node中
- function reconcileChildren(children, node) {
- for (let i = 0; i < children.length; i++) {
- let child = children[i];
- if (Array.isArray(child)) {
- for (let j = 0; j < child.length; j++) {
- render(child[j], node);
- }
- } else {
- render(child, node);
- }
- }
- }
- function updateNode(node, nextVal) {
- Object.keys(nextVal)
- .filter(k => k !== "children")
- .forEach(k => {
- if (k.slice(0, 2) === "on") {
- let eventName = k.slice(2).toLocaleLowerCase();
- node.addEventListener(eventName, nextVal[k]);
- } else {
- node[k] = nextVal[k];
- }
- });
- }
- // 返回真實(shí)dom節(jié)點(diǎn)
- // 執(zhí)行函數(shù)
- function updateFunctionComponent(vnode, parentNode) {
- const {type, props} = vnode;
- let vvnode = type(props);
- const node = createNode(vvnode, parentNode);
- return node;
- }
- // 返回真實(shí)dom節(jié)點(diǎn)
- // 先實(shí)例化,再執(zhí)行render函數(shù)
- function updateClassComponent(vnode, parentNode) {
- const {type, props} = vnode;
- let cmp = new type(props);
- const vvnode = cmp.render();
- const node = createNode(vvnode, parentNode);
- return node;
- }
- export default {
- render
- };
三、總結(jié)
在react源碼中,虛擬Dom轉(zhuǎn)化成真實(shí)Dom整體流程如下圖所示:
其渲染流程如下所示:
- 使用React.createElement或JSX編寫(xiě)React組件,實(shí)際上所有的 JSX 代碼最后都會(huì)轉(zhuǎn)換成React.createElement(...) ,Babel幫助我們完成了這個(gè)轉(zhuǎn)換的過(guò)程。
- createElement函數(shù)對(duì)key和ref等特殊的props進(jìn)行處理,并獲取defaultProps對(duì)默認(rèn)props進(jìn)行賦值,并且對(duì)傳入的孩子節(jié)點(diǎn)進(jìn)行處理,最終構(gòu)造成一個(gè)虛擬DOM對(duì)象
- ReactDOM.render將生成好的虛擬DOM渲染到指定容器上,其中采用了批處理、事務(wù)等機(jī)制并且對(duì)特定瀏覽器進(jìn)行了性能優(yōu)化,最終轉(zhuǎn)換為真實(shí)DOM
參考文獻(xiàn)
https://bbs.huaweicloud.com/blogs/265503)
https://huang-qing.github.io/react/2019/05/29/React-VirDom/
https://segmentfault.com/a/1190000018891454