前言
我们今天不分享 React
具体语法,组件, 通信 ,Ref ,Portals ,Context ,Hoc ,Hook 等等知识点,这些东西,大家细致的看一下官方文档都可以熟悉的进行开发任务,而今天我想谈谈 React
的工作原理 。
Lets Go !
什么是虚拟 Dom ?
什么是 Jsx ?
React 又是怎么工作的 ?
… …
我们来根据上边的三个问题,来对 React 进行一次浅度剖析 !
什么是虚拟 Dom ?
Virtual DOM (虚拟Dom) 是一种编程概念 ,在这个概念里 ,UI 是以一种虚拟的结构形式保存在内存中,然后通过编译转换,变成真实Dom的一种技术。那它到底是什么呢 ?
对象
,yes ,就是一个 Javascript 对象 , 用这个 Javascript 对象来表示Dom信息和结构,当状态变更的时候,重新渲染这个对象结构,这个 Javascript 对象称为 Virtual DOM 。
为什么不直接操作DOM ?
很简单,因为DOM操作很慢,细小的改变都有可能导致页面重排与重绘,耗性能 ; 所以 , 通过diff 算法对 Js 对象的操作可以批量的,最小化的执行 Dom 操作,从而提高性能 。diff 我们后期分析
。
什么是Jsx ?
你可以理解为是 React 独有的语法糖吧 , 其实 Jsx 是 javascript + xml 的语法扩展 , 但凡是遇到 { }
会被当做 Javascript 来执行,否则则是 xml 来执行 ;
为什么需要用 Jsx ?
也许是因为 Jsx 模板简洁,语法灵活吧,这一点我并不是太清楚 ;
原理:babel-loader 会预编译 Jsx 为 React.createElement(…) 函数执行 。
那React 又是怎么工作的呢 ?
从 Jsx
入手吧 , 首先来我们看官网的一段代码 【少许加工了一下,为了看的更明白】
一个常规的 React 组件 【编译前】
class HelloMessage extends React.Component {
render() {
return (
<div className="HelloClass" num="1">
Hello {this.props.name}
<span> is span</span>
</div>
);
}
}
ReactDOM.render(
<HelloMessage name="Taylor" />,
document.getElementById('hello-example')
);
这个常规的 React 组件,经过 babel-loader 【编译后】
class HelloMessage extends React.Component {
render() {
return React.createElement(
"div", // div 类型
{ className: "HelloClass", num: "1" }, // div 的属性
"Hello ", // div 的内容
this.props.name, // div 的内容 , props 直接传递
React.createElement( // 嵌套-继续
"span",
null,
" is span"
)
);
}
}
// 类组件也是转换成 `React.createElement(...)` 函数执行
ReactDOM.render(React.createElement(HelloMessage, { name: "Taylor" }),
document.getElementById('hello-example'));
看过代码,简单的分析过后,有没有发现,验证了我们上边说到的 Jsx 原理 ,经过babel-loader
编译之后变成了React.createElement(...)
函数执行,如果有嵌套,则 React.createElement(...)
也嵌套
有没有发现,我们不管是编译前,还是编译后,有一句代码没有变,那就是 class HelloMessage extends React.Component
继承了 React.component
类,其实简单点说,就是继承父组件的 props
。
okey ! 到这里 ,我们可以有一个简单的结论,就是:
- React 组件需要继承 React.Component 类
class HelloMessage extends React.Component
- Jsx 语法经过 babel-loader 编译后会变成
React.createElement(...) 函数执行
,多层则嵌套 - 通过 ReactDom.render 方法将元素挂载在页面上
我们来依次分析,并简单实现一下 :
React.component
export default function Component (props) {
this.props = props;
}
// 定义一个类组件与函数组件的标识 , 源码是这样标识的,虽然不知道为啥没有写成布尔值
Component.prototype.isReactComponent = {};
React.createElement(…)
回到React.createElement(...)
, 那么 React.createElement(...)
到底接收几个参数,分别又是干什么的呢 ? 可以看出来:
- 参数1:类型 (渲染的元素类型,组件类型 等)
- 参数2:属性 (元素或者组件的属性,包含 className,id,自定义 props 属性等)
- 参数3:子节点内容
可能有很多个子级
okey , 综上所述 ,我们来手写一个 mini 的 React.createElement() 。
在写 React.createElement(...)
之前,我们首先要搞懂两个事情,传入什么参数 ,返回什么值 ,传入什么参数,上边我们已经分析了出来,经过babel-loader
编译后需要传入三个参数分别是 类型,属性,子节点
,那返回什么呢 ? 我们来看看createElement
源码:
// 位置 : react/packages/react/src/ReactElement.js 348 行
// 删掉了一些 if(_DEV_)的多余代码
export function createElement(type, config, children) {
// 一系列声明
let propName;
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
// 如果传入了 config , 对以上声明属性进行赋值
if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// children : (arguments.length - 2) 去除前两个参数的剩下所有参数
// 最后将 children 并入 props 属性中
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray; // children 返回一个数组
}
// 默认值
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 最后返回 ReactElement 这个对象
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
一目了然 ,需要返回一个 ReactElement
对象,包含type,key, ref ,self , source , props
等 ,在这里我们主要为了了解 react
的工作原理 , 所以择取重要的来简单实现一下,暂时抛弃 key,ref,self
来, 话不多说,上代码
/*
三个参数 , 暂时返回两个属性 type 和 props
type:创建类型 , 原生标签 , 文本 , 函数组件 , 类组件 等
config: 属性
例举属性如 : 函数组件:{className: "border", name: "函数组件", __source: {…}, __self: undefined }
children: 子节点 , 多少个就不知道了
*/
function createElement (type, config, ...children) {
// 移除 config 暂时不用的 __source: {…}, __self: undefined 属性,方便控制台查看
if (config) {
delete config.__source;
delete config.__self;
}
// 这里不考虑 key . ref . slef
const props = {
...config,
/*
createTextNode 统一文本节点的数据结构,方便后续统一处理(child 直接就是文本内容)
如果过是一个 object 说明子节点下还有子节点,依然利用 React.createEement 继续编译为
{
type:"类型",
props:{}
}
的形式, 重点:babel-loader 将 jsx 编译成 React.creteElement 嵌套的形式
*/
children: children.map(child =>
typeof child === "object" ? child : createTextNode(child)
)
};
console.log({type,props});
return {
type,
props
};
}
// 纯文本统一一下格式,跟其它元素一样的格式,方便处理,类型自定义为 "TEXT"
function createTextNode (text) {
return {
type: "TEXT",
props: {
children: [],
nodeValue: text
}
};
}
来看看最后的返回结果,我自己的样例
okey ! 一个 mini
版本的 React.createElement()
方法就这样实现了,好,我们接着往下走,是不是到了 render
方法了 , 走,再去看一看源码 ~~~
ReactDOM.render(···)
render 方法在 react-dom 模块里边
// 位置 : react/packages/react-dom/src/client/ReactDOMLegacy.js 287 行
export function render(
element: React$Element<any>, // react 元素
container: Container, // 要放置的容器
callback: ?Function, // 回调函数
) {
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
// ··· 这里返回调用了 legacyRenderSubtreeIntoContainer 方法
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
legacyRenderSubtreeIntoContainer
顺着继续往下走
// 位置 : react/packages/react-dom/src/client/ReactDOMLegacy.js 175 行
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: Container,
forceHydrate: boolean,
callback: ?Function,
) {
let root: RootType = (container._reactRootContainer: any);
let fiberRoot;
// 判断是不是第一次渲染该组件 , 如果是第一次则创建,然后更新 dom , 否则直接进行 diff 更新 dom
if (!root) {
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// 更新 dom
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// 更新 dom
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
看到这里我们心中依然有了一个简单的概念,那就是
- 1,
render
方法接收三个参数,react 元素,渲染 react 元素的容器 ,回调函数 - 2,紧跟着调用
legacyRenderSubtreeIntoContainer
方法,创建 dom || 更新dom ,这里边牵扯到两个重点,也是React
的核心知识点【fiber 数据结构 , difff 算法】
我们后续来了解
今天在这里呢,我们以简洁的方式来实现一下 render
方法 :
创建一个 render
函数,做两件事儿(抛开回调函数来说)
/*
vnode jsx 经过 React.createElemnt 编译后的虚拟 dom
container 容器
步骤 1 : vnode -> node , 虚拟 dom 转换为真实 dom
步骤 2 : container.appendChild(node); , 将真实 dom 挂载给容器 container
*/
function render (vnode, container) {
const node = createNode(vnode, container);
node && container.appendChild(node);
}
实现一个 createNode
将虚拟 dom 转换为真实 dom
/*
虚拟 dom 渲染成真实 dom
vnode 虚拟 dom
parentNode 父节点 , 也就是将虚拟dom挂载的对应父容器
*/
function createNode (vnode, parentNode) {
// 转换组合的最后真实 dom 节点
let node = null;
/*
type:"类型"
props:参数和子节点 children
*/
const { type, props } = vnode;
// todo 根据节点类型,生成 dom 节点
if (type === "TEXT") {
// 文本节点 , type 上边自定义为 "TEXT" , nodeValue 是文本节点的内容值
node = document.createTextNode("");
} else if (typeof type === "string") {
// 原生标签节点,type 类型为字符串 :如 div , p , span 等 , 创建对应的标签元素
node = document.createElement(type);
} else if (typeof type === "function") {
// 类组件与函数组件 ,type 类型为 function
// isReactComponent 自定义区分标识 , Component 原型上定义
node = type.prototype.isReactComponent
? updateClassComponent(vnode, parentNode) // 类组件转换dom方法
: updateFunctionComponent(vnode, parentNode); // 函数组件转换 dom 方法
}
/*
子节点转换 dom , 其实就是遍历子节点 , 递归调用 render , 根据上边的逻辑继续转换
node : 当前节点,也就是当前子节点的父节点
props.children : 子节点
像类组件,函数组件一样,我们把它放置在外边
*/
reconcileChildren(node, props.children);
/*
更新 dom
node : 真实 node 节点
props : props 属性和子节点属性以及子节点内容值
将所有属性和子节点属性以及内容值解析渲染给node
*/
updateNode(node, props);
return node;
}
好了,一个简洁的 render
方法逻辑已经呈现在我们的眼前了,剩下的就是要一步步实现对应的转换逻辑了,我们一个一个来:
类组件
先模拟写一个类组件,继承了上边我们自定义的 React.component
类 ;
class ClassComponent extends Component {
render () {
return (
<div className="border">
ClassComponent - {this.props.name}
</div>
);
}
}
解析类组件其实就是需要先实例化,然后再执行 render 返回对应的虚拟 dom 再通过上边 createNode
逻辑进行转换
function updateClassComponent (vnode, parentNode) {
const { type, props } = vnode;
const instance = new type(props);
const vvnode = instance.render();
const node = createNode(vvnode, parentNode);
return node;
}
函数组件
function FunctionComponent (props) {
return <div className="border">FunctionComponent-{props.name}</div>;
}
函数组件就比类组件更简单一些,直接执行函数,返回对应的虚拟 dom 再通过 createNode
逻辑进行转换
function updateFunctionComponent (vnode, parentNode) {
const { type, props } = vnode;
// console.log(vnode) // 打印出来看看 , 创建的时候就决定了类型,原生标签,文本,函数,类,等
const vvnode = type(props); // 执行函数,返回子节点
const node = createNode(vvnode, parentNode);
return node;
}
子节点
遍历子节点,递归执行 render
走到 createNode
转换逻辑 ;
function reconcileChildren (node, children) {
for (let i = 0; i < children.length; i++) {
const child = children[i];
render(child, node);
}
}
更新dom
更新 dom 节点 , nextVal => props
属性和子节点属性以及值 最后挂载给真实 dom
node 节点 ,返回
function updateNode (node, nextVal) {
Object.keys(nextVal)
.filter(k => k !== "children")
.forEach(k => {
// console.log(node[k] + "----" + nextVal[k]);
node[k] = nextVal[k];
});
}
回炉 ~
再回到我们的官方实例, 是不是一个简单的工作原理已经了然于心了
class HelloMessage extends React.Component {
render() {
return (
<div>
Hello {this.props.name}
</div>
);
}
}
ReactDOM.render(
<HelloMessage name="Taylor" />,
document.getElementById('hello-example')
);
后续: 我们再来研究 fiber 数据结构 和 diff 算法
。