JSX
相信使用react
的大家对于jsx
已经游刃有余了,可是你真的了解jsx
的原理吗?
让我们由浅入深,来一层一层揭开jsx
的真实面目。
文章中涉及的代码地址 戳我👇。
React.createElement
在react
官方中讲到,关于jsx
语法最终会被babel
编译成为React.createElement()
方法。
我们来看看这段jsx
<div className="wang.haoyu">hello</div>
经过babel
编译后它变成这样的代码:
React.createElement("div", {
className:'wang.haoyu'
}, "hello");
当jsx
中存在多个节点元素时,比如:
<div>hello<span>world</span></div>
它会将多个节点的jsx
中children
属性变成多个参数进行传递下去:
React.createElement("div", null, "hello", React.createElement("span", null, "world"));
可以看到,外层的div
元素包裹的children
元素依次在React.createElement
中铺平排列进去,并不是树型结构排列。
需要注意的是,旧的
react
版本中,只要我们使用jsx
就需要引入react这个包。而且引入的变量必须大写React
,因为上边我们看到babel
编译完jsx
之后会寻找React
变量。
新版本中,不再需要引入
React
这个变量了。有兴趣的同学可以去看看打包后的react
代码,内部会处理成为Object(s.jsx)("div",{ children: "Hello" })
,而老的版本是React.createElement('div',null,'Hello')
。
这两种方式效果和原理是一模一样的,只是新版额外引入包去处理了引入。所以不需要单独进行引入
React
。
React
元素
React
之中元素是构建React
的最小单位,其实也就是虚拟Dom对象。
本质上jsx
执行时就是在执行函数调用,是一种工厂模式通过React.createElement
返回一个元素。
const element = <div>Hello</div>
console.log(element,'element')
先忽略掉一些ref/key
之类的属性,这个时候来看我们发现它其实就是一个js
对象,记录了type
表示元素类型。props
表示元素的接受的prop
,注意这里会将jsx
内部标签内容插入到props
的children
属性中。
需要注意的是这里的
children
属性,如果内部标签元素存在多个子元素时候。children
会是一个数组。因为这里仅仅只有文本节点,所以只有一个Hello
。
在我们平常使用react
项目的时候,index.tsx
中总是会存在这样一段代码:
ReactDOM.render(<App />, document.getElementById('root'));
结合上边我们所讲的React.createElement
,我们不难猜出ReactDOM.render
这个方法它的作用其实就是按照React.createElement
生成的虚拟DOM节点对象,生成真实DOM插入到对应节点中去,这就是简单的渲染过程。
元素的更新
react
中元素本身是不可变的。
比如:
const element = <h1 title="hello" >Hello</h1>
console.log(JSON.stringify(element,null,2))
当我们想将它的内容改成world
时,如果直接通过
element.props.children = 'world'
这样是不可以的,react
会提示:
Uncaught TypeError: Cannot assign to read only property 'children' of object '#<Object>'
无法给一个只读属性children
进行赋值,修改其他属性比如type
之类同理也是不可以的。
当我们通过这种方式给react
元素增加属性时,也是增加的。
Cannot add property xxx, object is not extensible
not extensible
是react
17之后才进行增加的。通过Object.freeze()
将对象进行处理元素。
需要注意
Object.freeze()
是一层浅冻结,在react
内部进行了递归Object.freeze()
。
所以在react
中元素本身是不可变的,当元素被创建后是无法修改的。只能通过重新创建一个新的元素来更新旧的元素。
你可以这样理解,在react
中每一个元素类似于动画中的每一帧,都是不可以变得。
当然在
react
更新中仅仅会更新需要更新的内容,内部会和Vue相同的方式去进行diff算法,高效更新变化的元素而不是更新重新渲染所有元素。
jsx
原理分析
需要注意我们这里使用旧的
React.createElement
方法,如果是^17
版本下,需要在环境变量中添加DISABLE_NEW_JSX_TRANSFORM=true
。
上边我们已经分析过React.createElement
这个方法的返回值,接下来我们就尝试自己来实现jsx
的渲染。
先来看看原本React中createElement
方法的返回值:
import React from 'react';
import ReactDOM from 'react-dom';
const element = (
<div className="header" style={{ color: 'red' }}>
<span>hello</span>world
</div>
);
console.log(JSON.stringify(element, null, 2), 'element');
接下来我们就根据结果来推写法,实现一个简单的createElement
方法
实现React.crateElement
方法-原生DOM
元素的渲染
- 实现
utils/react.js
// 这里之所以额外书写一个 wrapToDom元素 是为了方便对比 react源码中没有这段方法是特殊处理的
// 我们为了方便 将普通类型 也统一处理成为Object
const React = {
createElement: function (type, config, children) {
const props = {
...config,
};
// 上边讲到babel编译jsx后
// 如果参数大于3个 那么就有多个children props.children是一个数组
if (arguments.length > 3) {
props.children = Array.prototype.slice.call(arguments, 2);
} else {
props.children = children;
}
return {
type,
props,
};
},
};
export default React;
这一步我们已经实现了基础的React.createElement
方法。
index.tsx
import React from './utils/react';
import ReactDOM from 'react-dom';
// babel编译后的代码会引入 React.createElement
// 此时的React指向的是我们自己写的React
const element = (
<div className="header" style={{ color: 'red' }}>
<span>hello</span>world
</div>
);
ReactDOM.render(element, document.getElementById('root'));
实现ReactDOM.render
方法-将react
中源生DOM元素变成真实元素插入页面
- 接着咱们先来实现一个对于
children
类型的判断方法
// utils.js
// 常亮 判断文本类型
const REACT_TEXT = Symbol('REACT_TEXT')
// 无论以前是什么元素,都转成VDOM的对象形式
export function transformVNode(element) {
// 额外处理文本节点 将文本节点输出和其他节点一样的Object类型
if(typeof element === 'string' || typeof element === 'number') {
return { type: REACT_TEXT, props: { content: element } }
}
return element
}
- 接下来我们改造一下我们之前写好的
React.createElement
方法
import { transformVNode } from './utils';
const React = {
createElement: function (type, config, children) {
const props = {
...config,
};
if (arguments.length > 3) {
props.children = Array.prototype.slice
.call(arguments, 2)
.map(transformVNode);
} else {
props.children = transformVNode(children);
}
return {
type,
props,
};
},
};
export default React;
- 接下来我们已经拥有了对应的
VDom
对象,就可以开始实现React.render
方法。
React.render
核心思想就是将我们的Vdom
对象编程浏览器可以识别的标签节点挂载在对应元素上
。
/**
* 把虚拟DOM变成真实DOM插入
* @param {Object} vDom 虚拟DOM
* @param {HTMLElement} el 元素
*/
import { REACT_TEXT } from './constant';
// 真正渲染方法
function render(vDom, el) {
const newDom = createDom(vDom);
el.appendChild(newDom);
}
// 先不考虑自定义组件
function createDom(vDom) {
const { type, props } = vDom;
let dom;
// 文本节点
if (type === REACT_TEXT) {
dom = document.createTextNode(props.content);
} else {
dom = document.createElement(type);
}
// 更新属性
if (props) {
// 更新跟节点Dom属性
updateProps(dom, {}, props);
// 处理children 考虑undefined/null 不做任何处理
// 考虑 children是一个数组 那么就表示他拥有多个儿子
// 考虑children是一个Object 那么他就只有一个儿子节点
if (typeof props.children === 'object' && props.children.type) {
// 单个元素
render(props.children, dom);
} else if (Array.isArray(props.children)) {
// 多个元素
reconcileRender(props.children, dom);
}
}
// 记录挂载节点
vDom.__dom = dom;
return dom;
}
// 挂载多个dom元素 React.createElement先不考虑递归
function reconcileRender(vLists, parentDom) {
for (let node of vLists) {
render(node, parentDom);
}
}
/**
* 把虚拟DOM变成真实DOM插入
* @param {HTMLElement} dom 元素
* @param {Object} oldProps 元素本身的props 用于更新这里暂时用不到
* @param {Object} newProps 元素新的props
*/
function updateProps(dom, oldProps, newProps) {
// 合并props 暂时没有老的 仅处理新的
Object.keys(newProps).forEach((key) => {
if (key === 'children') {
// 单独处理children
return;
}
if (key === 'style') {
addStyleToElement(dom, newProps[key]);
} else if (key === 'content') {
// 文本不做任何操作
} else {
dom[key] = newProps[key];
}
});
}
function addStyleToElement(dom, styleObject) {
Object.keys(styleObject).forEach((key) => {
const value = styleObject[key];
dom.style[key] = value;
});
}
const ReactDOM = {
render,
};
export default ReactDOM;
其实这里的的核心思想就是通过render
方法将虚拟DOM根据对应的属性转化成为真实DOM节点进行递归挂载,最终通过appendChild
渲染到页面上。
写在最后
目前来说,我们已经基本实现了React.createElement
和ReactDom.render
这两个方法。
只不过目前来说仅仅针对于源生DOM
节点进行了处理。
在React
中我们知道会有各种各样我们自己定义的组件,接下来我们会一步一步去看看这些组件的渲染流程。
Function Component
上边我们讲到了React
中关于源生DOM节点的渲染和挂载。现在我们来看看关于Function Component
的渲染。
当然我们先来看看关于Function Component
渲染的结果。
import React from 'react';
import ReactDOM from 'react-dom';
interface IProps {
name: string;
}
const MyComponent: React.FC<IProps> = (props) => {
return (
<div>
你好,
<p>{props.name}</p>
</div>
);
};
const element = <MyComponent name="wang.haoyu" />;
console.log(element);
上边这段代码我们创建了一个函数组件,并且使用了这个函数组件赋值给了element
。我们来看看它打印出了什么:
这个时候我们可以看到,相对于普通dom节点。纯函数组件的不同点:
$$typeof
为Symbol(react.element)
表示这个元素节点的类型是一个纯函数组件。- 在普通dom节点中,
type
类型为对应的标签类型。而当为纯函数组件时。type
类型为函数自身。
组件的
type
类型,就是函数自身,这点很重要。
相信看到这里你已经明白element
纯函数组件元素应该如何转化成为上边的VDOM
对象了。我们再来看看babel的编译:
可以看到,针对纯函数组件的jsx
最终就是编译成为
const element = /*#__PURE__*/React.createElement(MyComponent, {
name: "wang.haoyu"
});
修改React.createElement方法
来看看我们之前写的React.createElement
方法,第一个参数为type
,第二个为组件的props
,第三个为children
...
我们发现我们之前写的React.createElement
方法在纯函数组件是不需要任何修改的:
import { transformVNode } from './utils';
const React = {
createElement: function (type, config, children) {
const props = {
...config,
};
if (arguments.length > 3) {
props.children = Array.prototype.slice
.call(arguments, 2)
.map(transformVNode);
} else {
props.children = transformVNode(children);
}
return {
type,
props,
};
},
};
export default React;
传入一个纯函数组件,仍然能返回正确的结果(这里先不考虑$$typeof
的结果)。返回虚拟DOM的type
属性指向它自身,config
为传入的props
。剩余children
作为属性挂载在props.children
上。
其实我们平常使用的
displayName
,defaultProps
都是挂载在这个函数自身的属性。看到这里你应该也能明白为什么我们平常需要获取这些属性的时候,需要使用xxx.type.displayName
等。
修改ReactDOM.render方法
既然React.createElement
方法不需要做任何修改。那么我们就来看看对应的ReactDOM.render
方法。
此时我们render
方法希望我们传入一个自定义函数组件,ReactDOM
也会将我们的自定义组件转化成为真实DOM进行挂载。
我们先来分析分析,经过React.createElement(FunctionCompoent,props,children)
。传入的type
是一个自身的函数,这个函数返回的是一个JSX
对象。
如果这么说你还是不能理解的话,你可以这样理解这段代码:
const MyComponent: React.FC<IProps> = (props) => {
return (
<div>
你好,
<p>{props.name}</p>
</div>
);
};
上边这段JSX
代码会babel
在编译阶段转化成为这样的代码:
const MyComponent = props => {
return /*#__PURE__*/React.createElement("div", null, "\u4F60\u597D,", /*#__PURE__*/React.createElement("p", null, props.name));
};
其实你完全可以将MyComponent
调用后返回的结果理解成为一个对象(本质上也就是对象),这样的话你会好理解很多:
注意:返回的是一个React.createElement函数的调用,这个函数调用返回的就是一个虚拟DOM对象。
搞清楚这些,我们来尝试改写ReactDOM.render
方法:
/**
* 把虚拟DOM变成真实DOM插入
* @param {Object} vDom 虚拟DOM
* @param {HTMLElement} el 元素
*/
import { REACT_TEXT } from './constant';
// 真正渲染方法
function render(vDom, el) {
const newDom = createDom(vDom);
el.appendChild(newDom);
}
// 先不考虑自定义组件
function createDom(vDom) {
const { type, props } = vDom;
let dom;
// 文本节点
if (type === REACT_TEXT) {
dom = document.createTextNode(props.content);
} else if (typeof type === 'function') {
// 如果是纯函数组件 type是一个函数 并且运行函数会返回一个虚拟DOM对象
// 通过createDom方法递归将虚拟DOM转化成真实DOM返回
return mountFunctionComponent(vDom);
} else {
dom = document.createElement(type);
}
// 更新属性
if (props) {
// 更新跟节点Dom属性
updateProps(dom, {}, props);
// 处理children 考虑undefined/null 不做任何处理
// 考虑 children是一个数组 那么就表示他拥有多个儿子
// 考虑children是一个Object 那么他就只有一个儿子节点
if (typeof props.children === 'object' && props.children.type) {
// 单个元素
render(props.children, dom);
} else if (Array.isArray(props.children)) {
// 多个元素
reconcileRender(props.children, dom);
}
}
// 记录挂载节点
vDom.__dom = dom;
return dom;
}
// 挂载函数式组件
function mountFunctionComponent(vDom) {
const { type, props } = vDom;
// type(props)执行FunctionComponent函数,返回对应的虚拟DOM对象
// 通过createDom方法将虚拟DOM转化成真实DOM返回
return createDom(type(props));
}
// 挂载多个dom元素 React.createElement先不考虑递归
function reconcileRender(vLists, parentDom) {
for (let node of vLists) {
render(node, parentDom);
}
}
/**
* 把虚拟DOM变成真实DOM插入
* @param {HTMLElement} dom 元素
* @param {Object} oldProps 元素本身的props 用于更新这里暂时用不到
* @param {Object} newProps 元素新的props
*/
function updateProps(dom, oldProps, newProps) {
// 合并props 暂时没有老的 仅处理新的
Object.keys(newProps).forEach((key) => {
if (key === 'children') {
// 单独处理children
return;
}
if (key === 'style') {
addStyleToElement(dom, newProps[key]);
} else if (key === 'content') {
// 文本不做任何操作
} else {
dom[key] = newProps[key];
}
});
}
function addStyleToElement(dom, styleObject) {
Object.keys(styleObject).forEach((key) => {
const value = styleObject[key];
dom.style[key] = value;
});
}
const ReactDOM = {
render,
};
export default ReactDOM;
可以看到我们在createDOM
方法上做了小小的修改,判断如果传入的vDom
的type
是一个函数的话:
- 传入
vDom
的props
,运行这个函数。得到返回的vDom
对象。 - 拿到
vDom
对象后,通过之前的createDom
方法将vDom
转化成真实节点返回。 - 此时
render
方法就可以拿到对应生成的真实DOM
对象,从而挂载在DOM
元素上。
本质上还是通过递归进行判断,如果是函数那么就运行函数的到返回的vDOM,然后在通过
createDom
将vDom
转化为对应的真实DOM挂载。
关于
jsx
转化成React.crateElement(...)
是babel
在做这一层的转译。后续涉及编译原理的知识在展开去看。
其实从这里也可以看出为什么
React
中返回的jsx
必须要求最外层元素需要一个包裹元素。
ReactDom.render方法接受传入的Element。内层只有一个根节点时,比如
const element = <div>This is <p>me</p>.<p>My name is wang.haoyu</p></div>
编译后:
React.createElement(
'div',
null,
'This is ',
React.createElement('p', null, 'me'),
'.',
React.createElement('p', null, 'My name is wang.haoyu')
);
ReactDOM.render
的处理方式为传入一个React
虚拟节点。通过该虚拟节点生成真实DOM
,然后在一层一层递归它的children
,将children
的虚拟节点通过createDOM
方法生成对应的真实DOM
然后在挂载在对应的父节点DOM上。
明确一个思想: ReactDOM.render()方法仅仅支持传入一个
VDOM
对象和el
。他的作用就是将VDOM
生成真实DOM挂载在el
上。此时如果VDOM
存在一些children
,那么ReactDOM.render
会递归他的children
,将children
生成的DOM节点挂载在parentDom
上。一层一层去挂载。
针对FunctionComponet
中children
的渲染
为了加深了解,我们再来走一遍我们自己手写的React
和ReactDOM
的过程。
当我传入这样一段代码:
function MyComponent(props) {
return (
<div>
<p>hello</p>
<p>{props.children}</p>
</div>
);
}
const element = (
<MyComponent name="wang.haoyu">
<div>你好</div>
<p>hello</p>
</MyComponent>
);
ReactDOM.render(element)
首先针对于我们刚才的代码,经过Babel
处理后会变成这样一段代码
const element1 = React.createElement(
MyComponent,
{
name: 'wang.haoyu',
},
React.createElement('div', null, '\u4F60\u597D'),
React.createElement('p', null, 'hello')
);
接下来会进入我们已经实现的React.createElement
会将MyComponent
这个函数组件进行转化。会转化成这样样子的对象:
const element = {
props: {
name: 'wang.haoyu',
children: [{
type: 'div',
props: {
children: {
type: Symbol(text)
content: '你好'
}
}
},
{
type: 'p',
props: {
children: {
type: Symbol(text)
content: 'hello'
}
}
}
]
},
type: MyComponent
}
之后我们调用了RenderDOM.render(element,el)
进行了渲染。
首先进入createDOM
函数中发现他的类型是一个函数组件。那么按照我们的逻辑就会运行这个函数组件,同时传入它的props
。此时我们可以清晰的看到type(props)
的结构。
调用这个函数组件并且传入对应的props
。
当我们调用这个函数组件的时候,会返回一个jsx
,这一步我们已经轻车熟路了。最终的jsx
会被转译成为VDOM对象:
function MyComponent(props) {
return (
<div>
<p>hello</p>
<p>{props.children}</p>
</div>
);
}
转化后的VDOM对象(省略React.crateElement
,直接输出VDOM结果):
- 第一次进入
createDOM
方法,发现他是FC
类型。传入自身props
调用自身FC
函数。 - 运行完毕函数后,会递归调用
createDOM
,此时VDOM
已经变成了这样的对象。
{
type: 'div',
props: {
children: [
{
type: 'p',
props: {
children: {
type: Symbol('text'),
content: 'hello',
},
},
},
{
type: 'p',
props: {
// 注意这里的props.children
// 为了理解 我这里先这样写 props其实就是函数组件调用时传入的那个props
children: props.children,
},
},
],
},
};
真实应该是这样:
{
type: 'div',
props: {
children: [
{
type: 'p',
props: {
children: {
type: Symbol('text'),
content: 'hello',
},
},
},
{
type: 'p',
props: {
// 本质上这里的内容就相当于 props.children
// children: props.children
children: [
{
type: 'div',
props: {
children: {
type: 'Symbol(text)',
content: '你好',
},
},
},
{
type: 'p',
props: {
children: {
type: 'props.children',
content: 'hello',
},
},
},
],
},
},
],
},
};
其实简单来说,组件标签内的元素。直接将它看作转译后的对象作为入参传入
FunctionComponent
中进行调用自身函数,得到返回的jsx
从而得到返回的新的VDOM
对象。
这里其实并不难,只是有部分绕。如果不是很明白可以尝试自己手写几遍跟着debugger
看看。搞懂之后过一段时间再来看看加深记忆就会很清晰了。
写在最后
之前对于React
中关于<MyComponet>你好</Mycomponent>
,以现在的逻辑去渲染这段代码还存在疑惑。之后会梳理总结下debugger
的详细流程。- ~~梳理完上述流程后,会展开谈一谈
React
中关于class
组件的渲染。
class
组件的渲染
class
组件初步分析
虽然react
17之后强烈推荐使用hooks
代替class component
,但是短期内react
并没有移除类组件的计划。并且一些情况下类组件是必不可少的,而且类组件中涉及react
中很多知识。所以接下来我们来看看react
中关于class
组件的实现。
React
内部组件分为源生组件和自定义组件。
- 源生组件经过
babel
编译后的VDOM
的type
属性类型是一个字符串,表示当前元素的节点标签。 - 自定义组件经过编译后
type
指向自身的函数。
在
javascript
的世界中其实并没有class
的概念,针对class
也不过是function
的语法糖。
我们来看看类组件的编译结果:
可以清楚的看到,类组件编译后React.createElement(ClassComponent,{ name:"wang.haoyu"})
。传入的type
(第一个参数),也为类组件自身。(函数)
当然说到这里一些同学会存在疑问了,既然类组件和函数组件type
属性都是一个Function
。那么如何区分类组件和函数组件呢。
在React
中class
组件因为继承自React.component
,所以class
组件的原型上会存在一个isReactComponent
属性。这个属性仅有类组件独有,函数组件是没有的,这就可以区分类组件和函数式组件。
我们尝试访问这个属性来看看:
其实ts
的类型提示已经告诉我们结果了,区分类组件和函数组件的区别就是类组件的原型上存在属性isReactComponent
属性。
class
类组件上的isReactComponent
值是一个空对象{}
,仅仅作为标示。
实现class
组件的渲染
接下来我们来实现classComponet
的渲染流程。
首先,我们先来实现React.Component
这个这个父类。
改造React.js
文件
我们给React
对象下新增一个Component
属性:
import { transformVNode } from './utils';
import { Component } from './component';
const React = {
Component,
createElement: function (type, config, children) {
const props = {
...config,
};
if (arguments.length > 3) {
props.children = Array.prototype.slice
.call(arguments, 2)
.map(transformVNode);
} else {
props.children = transformVNode(children);
}
return {
type,
props,
};
},
};
export default React;
接下来我们新建一个Component
文件来实现这个Component
类:
// component.js
class Component {
constructor(props) {
this.props = props;
}
}
// 实现类组件独有属性
Component.prototype.isReactComponent = {};
export { Component }
接下来我们来改造React.render
方法,让他支持class
组件的渲染:
/**
* 把虚拟DOM变成真实DOM插入
* @param {Object} vDom 虚拟DOM
* @param {HTMLElement} el 元素
*/
import { REACT_TEXT } from './constant';
// 真正渲染方法
function render(vDom, el) {
const newDom = createDom(vDom);
el.appendChild(newDom);
}
// 先不考虑自定义组件
function createDom(vDom) {
const { type, props } = vDom;
let dom;
// 文本节点
if (type === REACT_TEXT) {
dom = document.createTextNode(props.content);
} else if (typeof type === 'function') {
if (type.prototype.isReactComponent) {
// 原型上存在isReactComponent属性
return mountClassComponent(vDom);
} else {
// 如果是纯函数组件 type是一个函数 并且运行函数会返回一个虚拟DOM对象
// createDOM方法本质上就是将虚拟DOM对象转化成为真实DOM返回
return mountFunctionComponent(vDom);
}
} else {
dom = document.createElement(type);
}
// 更新属性
if (props) {
// 更新跟节点Dom属性
updateProps(dom, {}, props);
// 处理children 考虑undefined/null 不做任何处理
// 考虑 children是一个数组 那么就表示他拥有多个儿子
// 考虑children是一个Object 那么他就只有一个儿子节点
if (typeof props.children === 'object' && props.children.type) {
// 单个元素
render(props.children, dom);
} else if (Array.isArray(props.children)) {
// 多个元素
reconcileRender(props.children, dom);
}
}
// 记录挂载节点
vDom.__dom = dom;
return dom;
}
// 挂载class组件
function mountClassComponent(vDom) {
const { type, props } = vDom;
return createDom(new type(props).render());
}
// 挂载函数式组件
function mountFunctionComponent(vDom) {
const { type, props } = vDom;
// type(props)执行FunctionComponent函数,返回对应的虚拟DOM对象
// 通过createDom方法递归将虚拟DOM转化成真实DOM返回
return createDom(type(props));
}
// 挂载多个dom元素 React.createElement先不考虑递归
function reconcileRender(vLists, parentDom) {
for (let node of vLists) {
render(node, parentDom);
}
}
/**
* 把虚拟DOM变成真实DOM插入
* @param {HTMLElement} dom 元素
* @param {Object} oldProps 元素本身的props 用于更新这里暂时用不到
* @param {Object} newProps 元素新的props
*/
function updateProps(dom, oldProps, newProps) {
// 合并props 暂时没有老的 仅处理新的
Object.keys(newProps).forEach((key) => {
if (key === 'children') {
// 单独处理children
return;
}
if (key === 'style') {
addStyleToElement(dom, newProps[key]);
} else if (key === 'content') {
// 文本不做任何操作
} else {
dom[key] = newProps[key];
}
});
}
function addStyleToElement(dom, styleObject) {
Object.keys(styleObject).forEach((key) => {
const value = styleObject[key];
dom.style[key] = value;
});
}
const ReactDOM = {
render,
};
export default ReactDOM;
其实本质上关于class
组件的挂载实现和FunctionComponet
组件的实现是类似的。
- 首先判断传入的
type
是否是函数,如果是函数那么无非两种类型。 - 接下来判断是否是
class
组件,因为我们之前已经给父类的prototype
上挂载了isReactComponent
方法。所以通过子类.prototype.isReactComponent
去查找是否是class
组件。 - 如果是
class
组件,那么我们需要做的同样是将他的render
方法返回的Vdom
对象通过createDom
方法转化为真实Dom
节点来进行挂载。 - 把握了核心需要做的事情,接下来就很简单,无非就是
createDom(new type(props).render())
的到render
方法返回的vDom
对象,通过createDom
去将虚拟DOM转化为真实Dom
。
这里我们就已经实现了简单的
class
组件的挂载了。其实万遍不离其宗。本质上就是我们需要通过createDom
将传入的vDom
对象转化成真实DOM。
核心思想
createDom
如果传入的是一个普通节点,那么就直接根据对应type
创建标签。createDom
如果传入的是一个函数组件,那么就调用这个函数组件得到它返回的vDom
节点,然后在通过createDom
将vDom
渲染成为真实节点。createDom
如果传入的是一个class
组件,那么就new Class(props).render()
得到返回的vDom
对象,然后在将返回的vDom
渲染成为真实Dom
。
无论是
FC
还是CC
这两种组件,内部本质上还是基于普通DOM节点的封装,所以我们只需要递归调用他们直接返回基本的DOM节点之后进行挂载就OK啦~万变不离其宗嘛
写在最后
至此我们已经完成了基础的FunctionComponent
,classComponent
的渲染。
到这里我们已经了解基础的渲染流程,通过createDom
方法将vDom
对象递归转变为真实DOM
。
之后我们会更加深入去了解ClassComponet
和FunctionCompont
,去深入体会React
的设计哲学。