react原理
虚拟dom:
用js对象表示dom信息和结构,当状态变更的时候,重新渲染这个js的对象结构,这个js对象称为虚拟dom。
为什么需要虚拟dom?
dom操作很慢,小操作都可能导致重绘,非常消耗性能,相比于dom,js对象操作起来更加快,而且更简单。通过diff算法对比新旧虚拟dom之间的差异,可以批量,最小化的操作don,提高性能。
怎么将虚拟dom转换为真实dom:
react中通过jsx描述视图,jsx其实是一种语法糖。通过babel-loader将jsx语法糖转译,变成React.createElement(...)形式,将虚拟dom转换为真实dom。如果状态变化,虚拟dom也将变化,通过diff算法对比新老虚拟dom,实现真实dom变化。
jsx:
为什么需要jsx?
- 1.开发效率:使用jsx编写模版简单快速
- 2.执行效率:jsx编译为js代码后进行了优化,执行更快
- 3.类型安全:在编译过程中就能发现错误
原理:babel-loader会将jsx预编译为React.createElement()
可以看下react官网一个例子: 使用jsx:
class HelloMessage extends React.Component {
render() {
return (
<div>
Hello {this.props.name}
</div>
);
}
}
ReactDOM.render(
<HelloMessage name="Taylor" />,
document.getElementById('hello-example')
);
不使用jsx:
class HelloMessage extends React.Component {
render() {
return React.createElement(
"div",
null,
"Hello ",
this.props.name
);
}
}
ReactDOM.render(React.createElement(HelloMessage, { name: "Taylor" }), document.getElementById('hello-example'));
下面看下react中几个api
- React.createElement:创建虚拟dom
- React.Component:实现自定义组件
- ReactDom.render():渲染真实dom
看下createElement源码:
/**
* Create and return a new ReactElement of the given type.
* See https://reactjs.org/docs/react-api.html#createelement
*/
export function createElement(type, config, children) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
if (__DEV__) {
warnIfStringRefCannotBeAutoConverted(config);
}
}
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// Remaining properties are added to a new props object
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
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];
}
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}
// Resolve default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
if (__DEV__) {
if (key || ref) {
const displayName =
typeof type === 'function'
? type.displayName || type.name || 'Unknown'
: type;
if (key) {
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
通过上面源码结合不使用jsx的代码,可以看出createElement是将传入的节点转换为虚拟dom。 第一个参数是type,表示节点类型,这里节点类型有很多,如:文本节点,html标签节点,class组件,函数组件,fragment。 第二个参数是config,表示节点的属性和value。 第三个参赛是children,表示子节点。
看下ReactDom.render()源码:
export function render(
element: React$Element<any>,
container: Container,
callback: ?Function,
) {
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
if (__DEV__) {
const isModernRoot =
isContainerMarkedAsRoot(container) &&
container._reactRootContainer === undefined;
if (isModernRoot) {
console.error(
'You are calling ReactDOM.render() on a container that was previously ' +
'passed to ReactDOM.createRoot(). This is not supported. ' +
'Did you mean to call root.render(element)?',
);
}
}
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
源码结合render使用,可以看出第一个参数是要渲染的dom,第二个参数是要将dom渲染到的容器,最后返回。
再看下Compoent源码:
function Component(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
// We initialize the default updater but the real one gets injected by the
// renderer.
this.updater = updater || ReactNoopUpdateQueue;
}
Component.prototype.isReactComponent = {};
其中通过Component.prototype.isReactComponent来区分是否是class组件还是函数组件。
下面实现下上面三个简易api
/src/index.js
import React from "./react/index";
import ReactDOM from "./react/react-dom";
import Component from "./react/Component";
import "./index.css";
const jsx = (
<div className="border">
<p>aaa</p>
</div>
);
ReactDOM.render(jsx, document.getElementById("root"));
因为是通过babel编译jsx为createElement,通过React导出。所以在index.js中写createElement
// react/index.js
// 创建react element,并返回
function createElement(type, config, ...children) {
// children是数组,此时需要判断child是否是对象,不是对象代表是文本节点,为了统一数据结构,
// 创建文本节点函数如下,包括type和props
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
}
};
}
// 导出createElement
export default {
createElement
};
class组件是继承Component,所以Component是构造函数,将props挂载出去,同时标志是否是函数组件还是class组件
// react/Component.js
export default function Component(props) {
this.props = props;
}
Component.prototype.isReactComponent = {};
上面src/index.js目前只写了html节点和文本节点,现在实现render将节点显示出来。
// react/react-dom.js
// 将虚拟dom转换为真实dom,并渲染出来
function render(vnode, container){
// vnode => node
const node = createNode(vnode);
container.appendChild(node)
}
// 将vnode转换为真是node
function createNode(vnode){
const { type, props } = vnode;
// 开始定义真实节点为null;
let node = null;
// 通过type判断是哪种节点
if(type === 'TEXT'){
// 文本节点
node = document.createTextNode('');
}else if(typeof type === 'string'){
// html节点
node = document.createElement(type)
}
// 最后返回真实node
return node;
}
上面将父节点dom渲染出来,下面将节点的属性和子节点渲染出来。 渲染子节点:
function createNode(vnode){
const { type, props } = vnode;
...
// 渲染子节点
// 此时props.children是虚拟节点,node是要渲染到的容器container
reconcileChildren(props.children, node)
return node;
}
// 渲染子节点
function reconcileChildren(children, node){
// children是数组
for(let i =0;i<children.length;i++){
// 考虑jsx是数组渲染dom
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 createNode(vnode){
const { type, props } = vnode;
...
// 渲染子节点
// 此时props.children是虚拟节点,node是要渲染到的容器container
reconcileChildren(props.children, node)
// 渲染属性
// 将props内容渲染到node上面
updateNode(node, props);
return node;
}
// 渲染子节点
function reconcileChildren(children, node){
// children是数组
for(let i =0;i<children.length;i++){
// 考虑jsx是数组渲染dom
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(i => {
node[i] = nextVal[i]
})
}
现在将文本节点和html节点渲染出来,看下结果:
下面写下渲染class组件,函数组件,fragment。
更新class组件:
function createNode(vnode){
const { type, props} = vnode;
let node = null;
if(type === 'TEXT'){
node = document.createTextNode('')
}else if(typeof type === 'string'){
node = document.createElement(type)
}else if(typeof type ==='function'){
// 通过isReactComponent区分是函数组件还是class组件
node = type.prototype.isReactComponent ?
updateClassCom(vnode):
updateFuncCom(vnode)
}else{
node = document.createDocumentFragment()
}
reconcileChildren(props.children, node)
updateNode(node,props)
return node;
}
// 更新class组件
function updateClassCom(vnode){
const { type, props} = vnode;
// 此时type是class,所以需要new
const cmp = new type(props);
// 此时cmp原型上有render()
// 调用render,获取虚拟dom
const vvnode = cmp.render();
// 将虚拟dom转为真实dom,并渲染
const node = createNode(vnode)
return node;
}
// 更新函数组件
function updateFuncCom(vnode){
const { type, props} = vnode;
const cmp = type(props)
const vvnode = cmp.render();
const node = createNode(vvnode)
return node;
}
看下demo如下:
import React from "./react/index";
import ReactDOM from "./react/react-dom";
import Component from "./react/Component";
import "./index.css";
class ClassComponent extends Component {
static defaultProps = {
color: "pink"
};
render() {
return (
<div className="border">
class组件-{this.props.name}
<p className={this.props.color}>omg</p>
</div>
);
}
}
function FunctionComponent(props) {
return <div className="border">函数组件-{props.name}</div>;
}
const jsx = (
<div className="border">
<p>aaa</p>
<ClassComponent name="class" color="red" />
<FunctionComponent name="function" />
<>
<h1>aaa</h1>
<h1>bbb</h1>
</>
</div>
);
ReactDOM.render(jsx, document.getElementById("root"));
此时,文本节点,html标签,函数组件,class组件和fragment都渲染出来了。
以上学习了createElement,Compoent,render()三个简易api。