这个故事要从几年前,React
和 react-dom
离婚提及,先插一嘴,第三者不是张三(纯博人眼球)。css
React
刚流行起来时,并无 react-dom
,忽然有一天,React
发达了,想学时间管理,而后阅人无数,就这样 React
成了王,而 react-dom
与之分开沦为了妾,React
可谓妻妾成群,咱们随便举几个例子: React-Native
、Remax
等。为啥 React
如此无情?我摊牌了,编不下去了,就说好好写文章他不香吗?react
正儿八经的,咱们开始!android
相信你们对于跨端这个概念不陌生,什么是跨端?就是让你感受写一套代码能够作几我的的事,好比,我用 React
能够写Web 、能够写小程序 、能够写原生应用,这样能极大下降成本,但其实,你的工做交给 React
去作了,咱们能够对应一下:ios
这样一捋是否是清晰了?咱们再看一张图web
到这里,你是否明白了当初 React
和 react-dom
分包的用意了?React
这个包自己代码量不多,他只作了规范和api定义,平台相关内容放在了与宿主相关的包,不一样环境有对应的包面对,最终展示给用户的是单单用 React
就把不少事儿作了。面试
那按这样说,咱们是否是也能够定义本身的React渲染器?固然能够,否则跟着这篇文章走,学完就会,会了还想学。npm
首先使用React脚手架建立一个demo项目小程序
安装脚手架react-native
npm i -g create-react-app
api
建立项目
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——东家、宿主,见名知意,HostConfig是对于宿主相关的配置,这里所说的宿主就是运行环境,是web、小程序、仍是原生APP。有了这个配置,react-reconciler
在进行调度后,便能根据宿主环境,促成UI界面更新。
咱们继续来到浏览器,跟随报错信息,完善咱们hostConfig的内容,我将其中核心的方法列举以下,供你们参考学习。
看到这些方法不由联想到DOM
相关操做方法,都是语义化命名,这里不赘述各个方法的实际含义,一下咱们修改相关方法,从新让项目跑起来,以助于你们理解渲染器的工做原理。
以上方法中,咱们重点理解一下 createInstance
和 commitUpdate
, 其余方法我在最后经过代码片断展现出来,供你们参考。(注:相关实现可能与实际使用有较大的差异,仅供借鉴学习)
方法参数
返回值
根据传入type,建立dom元素,并处理props等,最终返回这个dom元素。本例咱们只考虑一下几个props
代码实现
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,相关操做咱们就不深刻了。
更新来自于哪里?很容易想到 setState
,固然还有 forceUpdate
,好比老生常谈的问题:兄嘚,setState
是同步仍是异步啊?啥时候同步啊?这就涉及到 fiber
的内容了,其实调度是经过计算的 expirationTime
来肯定的,将必定间隔内收到的更新请求入队并贴上相同时间,想一想,若是其余条件都同样的状况下,那这几回更新都会等到同一个时间被执行,看似异步,实则将优先权让给了更须要的任务。
小小拓展了一下,咱们回来,更新来自于 setState
、forceUpdate
,更新在通过系列调度以后,最终会提交更新,这个操做就是在 commitUpdate方法完成。
方法参数
这里的操做其实与上面介绍的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);
},
};
复制代码
来到浏览器,正常工做了,点击页面,计数增长。
以上就是本节的全部内容了,看罢你都明白了吗?若是想看其余框架原理,欢迎留言评论
我是合一,英雄再会。