React
核心 API
加入了自己的理解和总结。注意代码中将react 版本锁定在17.0.0 之前。
简单粗暴,我们先来看下面这段代码:
import React from 'react'
import ReactDOM from 'react-dom'
function HelloMessage(props) {
return (
hello, {props.name}
尝试一起来写个简易的react core api}
)
}
ReactDOM.render(
,
document.getElementById('root'));
我们知道,JSX
其实是语法糖,17.0.0 版本前通过Babel将jsx转换成React.createElement(type,config,children)
的函数调用,React.createElement
方法主要是对一系列参数「type,key,ref,props,childrend」进行处理,最终创建并返回虚拟dom,这也是我们为何在头部引入React
的缘故;17.x版本之后,不需要显示导入import React from 'react'
,babel对jsx进行了新的转换不需要依React.createElement
,babel编译后会自动导入
import { jsx as _jsx } from 'react/jsx-runtime',
...
return _jsx(type, config, children)
我们可以通过Babel REPL来看下,这段代码实际被转换成什么了
import React from 'react';
import ReactDOM from 'react-dom';
function HelloMessage(props) {
return React.createElement(
"div",
null,
"hello,
",
props.name,
React.createElement("span", null, "尝试一起来写个简易的react core api"));
}
ReactDOM.render( React.createElement(HelloMessage, {
name: "lxcy"
}), document.getElementById('root'));
OK! 我们再来捋一下createElement
到底做了什么?结合源码,我简单总结了几点
到此,我们可以尝试实现createElement
function createElement(type, config, ...children) {
delete config.__self
delete config.__source
// 处理一些特殊的props, 如:key, ref, 源码中还有对default Props的处理
const { key, ref, ...extraProps } = config
return {
$$typeof: Symbol('react.element'), // 标识 React组件类型
type,
ref,
key,
props: {
...extraProps,
children: children.map(c => typeof c === 'object' ? c : createTextNode(c))
}
}
}
function createTextNode(value) {
return {
type: undefined,
props: {
nodeValue: value,
children: []
}
}
}
render
接受createElement
返回的js对象和一个容器,将 vdom 转换成真实的 dom,然后挂载到容器中;
render
函数返回的是 type 的实例,比如是原生 dom 则返回元素的实例,class 组件返回类的实例对象,函数组件则返回 null。
function render(vnode, container) {
// vdom 是一棵 js 对象树,通过递归 diff 来更新 真实 dom 树
// 为了方便递归,我们将抽取为一个函数, 作为虚拟 dom 的 diff 块
const node = initVDOM(vnode);
container.appendChild(node);
return node;
}
根据 type 类型,分为 class 组件、function 组件、原生标签和文本节点;源码中 Component
类会在其原型上增加类组件标识Component.prototype.isReactComponent = {};
因此,我们可以通过此属性来区分类组件和函数组件。
function shouldConstruct(Component) {
const prototype = Component.prototype;
return !!(prototype && prototype.isReactComponent);
}
function initVDOM(vnode) {
const { type } = vnode;
if (typeof type === 'function') {
if (shouldConstruct(type)) {
return createClassElement(vnode);
} else {
return createFunElement(vnode);
}
} else if (typeof type === 'string') {
return createDOMElement(vnode);
} else {
return document.createTextNode(vnode.props.nodeValue);
}
}
先来实现原生标签
注意⚠️:这里以及后面的实现都未考虑状态变更,只实现了基本的 dom 挂载,在后续 fiber 架构实现 文章中会补齐状态变更和 dom 更新的实现;
function createDOMElement(vnode) {
// 通过 type 可以拿到元素标签
const { type, props } = vnode;
// 1. 创建元素
const node = document.createElement(type);
// 分离出一些特殊的属性,比如className、htmlFor等
const { className, htmlFor, children, ...res } = props;
// 2. 设置属性
if (className) {
node.setAttribute('class', className);
}
if (htmlFor) {
node.setAttribute('for', htmlFor);
}
Object.keys(res).forEach(k => node.setAttribute(k, props[k]));
// 3. 遍历解析子元素,并插入父节点
children.forEach(c => {
// 处理 数组遍历出多个子节点
if (Array.isArray(c)) {
c.forEach(l => {
node.appendChild(initVDOM(l))
})
} else {
node.appendChild(initVDOM(c))
}
})
// 4. 最后将元素返回
return node;
}
vnode 对象映射了元素,属性和子元素,因此可以通过type
创建元素,插入 props 中的属性,react 中有一些特殊的属性名,如className, htmlfor ...
需要进行处理,包括事件处理名,如onClick
; 这里需要特别说明的是,在处理 children
时,我们需要考虑数组类型,当子元素是数组时,需要逐一递归处理;常见场景如:
{ list.map(c => {c.name}) }
同理,在 class 组件和 function 组件 中,我们只需要拿到 vdom ,然后递归调用 initVDOM(vdom)
, 函数组件非常简单,type 是一个函数,可以直接通过 type(props)
调用来返回 vnode树,class 组件则可以通过实例化并调用其render
函数 来返回;
// 如果是类组件,获取实例,并调用 render() 来返回 vnode
function createClassElement(vnode) {
const { type, props } = vnode
// 通过 class 组件实例化,调用render()函数,返回渲染元素
const instance = new type(props);
const rendererElement = instance.render();
return initVDOM(rendererElement);
}
// 如果是函数组件,它的实例为null, 直接调用函数即可
function createFunElement(vnode) {
const { type, props } = vnode
const rendererElement = type(props);
return initVDOM(rendererElement);
}
至此,100行代码左右,就可以初步实现一个react, 当然还有非常多的细节我们没有去考虑,包括最重要的状态更新,React16
版本引入了fiber架构,我将在下一篇简易实现fiber 架构 中来实现状态的更新;
这里简单补齐一下Component
的代码
class Component {
constructor(props) {
this.props = props
this.state = {}
this.updater = {}
}
setState(ins) { }
forceUpdate() {}
}
Component.prototype.isReactComponent = {};
关于setState 和 forceUpdate
的具体实现,将会放在单独的文章中讲解,敬请关注!