介绍
官网上这么解释React的“用于构建用户界面的JavaScript库”,React 使用声明式组件化的方式编写UI,让你的代码更加可靠,且方便调试。
React源码还是非常复杂的,我们今天就来简化的实现下React结构核心API:
- React.createElement
- React.Component
- ReactDom.render
先了解下JSX
1.什么是 JSX?
React使用 JSX 来替代常规的JavaScript。- JSX 是一个看起来很像XML的
JavaScript语法扩展。
2.为什么需要在React用 JSX?
- JSX 执行更快,因为它的编译为
JavaScript代码后进行了优化。 - 进行类型安全检查,可防止注入攻击,在编译过程中就能发现错误。
- 编写模块更加简单快捷。
3.怎么用 JSX,这里就不详细阐述了,建议看看官方文档。
4.原理:babel-loader会预编译 JSX 为React.createElement(...)
接下来我逐步来实现简易的React核心 API,我从原生DOM、函数组件、类组件3种类型来剖析。
先看看测试示例源码 index.js:
大家知道,JSX 最终转换成普通的
JavaScipt对象,我在这直接声明一个 JSX 变量里面包括元素,另外增加了自定义组件,这里创建是函数组件并接受props参数,这为什么传props?带着这个疑问往下看。 组件写就得让其在页面渲染出来,应该导入我们熟悉的react-dom包,这包提供个render方法,该方法最常见接收2个参数,一个是我要渲染的 JSX,另个就是挂的根容器。
import ReactDOM from 'react-dom';
// function Component
function Comp(props){
return <h2>hi {props.name}</h2>
}
const jsx = (
<div id="demo" style={{color:"red",border:'1px solid blue'}}>
<span>hi</span>
<Comp name="函数组件"></Comp>
</div>
);
console.log(jsx); // 输出看下具体结构
ReactDOM.render(jsx,document.querySelector('#root'))
以上代码测试肯定是编译不成功的,结果报了一坨错误:
错误提示说根本没有声明
React,因为我这根本没导入React。
为什么要导入React? 原因是 JSX 在webpack进行打包的时候都会用过babel-loader转换成React.createElement的形式。如下图:
// function Component
function Comp(props) {
return React.createElement(
"h2",
null,
"hi ",
props.name
);
}
const jsx = React.createElement(
"div",
{ id: "demo", style: { color: "red", border: '1px solid blue' } },
React.createElement(
"span",
null,
"hi"
),
React.createElement(Comp, { name: "函数组件" })
);
运行起来,我们看下 JSX 输出是怎么样一个数据结构:
从数据结构上看就是个普通的Object,这里有很多属性值得注意,比如:
key好比是渲染列表的时候传的唯一值,主要作用是为了提高在diff过程的效率;props属性里既包含当前元素属性又包含子元素(children),仔细观察children元素数据结构与其父元素是一样的;ref用于引用DOM;type就更有用了,可以直接标明当前标签的类型是什么;
从上可看出,其实VDOM就是用来描述咱们DOM结构的JavaScript对象,为什么需要这个虚拟 DOM 后面会详情说明。
实践
了解了JSX,React.createElement,接下来我们首先来实现下React.createElement这个接口,新建一个react.js文件, index.js让用导入我们新建的React。
import React from 'react.js'; // 新建的react
import ReactDOM from 'react-dom';
// function Component
function Comp(props){
return <h2>hi {props.name}</h2>
}
const jsx = (
<div id="demo" style={{color:"red",border:'1px solid blue'}}>
<span>hi</span>
<Comp name="函数组件"></Comp>
</div>
);
console.log(jsx); // 输出看下具体结构
ReactDOM.render(jsx,document.querySelector('#root'))
如果不知道createElement传说明参数,我们可以输出下auguments了解下
从输出参数结果看,肯定有一个
type参数来表示标签类型的,参数2表示元素属性的,参数3则是表示若干个子元素,另外我们通过上面 JSX 输出结果得知props属性下有个children属性,children不是独立的,而是要把收集到一个数组里去,然后单独放props里。
通过标签类型的处理增加
vtype类型属性,用于在vdom转 DOM 时区分组件类型
react.js 代码如下:
/**
*
* createElement
* @param {any} type 标签类型 或者组件类型,如div
* @param {Element Attribute} props 标签属性
* @param {something child Element} children 若干数量不等的子元素
*/
function createElement(type, props, ...children) {
console.log('createElement', arguments);
props.children = children;
return { type, props};
}
export default { createElement };
运行会以下错误:
为什么报错,因为本身
React本身非常健壮,接收参数的时候会进行检查,我们这里支持返回了{ type, props }两参数,React认为是不足的,比如之前我们看到ref、key 等;所有我们这里只能自己创建个react-dom.js,用自己创建的render方法来实现渲染。
react-dom.js代码如下:
/**
* render渲染函数
*
* @param {Object} vnode 就是createElement创建的虚拟DOM
* @param {Element} container 挂载容器
*/
function render(vnode, container) {
container.innerHTML = `<pre>${JSON.stringify(vnode, null, 2)}</pre>`;
}
export default { render };
页面输出结果
紧接着考虑,有没有办法能把
render接收到的vnode转化成真正的DOMNode,这里逻辑有点多,所有我新建了一个vdom.js文件来处理。
vdom.js 代码如下:
/**
* createVNode 创建虚拟节点,对createElement返回的vdom做些加工处理
* @export
* @param {Number} vtype 元素的类型,1:原生元素,2:function组件,3:class组件
* @param {Object} type 标签元素类型
* @param {Object} props 标签属性
*/
export function createVNode(vtype, type, props) {
const vnode = { vtype, type, props };
// console.log('vnode', vnode);
return vnode;
}
这里看参数多加了一个vtype,为何要这么做呢?我这考虑到原生HTML原生,另外考虑自定义的function组件和Class类型组件,这里用vtype参数来判断,看楼上新建的react.js中的返回并没有vtype,所以这里需要通过type 特殊处理下:
function createElement(type, props, ...children) {
props.children = children;
delete props.__source; // 移除无用的属性
delete props.__self;
// type: 标签类型,如div
// vtype :组件类型
let vtype;
if (typeof type === 'string') {
// 原生标签
vtype = 1;
} else if (typeof type === 'function') {
if (type.isClassComponent) {
// 类组件
vtype = 2;
} else {
// 函数组件
vtype = 3;
}
}
return createVNode(vtype, type, props); // vdom.js
}
// 用来实现class组件的
export class Component {
//用于区分组件是class还是function, 因为typeof对类和函数都返回funciton而无法区分类和函数
static isClassComponent = true;
constructor(props) {
this.props = props;
this.state = {};
}
// 可以放心大胆的用setState
setState() {}
}
接下来要做件重要的事,就是把VDOM转换成真实DOM,我们在vdom.js 增加initVNode方法并导出。函数内部分别对文本节点、元素标签、函数组件、类组件分别做了处理。
并且对属性和特殊属性做了处理。代码如下:
/**
* vdom 转换为dom
* 初始化虚拟节点
* @export
* @param {Object} vnode
*/
export function initVNode(vnode) {
const { vtype } = vnode;
if (!vtype) {
// 文本节点
return document.createTextNode(vnode);
}
if (vtype === 1) {
// 原生标签
return createElement(vnode);
} else if (vtype === 2) {
// 类组件
return createClassComponent(vnode);
} else if (vtype === 3) {
// 函数组件
return createFunComponent(vnode);
}
}
/**
* 创建原生元素标签
* 函数组件和Class组件创建最终都会执行到 该原生....
* @param {Object} vnode
* @returns
*/
function createElement(vnode) {
// 根据type创建元素
const { type, props } = vnode;
const node = document.createElement(type);
// 处理属性, 原生自定义属性,特殊属性children
const { key, children, ...rest } = props;
Object.keys(rest).forEach((k) => {
// 处理JSX里特殊属性名: className, htmlFor
if (k === 'className') {
node.setAttribute('class', rest[k]);
} else if (k === 'htmlFor') {
node.setAttribute('for', rest[k]);
} else if (k === 'style' && typeof rest[k] === 'object') {
// 内联 style用js写法的处理 ,这里就比较多了,这里就些了正常情况,如果font-size这样就不行
const style = Object.keys(rest[k])
.map((s) => `${s}:${rest[k][s]}`)
.join(';');
node.setAttribute('style', style);
} else {
node.setAttribute(k, rest[k]);
}
});
// 递归子元素,// children父节点=> node
children.forEach((c) => {
// console.log('children',c)
node.appendChild(initVNode(c));
});
return node;
}
/**
* 创建Class组件
*
* @param {Object} vnode
* @returns
*/
function createClassComponent(vnode) {
//根据类组件看, type是class 组件声明
const { type, props } = vnode;
const component = new type(props);
const vdom = component.render();
return initVNode(vdom);
}
/**
* 创建函数组件
*
* @param {Object} vnode
* @returns
*/
function createFunComponent(vnode) {
// type是函数
const { type, props } = vnode;
const vdom = type(props);
return initVNode(vdom);
}
这里特别要注意下处理属性的时候有些特别的属性名,比如:对于 JSX 里
class和for是保留字所以用className、htmlFor等;另外测试了style内联样式的简单处理。
JSX关于属性props:
class属性需要写成className,for属性需要写成htmlFor,这是因为class和for是JavaScript的保留字。- 直接在标签上使用
style属性时,要写成style={{}}是两个大括号,外层大括号是告知jsx这里是js语法,和真DOM不同的是,属性值不能是字符串而必须为对象,需要注意的是属性名同样需要驼峰命名法。即margin-top要写成marginTop。 this.props下不要用children作为对象的属性名。因为this.props.children获取的该标签下的所有子标签。this.props.children的值有三种可能:- 如果当前组件没有子节点,它就是
undefined; - 如果有一个子节点,数据类型是
object; - 如果有多个子节点,数据类型就是
array。
- 如果当前组件没有子节点,它就是
所以,处理this.props.children的时候要小心。官方建议使用React.Children.map来遍历子节点,而不用担心数据类型引发的错误。
// class comp
class Comp2 extends Component {
render() {
return (
<div>
<h2>hi {this.props.name}</h2>
</div>
);
}
}
// 测试 处理数组
const users = [
{ name: 'hank', age: 30 },
{ name: 'nimo', age: 7 },
];
// vdom
const jsx = (
<div id="demo" style={{ color: 'red', border: '1px solid blue' }}>
<span>hi</span>
<Comp name="函数组件"></Comp>
<Comp2 name="类组件"></Comp2>
<ul>
{users.map((user) => (
<li key={user.name}>{user.name}</li>
))}
</ul>
</div>
);
运行结果:
为何li没有正常显示?,这种情况上面的createElement 里处理children 只针对单一虚拟 DOM 的处理,没考虑是多值的数组情况。下面是处理后的
// 递归子元素,// children父节点=> node
children.forEach(c => {
console.log('children',c)
// 如果子元素是个数组,改怎么处理 => 处理循环的
if(Array.isArray(c)) {
c.map(el => {
node.appendChild(initVNode(el))
})
} else{
node.appendChild(initVNode(c))
}
})
测试结果 OK
一个简单的粗略的实现,希望对你在学习React过程中有一定帮助,如任何问题和建议欢迎留言....