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是react17之后才进行增加的。通过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组件初步分析
虽然react17之后强烈推荐使用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的设计哲学。