原文地址:http://purplebamboo.github.io/2015/09/15/reactjs_source_analyze_part_one/
reactjs是目前比较火的前端框架,但是目前并没有很好的解释原理的项目。reactjs源码比较复杂不适合初学者去学习。所以本文通过实现一套简易版的reactjs,使得理解原理更加容易。包括:
- reactjs源码分析-上篇(首次渲染实现原理)
- reactjs源码分析-下篇(更新机制实现原理)
声明:
- 本文假定你已经对reactjs有了一定的了解,如果没有至少看下ruanyifeng老师的入门demo。
- jsx不在本文的讨论范围,所有的例子原理都是使用原生的javascript。
- 篇幅限制,服务器端的reactjs也不在本文讨论范围内。
- 为了演示方便,本文以jQuery作为基本工具库。
- 为了更清晰的演示原理,本文会忽略很多细节的东西,千万不要用于生产环境。
所有实例源码都托管在github。点这里里面有分步骤的例子,可以一边看一边运行例子。
前言
前端的发展特别快,经历过jQuery一统天下的工具库时代后,现在各种框架又开始百家争鸣了。angular,ember,backbone,vue,avalon,ploymer还有reactjs,作为一个前端真是稍不留神就感觉要被淘汰了,就在去年大家还都是angularjs的粉丝,到了今年又开始各种狂追reactjs了。前端都是喜新厌旧的,不知道最后这些框架由谁来一统天下,用句很俗的话说,这是最好的时代也是最坏的时代。作为一个前端,只能多学点,尽量多的了解他们的原理。
reactjs的代码非常绕,对于没有后台开发经验的前端来说看起来会比较吃力。其实reactjs的核心内容并不多,主要是下面这些:
- 虚拟dom对象(Virtual DOM)
- 虚拟dom差异化算法(diff algorithm)
- 单向数据流渲染(Data Flow)
- 组件生命周期
- 事件处理
下面我们将一点点的来实现一个简易版的reactjs,实现上面的那些功能,最后用这个reactjs做一个todolist的小应用,看完这个,或者跟着敲一遍代码。希望让大家能够更好的理解reactjs的运行原理。
先从最简单的开始
我们先从渲染hello world开始吧。
我们看下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<script type="text/javascript">
React.render('hello world',document.getElementById("container"))
</script>
/**
对应的html为
<div id="container"></div>
生成后的html为:
<div id="container">
<span data-reactid="0">hello world</span>
</div>
*/
|
假定这一行代码,就可以把hello world 渲染到对应的div里面。
我们来看看我们需要为此做些什么:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
function ReactDOMTextComponent(text) {
this._currentElement = '' + text;
this._rootNodeID = null;
}
ReactDOMTextComponent.prototype.mountComponent = function(rootID) {
this._rootNodeID = rootID;
return '<span data-reactid="' + rootID + '">' + this._currentElement + '</span>';
}
function instantiateReactComponent(node){
if(typeof node === 'string' || typeof node === 'number'){
return new ReactDOMTextComponent(node)
}
}
React = {
nextReactRootIndex:0,render:function(element,container){
var componentInstance = instantiateReactComponent(element);
var markup = componentInstance.mountComponent(React.nextReactRootIndex++);
$(container).html(markup);
$(document).trigger('mountReady'); }
}
|
代码分为三个部分:
- React.render 作为入口负责调用渲染
- 我们引入了component类的概念,ReactDOMTextComponent是一个component类定义,定义对于这种
文本类型 的节点,在渲染,更新,删除时应该做什么操作,这边暂时只用到渲染,另外两个可以先忽略
- instantiateReactComponent用来根据element的类型(现在只有一种string类型),返回一个component的实例。其实就是个类工厂。
nextReactRootIndex作为每个component的标识id,不断加1,确保唯一性。这样我们以后可以通过这个标识找到这个元素。
可以看到我们把逻辑分为几个部分,主要的渲染逻辑放在了具体的componet类去定义。React.render负责调度整个流程,这里是调用instantiateReactComponent生成一个对应component类型的实例对象,然后调用此对象的mountComponent获取生成的内容。最后写到对应的container节点中。
可能有人问,这么p大点功能,有必要这么复杂嘛,别急。往下看才能体会这种分层的好处。
引入基本elemetnt
我们知道reactjs最大的卖点就是它的虚拟dom概念,我们一般使用React.createElement 来创建一个虚拟dom元素。
虚拟dom元素分为两种,一种是浏览器自带的基本元素比如 div p input form 这种,一种是自定义的元素。
这边需要说一下我们上节提到的文本节点,它不算虚拟dom,但是reacjs为了保持渲染的一致性。文本节点是在外面包了一层span标记,也给它配了个简化版component(ReactDOMTextComponent)。
这节我们先讨论浏览器的基本元素。
在reactjs里,当我们希望在hello world外面包一层div,并且带上一些属性,甚至事件时我们可以这么写:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
function hello(){
alert('hello')
}
var element = React.createElement('div',{id:'test',onclick:hello},'click me')
React.render(element,document.getElementById("container"))
|
上面使用React.createElement 创建了一个基本元素,我们来看看简易版本React.createElement 的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
function ReactElement(type,key,props){
this.type = type;
this.key = key;
this.props = props;
}
React = {
nextReactRootIndex:0,createElement:function(type,config,children){
var props = {},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 = $.isArray(children) ? children : [children] ;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
return new ReactElement(type,props);
},container){
var componentInstance = instantiateReactComponent(element);
var markup = componentInstance.mountComponent(React.nextReactRootIndex++);
$(container).html(markup);
$(document).trigger('mountReady');
}
}
|
createElement只是做了简单的参数修正,最终返回一个ReactElement实例对象也就是我们说的虚拟元素的实例。
这里注意key的定义,主要是为了以后更新时优化效率,这边可以先不管忽略。
好了有了元素实例,我们得把他渲染出来,此时render接受的是一个ReactElement而不是文本,我们先改造下instantiateReactComponent:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
function instantiateReactComponent(node){
if(typeof node === 'string' || typeof node === 'number'){
return new ReactDOMTextComponent(node);
}
if(typeof node === 'object' && typeof node.type === 'string'){
return new ReactDOMComponent(node);
}
}
|
我们增加了一个判断,这样当render的不是文本而是浏览器的基本元素时。我们使用另外一种component来处理它渲染时应该返回的内容。这里就体现了工厂方法instantiateReactComponent的好处了,不管来了什么类型的node,都可以负责生产出一个负责渲染的component实例。这样render完全不需要做任何修改,只需要再做一种对应的component类型(这里是ReactDOMComponent)就行了。
所以重点我们来看看ReactDOMComponent 的具体实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
function ReactDOMComponent(element){
this._currentElement = element;
this._rootNodeID = null;
}
ReactDOMComponent.prototype.mountComponent = function(rootID){
this._rootNodeID = rootID;
var props = this._currentElement.props;
var tagOpen = '<' + this._currentElement.type;
var tagClose = '</' + this._currentElement.type + '>';
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="' + this._rootNodeID + '"]',eventType + '.' + this._rootNodeID,props[propKey]);
}
if (props[propKey] && propKey != 'children' && !/^on[A-Za-z]/.test(propKey)) {
tagOpen += ' ' + propKey + '=' + props[propKey];
}
}
var content = '';
var children = props.children || [];
var childrenInstances = [];
var that = this;
$.each(children,function(key,child) {
var childComponentInstance = instantiateReactComponent(child);
childComponentInstance._mountIndex = key;
childrenInstances.push(childComponentInstance);
var curRootId = that._rootNodeID + '.' + key;
var childMarkup = childComponentInstance.mountComponent(curRootId);
content += ' ' + childMarkup;
})
this._renderedChildren = childrenInstances;
return tagOpen + '>' + content + tagClose;
}
|
我们增加了虚拟dom reactElement的定义,增加了一个新的componet类ReactDOMComponent。 这样我们就实现了渲染浏览器基本元素的功能了。
对于虚拟dom的渲染逻辑,本质上还是个递归渲染的东西,reactElement会递归渲染自己的子节点。可以看到我们通过instantiateReactComponent屏蔽了子节点的差异,只需要使用不同的componet类,这样都能保证通过mountComponent最终拿到渲染后的内容。
另外这边的事件也要说下,可以在传递props的时候传入{onClick:function(){}}这样的参数,这样就会在当前元素上添加事件,代理到document。由于reactjs本身全是在写js,所以监听的函数的传递变得特别简单。
这里很多东西没有考虑,比如一些特殊的类型input select等等,再比如img不需要有对应的tagClose等。这里为了保持简单就不再扩展了。另外reactjs的事件处理其实很复杂,实现了一套标准的w3c事件。这里偷懒直接使用jQuery的事件代理到document上了。
自定义元素
上面实现了基本的元素内容,我们下面实现自定义元素的功能。
随着前端技术的发展浏览器的那些基本元素已经满足不了我们的需求了,如果你对webcomponents有一定的了解,就会知道人们一直在尝试扩展一些自己的标记。
reactjs通过虚拟dom做到了类似的功能,还记得我们上面element.type只是个简单的字符串,如果是个类呢?如果这个类恰好还有自己的生命周期管理,那扩展性就很高了。
如果对生命周期等概念不是很理解的,可以看看我以前的另一片文章:javascript组件化
我们看下reactjs怎么使用自定义元素:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
var HelloMessage = React.createClass({
getInitialState: function() {
return {type: 'say:'};
},componentWillMount: function() {
console.log('我就要开始渲染了。。。')
},componentDidMount: function() {
console.log('我已经渲染好了。。。')
},render: function() {
return React.createElement("div",null,this.state.type,"Hello ",this.props.name);
}
});
React.render(React.createElement(HelloMessage,{name: "John"}),document.getElementById("container"));
|
React.createElement 接受的不再是字符串,而是一个class。
React.createClass 生成一个自定义标记类,带有基本的生命周期:
- getInitialState 获取最初的属性值this.state
- componentWillmount 在组件准备渲染时调用
- componentDidMount 在组件渲染完成后调用
对reactjs稍微有点了解的应该都可以明白上面的用法。
我们先来看看React.createClass的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
var ReactClass = function(){
}
ReactClass.prototype.render = function(){}
React = {
nextReactRootIndex:0,createClass:function(spec){
var Constructor = function (props) {
this.props = props;
this.state = this.getInitialState ? this.getInitialState() : null;
}
Constructor.prototype = new ReactClass();
Constructor.prototype.constructor = Constructor;
$.extend(Constructor.prototype,spec);
return Constructor;
},children){
...
},container){
...
}
}
|
可以看到createClass生成了一个继承ReactClass的子类,在构造函数里调用this.getInitialState获得最初的state。
为了演示方便,我们这边的ReactClass相当简单,实际上原始的代码处理了很多东西,比如类的mixin的组合继承支持,比如componentDidMount等可以定义多次,需要合并调用等等,有兴趣的去翻源码吧,不是本文的主要目的,这里就不详细展开了。
我们这里只是返回了一个继承类的定义,那么具体的componentWillmount,这些生命周期函数在哪里调用呢。
看看我们上面的两种类型就知道,我们是时候为自定义元素也提供一个componet类了,在那个类里我们会实例化ReactClass,并且管理生命周期,还有父子组件依赖。
好,我们老规矩先改造instantiateReactComponent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
function instantiateReactComponent(node){
if(typeof node === 'string' || typeof node === 'number'){
return new ReactDOMTextComponent(node);
}
if(typeof node === 'object' && typeof node.type === 'string'){
return new ReactDOMComponent(node);
}
if(typeof node === 'object' && typeof node.type === 'function'){
return new ReactCompositeComponent(node);
}
}
|
很简单我们增加了一个判断,使用新的component类形来处理自定义的节点。我们看下 ReactCompositeComponent的具体实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
function ReactCompositeComponent(element){
this._currentElement = element;
this._rootNodeID = null;
this._instance = null;
}
ReactCompositeComponent.prototype.mountComponent = function(rootID){
this._rootNodeID = rootID;
var publicProps = this._currentElement.props;
var ReactClass = this._currentElement.type;
var inst = new ReactClass(publicProps);
this._instance = inst;
inst._reactInternalInstance = this;
if (inst.componentWillMount) {
inst.componentWillMount();
}
var renderedElement = this._instance.render();
var renderedComponentInstance = instantiateReactComponent(renderedElement);
this._renderedComponent = renderedComponentInstance;
var renderedMarkup = renderedComponentInstance.mountComponent(this._rootNodeID);
$(document).on('mountReady',function() {
inst.componentDidMount && inst.componentDidMount();
});
return renderedMarkup;
}
|
实现并不难,ReactClass的render一定是返回一个虚拟节点(包括element和text),这个时候我们使用instantiateReactComponent去得到实例,再使用mountComponent拿到结果作为当前自定义元素的结果。
应该说本身自定义元素不负责具体的内容,他更多的是负责生命周期。具体的内容是由它的render方法返回的虚拟节点来负责渲染的。
本质上也是递归的去渲染内容的过程。同时因为这种递归的特性,父组件的componentWillMount一定在某个子组件的componentWillMount之前调用,而父组件的componentDidMount肯定在子组件之后,因为监听mountReady事件,肯定是子组件先监听的。
需要注意的是自定义元素并不会处理我们createElement时传入的子节点,它只会处理自己render返回的节点作为自己的子节点。不过我们在render时可以使用this.props.children拿到那些传入的子节点,可以自己处理。其实有点类似webcomponents里面的shadow dom的作用。
上面实现了三种类型的元素,其实我们发现本质上没有太大的区别,都是有自己对应component类来处理自己的渲染过程。
大概的关系是下面这样。
于是我们发现初始化的渲染流程都已经完成了。
结语
整个初次渲染的流程基本就分析完毕了。看看我们目前的进展,事件监听做了,虚拟dom有了。基本的组件生命周期也有了。我们这个小玩具已经可以简单跑跑了。下篇文章我们将一起去实现reactjs的更新机制,看看它最核心的diff算法是怎么回事。 (编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|