react.js概念
react.js框架是facebook推出的UI框架,主要解决视图层展示中的一些痛点问题:
- 虚拟dom:避免频繁的访问dom,修改dom带来的性能问题,在加上diff算法实现不重复渲染UI组件;
- 组件化:对于公司组件库的积累和沉淀,有利于资源复用,提高效率节省成本;
- 单向数据流:react提出单向数据流,数据的流向有规律可循;
更想说的是通过对react技术栈的使用,颠覆了许多前端开发的习惯。比以前更少的接触dom,控制dom;转而更多的关心业务逻辑,数据的流动和处理,更像是在写服务端代码。
虚拟dom
在React中,render执行的结果得到的并不是真正的DOM节点,结果仅仅是轻量级的JavaScript对象,我们称之为virtual DOM。
react具有batching(批处理)和高效的Diff算法,这使我们无需估计性能问题,毫无估计的刷新页面;虚拟dom的diff算法会告诉系统哪些dom需要操作,这一过程不需要认为干预,但是进一步了解虚拟dom的实现原理对于开发和优化都是有好处的。
比较innerHTML 和Virtual DOM 的重绘过程如下:
innerHTML: render html string + 重新创建所有 DOM 元素
Virtual DOM: render Virtual DOM + diff + 必要的 DOM 更新
Diff算法
上边也提到了diff算法,在react中不同状态对应不同的页界面,对俩个界面的不同通过对dom树的进行diff算法分析。
Facebook工程师结合Web界面的特点做出了两个简单的假设,使得Diff算法复杂度直接降低到O(n):
- 两个相同组件产生类似的DOM结构,不同的组件产生不同的DOM结构;
- 对于同一层次的一组子节点,它们可以通过唯一的id进行区分。
逐层进行节点比较
提到树,相信大多数同学立刻想到的是二叉树,遍历,最短路径等复杂的数据结构算法。而在React中,树的算法其实非常简单,那就是两棵树只会对同一层次的节点进行比较, 也就是进行逐层比较。如下图所示:
React只会对相同颜色方框内的DOM节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个DOM树的比较。
例如,考虑有下面的DOM结构转换:
A节点被整个移动到D节点下,直观的考虑DOM Diff操作应该是
A.parent.remove(A);
D.append(A);
但因为React只会简单的考虑同层节点的位置变换,对于不同层的节点,只有简单的创建和删除。当根节点发现子节点中A不见了,就会直接销毁A;而当D发现自己多了一个子节点A,则会创建一个新的A作为子节点。因此对于这种结构的转变的实际操作是:
A.destroy();
A = new A();
A.append(new B());
A.append(new C());
D.append(A);
可以看到,以A为根节点的树被整个重新创建。
不同节点类型的比较
在React中即比较两个虚拟DOM节点,当两个节点不同时,应该如何处理。这分为两种情况:
(1)节点类型不同 :
React直接删除前面的节点,然后创建并插入新的节点。假设我们在树的同一位置前后两次输出不同类型的节点。
renderA: <div />
renderB: <span />
=> [removeNode <div />], [insertNode <span />]
当一个节点从div变成span时,简单的直接删除div节点,并插入一个新的span节点。这符合我们对真实DOM操作的理解。
如果该删除的节点之下有子节点,那么这些子节点也会被完全删除,它们也不会用于后面的比较。这也是算法复杂能够降低到O(n)的原因。
以此类推,不同类型的组件也是相同的比较逻辑:
renderA: <Header />
renderB: <Content />
=> [removeNode <Header />], [insertNode <Content />]
(2)节点类型相同,但是属性不同。
React会对属性进行重设从而实现节点的转换。例如:
renderA: <div id="before" />
renderB: <div id="after" />
=> [replaceAttribute id "after"]
虚拟DOM的style属性稍有不同,其值为一个对象,因此转换过程如下:
renderA: <div style={{color: 'red'}} />
renderB: <div style={{fontWeight: 'bold'}} />
=> [removeStyle color], [addStyle font-weight 'bold']
由DOM Diff算法理解组件的生命周期
React组件的生命周期,其中的每个阶段其实都是和DOM Diff算法息息相关的。例如以下几个方法:
- constructor: 构造函数,组件被创建时执行;
- componentDidMount: 当组件添加到DOM树之后执行;
- componentWillUnmount: 当组件从DOM树中移除之后执行,在React中可以认为组件被销毁;
- componentDidUpdate: 当组件更新时执行。
上边图示的执行采用文字事例描述如下:
C will unmount.
C is created.
B is updated.
A is updated.
C did mount.
D is updated.
R is updated.
可以看到,C节点是完全重建后再添加到D节点之下,而不是将其“移动”过去。
列表节点的比较
那么当它们在同一层时,又是如何处理的呢?这就涉及到列表节点的Diff算法。相信很多使用React的同学大多遇到过这样的警告:
这是React在遇到列表时却又找不到key时提示的警告。虽然无视这条警告大部分界面也会正确工作,但这通常意味
列表节点的操作通常包括添加、删除和排序。例如下图,我们需要往B和C直接插入节点F,在jQuery中我们可能会直接使用$(B).after(F)来实现。而在React中,我们只会告诉React新的界面应该是A-B-F-C-D-E,由Diff算法完成更新界面。
这时如果每个节点都没有唯一的标识,React无法识别每一个节点,那么更新过程会很低效,即,将C更新成F,D更新成C,E更新成D,最后再插入一个E节点。效果如下图所示:
可以看到,React会逐个对节点进行更新,转换到目标节点。而最后插入新的节点E,涉及到的DOM操作非常多。而如果给每个节点唯一的标识(key),那么React能够找到正确的位置去插入新的节点,入下图所示:
对于列表节点顺序的调整其实也类似于插入或删除,我们将树的形态从shape5转换到shape6:
即将同一层的节点位置进行调整。如果未提供key,那么React认为B和C之后的对应位置组件类型不同,因此完全删除后重建,控制台输出如下:
B will unmount.
C will unmount.
C is created.
B is created.
C did mount.
B did mount.
A is updated.
R is updated.
而如果提供了key,如下面的代码:
shape5: function() { return ( <Root> <A> <B key="B" /> <C key="C" /> </A> </Root> );},
shape6: function() { return ( <Root> <A> <C key="C" /> <B key="B" /> </A> </Root> );},
那么控制台输出如下:
C is updated.
B is updated.
A is updated.
R is updated.
可以看到,对于列表节点提供唯一的key属性可以帮助React定位到正确的节点进行比较,从而大幅减少DOM操作次数,提高了性能。
jsx语法
react的虚拟dom是最大的亮点,可以再内存中生成dom树,通过创建虚拟的dom树来减少对真实dom的操作,实现性能的提升。不论是真实dom还是虚拟dom都是用javascript来创建的。
jsx的诞生就是为了实现虚拟dom的创建,采用类似html的语法也是为了人们更容易接受和理解。可以想象当你编写一段jsx代码,并且render到浏览器,中间要经过转换为javascript对象,再转换为真实dom的多个过程,当然这些转换对我们是透明的。
下边介绍一下jsx的一些具体特点:
jsx看起来像xml/html的javascript语法扩展,对于jsx的编写本人认为完全可以按照html的使用方式来写,只是要注意jsx的特别之处,也就是重点掌握这些特点,很快就能完全了解jsx的全貌。
标签类型
html类型标签:以小写字母打头的闭合标签;如:<div className="foo" />
react组件标签:以大写字母打头的闭合标签;如:<MyComponent someProperty={true} />-
标签属性
属性定义://有属性值 <div myProp="value" /> //无属性值 <div nav /> 等价于 <div nav={true} />
属性冲突:
一些标识符像 class 和 for 不建议作为 XML 属性名。作为替代,React DOM 使用 className 和 htmlFor 来做对应的属性。
属性表达式:
使用 JavaScript 表达式作为属性值,只需把这个表达式用一对大括号 ({}) 包起来,不要用引号 ("")。var person = <Person name={window.isLoggedIn ? window.name : ''} />;
Boolean 属性:
省略属性的值,jsx会认为属性的值为true,其他情况的值必须用表达式方式,再html中有些表单元素,含有属性如disabled, required, checked 和 readOnly// 在JSX中,对于禁用按钮这二者是相同的。 <input type="button" disabled />; <input type="button" disabled={true} />; // 在JSX中,对于不禁用按钮这二者是相同的。 <input type="button" />; <input type="button" disabled={false} />;
自定义属性:
html本身不存在的属性,react会自动过滤掉,只用data- 前缀的属性保留下来。
如:<div data-custom-attribute="foo" />style属性:
//形式一 <div style={{color: '#ff0000', fontSize: '14px'}}>Hello World.</div> //形式二 var style = { color: '#ff0000', fontSize: '14px' }; var node = <div style={style}>HelloWorld.</div>;
样式的属性名写法采用驼峰式,例如“background-color”变为“backgroundColor”, “font-size”变为“fontSize”,这和标准的JavaScript操作DOM样式的API是一致的。
使用事件:
//组件内部事件的调用方式,建议绑定bind()的工作放在构造函数里边,提升效率 <button onClick={this.checkAndSubmit.bind(this)}>Submit</button> //再redux中定义的action事件的绑定案例,后期会说明 <input type='text' className='product_num' onChange={this.props.actions.handleChange.bind(this,item.id)} maxLength='5' value={item.num} />
上边代码就是jsx绑定事件的形式,比较直观的展现事件和节点的关系。
绑定原理:
react不会真正的绑定事件到每个具体元素上,而采用事件代理的模式,再根节点document上为每种事件添加唯一的listener,然后通过事件的target找到真实的出发元素;这样从出发元素到顶层节点之间的所有节点如果有绑定这个事件,react都会触发对应的事件处理函数,这就是react模拟事件系统。基于这种系统,用户不用关心什么时机去移除事件绑定,再真实dom节点移除时会自动解除对应事件的绑定。
子节点表达式:
// 输入 (JSX):
var content = <Container>{window.isLoggedIn ? <Nav /> : <Login />}</Container>;
- 注释:
注释方式和javascript类似,只是需要注意的,再注释子节点块的时候,需要使用{} 包含注释部分
<Nav>
{ /* child comment, 用 {} 包围 */}
<Person
/* 多行注释 */
name={window.isLoggedIn ? window.name : ''} // 行尾注释
/>
</Nav>
- html实体
// 错误: 会显示 “First · Second”
<div>{'First · Second'}</div>
//一下几种形式都可解决上边的错误:
//直接用 Unicode 字符。这时要确保文件是 UTF-8 编码且网页也指定为 UTF-8 编码
<div>{'First · Second'}</div>
//安全的做法是先找到 实体的 Unicode 编号,然后在 JavaScript 字符串里使用
<div>{'First \u00b7 Second'}</div>
<div>{'First ' + String.fromCharCode(183) + ' Second'}</div>
//在数组里混合使用字符串和 JSX 元素
<div>{['First ', <span>·</span>, ' Second']}</div>
//直接插入原始HTML
<div dangerouslySetInnerHTML={{'{{'}}__html: 'First · Second'}} />
组件
React将用户界面看做简单的状态机器。当组件处于某个状态时,那么就输出这个状态对应的界面。通过这种方式,就很容易去保证界面的一致性。
在React中,你简单的去更新某个组件的状态,然后输出基于新状态的整个界面。React负责以最高效的方式去比较两个界面并更新DOM树。
这种组件模型简化了我们思考的方式:对组件的管理就是对状态的管理。
组件就像是一个函数,唯一交互窗口props是参数,state更像内部变量,render方法对应return返回客户想要的内容。
组件形式
考虑到实用性和未来的趋势,下边列出组件的俩种写法和他们的用途:
- class方式:class是es6的新特性,可以想写类一样写组件,里边可以有内部state,周期函数等
- function方式:对于那种使用一个render()方法的组件,可以用function的方式来简化代码。
组件状态-state
除了props之外,组件还有一个很重要的概念:state。组件规范中定义了setState方法,每次调用时都会更新组件的状态,触发render方法。需要注意,render方法是被异步调用的,这可以保证同步的多个setState方法只会触发一次render,有利于提高性能。和props不同,state是组件的内部状态,除了初始化时可能由props来决定,之后就完全由组件自身去维护。
组件属性-props
当给予的参数一定时,那么输出也是一定的,而React组件通过唯一的props接口避免了逻辑复杂性,让开发测试都更加容易。
React强烈不推荐去修改自身的props,因为这会破坏UI和Model的一致性,props只能够由使用者来决定。
context
生命周期
componentDidMount: 在组件第一次render之后调用,这时组件对应的DOM节点已被加入到浏览器。在这个方法里可以去实现一些初始化逻辑。
componentWillUnmount: 在DOM节点移除之后被调用,这里可以做一些相关的清理工作。
shouldComponentUpdate: 这是一个和性能非常相关的方法,在每一次render方法之前被调用。它提供了一个机会让你决定是否要对组件进行实际的render。例如:
shouldComponentUpdate(nextProps, nextState) {
return nextProps.id !== this.props.id;
}
当此函数返回false时,组件就不会调用render方法从而避免了虚拟DOM的创建和内存中的Diff比较,从而有助于提高性能。当返回true时,则会进行正常的render的逻辑。
组件是React的核心,虽然功能很强大,但是其API和概念却十分简单,以至于你只要实现一个render方法就可以创建一个组件。这大大降低了React学习门槛。