- React 的几种组件以及首次渲染实现
- React 更新机制的实现以及 React diff 算法
React 的代码还是非常复杂的,虽然这里是一个简化版本。但是还是需要有不错的面向对象思维的。React 的核心主要有一下几点。
- 虚拟 dom 对象(Virtual DOM)
- 虚拟 dom 差异化算法(diff algorithm)
- 单向数据流
- 组件声明周期
- 事件处理
本文代码仓库
- 直接在游览器中打开 main.html 中查看效果
- 更改代码请先执行执行
npm i
安装依赖(使用了部分 es6 代码) - 修改代码后请执行
npm run dev
重新编译代码
实现一个 hello React!的渲染
看如下代码:
// js
React.render('hello React!',document.getElementById("root"))
// html
<div id="root"></div>
// 生成代码
<div id="root">
<span data-reactid="0">hello React!</span>
</div>
针对上面代码的具体实现
/** * component 类 * 文本类型 * @param {*} text 文本内容 */
function ReactDOMTextComponent(text) {
// 存下当前的字符串
this._currentElement = "" + text;
// 用来标识当前component
this._rootNodeID = null;
}
/** * component 类 装载方法,生成 dom 结构 * @param {number} rootID 元素id * @return {string} 返回dom */
ReactDOMTextComponent.prototype.mountComponent = function(rootID) {
this._rootNodeID = rootID;
return (
'<span data-reactid="' + rootID + '">' + this._currentElement + "</span>"
);
};
/** * 根据元素类型实例化一个具体的component * @param {*} node ReactElement * @return {*} 返回一个具体的component实例 */
function instantiateReactComponent(node) {
//文本节点的情况
if (typeof node === "string" || typeof node === "number") {
return new ReactDOMTextComponent(node);
}
}
const React = {
nextReactRootIndex: 0,
/** * 接收一个React元素,和一个dom节点 * @param {*} element React元素 * @param {*} container 负责装载的dom */
render: function(element, container) {
// 实例化组件
var componentInstance = instantiateReactComponent(element);
// 组件完成dom装载
var markup = componentInstance.mountComponent(React.nextReactRootIndex++);
// 将装载好的 dom 放入 container 中
$(container).html(markup);
$(document).trigger("mountReady");
}
};
这里代码分为三个部分:
- 1 React.render 作为入口接受一个 React 元素和游览器中的 dom 负责调用渲染,nextReactRootIndex 为每个 component 的唯一标识
- 2 引入 component 类的概念,ReactDOMTextComponent 是一个 component 类定义。ReactDOMTextComponent 针对于文本节点进行处理。并且在 ReactDOMTextComponent 的原型上实现了 mountComponent 方法,用于对组件的渲染,返回组件的 dom 结构。当然 component 还具有更新和删除操作,这里将在后续讲解。
- 3 instantiateReactComponent 用来根据 element 的类型(现在只有一种 string 类型),返回一个 component 的实例。其实就是个类工厂。
在这里我们把逻辑分为几个部分,渲染逻辑则由 component 内部定义,React.render 负责调度整个流程,在调用 instantiateReactComponent 生成一个对应 component 类型的实例对象,再调用对象的 mountComponent 返回 dom,最后再写到 container 节点中
相关参考视频讲解:进入学习
虚拟 dom
虚拟 dom 无疑是 React 的核心概念,在代码中我们会使用 React.createElement 来创建一个虚拟 dom 元素。
虚拟 dom 分为两种一种是游览器自带的基本元素比如 div,还有一种是自定义元素(文本节点不算虚拟 dom)
虚拟节点的使用方式
// 绑定事件监听方法
function sayHello(){
alert('hello!')
}
var element = React.createElement('div',{
id:'jason',onclick:hello},'click me')
React.render(element,document.getElementById("root"))
// 最终生成的html
<div data-reactid="0" id="jason">
<span data-reactid="0.0">click me</span>
</div>
我们使用 React.createElement 来创建一个虚拟 dom 元素,以下是简易实现
/**
* ReactElement 就是虚拟节点的概念
* @param {*} key 虚拟节点的唯一标识,后期可以进行优化
* @param {*} type 虚拟节点类型,type可能是字符串('div', 'span'),也可能是一个function,function时为一个自定义组件
* @param {*} props 虚拟节点的属性
*/
function ReactElement(type, key, props) {
this.type = type;
this.key = key;
this.props = props;
}
const React = {
nextReactRootIndex: 0,
/**
* @param {*} type 元素的 component 类型
* @param {*} config 元素配置
* @param {*} children 元素的子元素
*/
createElement: function(type, config, children) {
var props = {
};
var propName;
config = config || {
};
var key = config.key || null;
for (propName in config) {
if (config.hasOwnProperty(propName) && propName !== "key") {
props[propName] = config[propName];
}
}
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = Array.isArray(children) ? children : [children];
} else if (childrenLength > 1) {
var childArray = [];
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
return new ReactElement(type, key, props);
},
/**
* 自行添加上文中的render方法
*/
};
createElement 方法对传入的参数做了一些处理,最终会返回一个 ReactElement 虚拟元素实例,key 的定义可以提高更新时的效率
有了虚拟元素实例,我们需要改造一下 instantiateReactComponent 方法
/** * 根据元素类型实例化一个具体的component * @param {*} node ReactElement * @return {*} 返回一个具体的component实例 */
function instantiateReactComponent(node) {
//文本节点的情况
if (typeof node === "string" || typeof node === "number") {
return new ReactDOMTextComponent(node);
}
//浏览器默认节点的情况
if (typeof node === "object" && typeof node.type === "string") {
//注意这里,使用了一种新的component
return new ReactDOMComponent(node);
}
}
我们增加了一个判断,这样当 render 的不是文本而是浏览器的基本元素时。我们使用另外一种 component 来处理它渲染时应该返回的内容。这里就体现了工厂方法 instantiateReactComponent 的好处了,不管来了什么类型的 node,都可以负责生产出一个负责渲染的 component 实例。这样 render 完全不需要做任何修改,只需要再做一种对应的 component 类型(这里是 ReactDOMComponent)就行了。
ReactDOMComponent
的具体实现
/** * component 类 * react 基础标签类型,类似与html中的('div','span' 等) * @param {*} element 基础元素 */
function ReactDOMComponent(element) {
// 存下当前的element对象引用
this._currentElement = element;
this._rootNodeID = null;
}
/** * component 类 装载方法 * @param {*} rootID 元素id * @param {string} 返回dom */
ReactDOMComponent.prototype.mountComponent = function(rootID) {
this._rootNodeID = rootID;
var props = this._currentElement.props;
// 外层标签
var tagOpen = "<" + this._currentElement.type;
var tagClose = "</" + this._currentElement.type + ">";
// 加上reactid标识
tagOpen += " data-reactid=" + this._rootNodeID;
// 拼接标签属性
for (var propKey in props) {
// 属性为绑定事件
if (/^on[A-Za-z]/.test(propKey)) {
var eventType = propKey.replace("on", "");
// 对当前节点添加事件代理
$(document).delegate(
'[data-reactid=