为何使用react
jquery 修改一个功能时:
通过DOM上的各种属性,找到这个节点,然后直接修改其中的值,很难保证查找的DOM节点没有遗漏。需要修改多处的值,难以保证自己的修改完整、没有副作用。
DOM树的状态和js中数据的状态是分离的,修改DOM的目的就是维护DOM树和js状态的一致
项目复杂之后,维护成本高:代码运行的某一时刻中,虽然你目标的DOM状态是确定的,但当前DOM树的状态却可能是多种多样的(不确定的)。理论上要把DOM树改变成目标状态的话, 你的代码就需要考虑当前DOM树各种可能的状态。不仅如此,你的代码还需要包含改变DOM树状态 的一步步的操作。
React响应式的设计方式
UI=f(data)
可以通过改变应用中的数据来改变React应用的状态,React应用中的每一部分都使用props(外部传入)和states(本身)两个属性来储存状态。state属性产生于ReactElement的内部,可传递到子ReactElement中作为props。
PS:React与传统MVC客户端框架有着大大的不同,因为react框架只是利用javascript去合成复杂的用户界面,所有与用户之间的交互都在可声明的组件中,并没有直接可以看到其他框架中很常见的数据绑定。
简介Virtual DOM
上文已经提到React能够实现DOM树和真实数据状态的一致,这主要归功于其Virtual DOM的实现,使其不用使用类似MVVM的双向绑定这种方式也能保持View和Model状态的一致性。
virtual DOM的基础是virtual Node,一般是树状结构,是一个轻量级的js对象。
其用于创建真实节点的数据包括:元素类型、元素属性、元素的子节点。
首先更新virtual Dom树,对比前后两颗树的区别,将差异部分映射到实际DOM。
//得到一个virtual Node
var tree=h('div',[
h('span','hello'),
h('span','world')
])
//通过virtual-dom提供的createElement创建真实节点
var rootNode=createElement(tree);
//追加到页面
document.body.appendChild(rootNode);
更新
//状态变更时,重新构造一颗用js表示的DOM树
var new_tree=render(new_state);
//使用高效的diff算法,比较两颗树,记录差异
var patches=diff(tree,new_tree);
//将差异应用到真实DOM
rootNode=patch(rootNode,patches);
诺,其主要思路就如上了,如有兴趣,可以进一步欣赏其源码,网上简易版的实现也有很多。
PS:标准的diff算法(如版本控制工具Git的diff)时间复杂度为O(n^3),这种代价对于在前端运行来说来高了。react结合Web界面的特点做出了两个简单的假设,将其diff算法的时间复杂度降低到O(n)。
- 两个相同组件产生类似的DOM结构,不同的组件产生不同的DOM结构;
- 对于同一层次的一组子节点,它们可以通过唯一的id进行区分。
两个相同组件产生类似的DOM结构,不同的组件产生不同的DOM结构;
对于同一层次的一组子节点,它们可以通过唯一的id进行区分。
一切皆组件
不仅仅是良好的组件封装与使用的易用性,更重要的是除了要渲染出来的UI也可以当做组件。比如只是定时向服务端发送当前页面数据的请求,除此之外没有任何渲染逻辑。
import {Component} from 'react';
class postMessage extends Component{
constructor(props){
super(props)
}
componentDidMount(){
this.timer=setInterval(function(){
fetch('/api/fetchData')
},1000)
}
componentWillUnmount(){
clearInterval(this.timer)
}
render(){
return null;
}
}
export default postMessage;
生命周期
组件加载(初始化):
-
constructor()对state初始化工作
constructor(props){ super(props); this.state={} }
componentWillMount()
render方法之前,只执行一次render()
渲染组件挂载到网页-
componentDidMount()
组件加载完之后立即执行,组件已经生成DOM结构,并且已经被挂载到了网页上,可以保证数据的加载。【常在该方法中加载外部数据】在这儿执行setTimeout或者AJAX请求()。如果是异步获取数据,可以在componentWillUnmount中取消发送请求。
调用setState方法会重新渲染为何在这儿获取数据?
constructor中会妨碍组件的渲染;
componentWillMount中触发setState不会重新渲染;
组件更新
-
componentWillReceiveProps(nextProps)
在接收到新的props时才被执行
-
shouldComponentUpdate(nextProps,nextState)
【如果需要考虑性能,可以使用shouldComponentUpdate来提升速度】
是拯救性能的紧急出口
componentWillUpdate
render
-
componentDidUpdate
【在组件完成更新后被使用,比如清除notification文字等操作】
-
componentWillUnmount
【执行一些必要的清理任务,比如清除setTimeout或者在componentDidMount
中创建的DOM元素】
高阶组件
const HoC = (WrappedComponent) => {
const WrappingComponent = (props) => (
<div className="foo">
<WrappedCompoent {...props} />
</div>
);
return WrappingComponent;
};
高阶组件中可以传入任意多个想要传入的组件。
const HoC = (WrappedComponent, LoginView) => {
const WrappingComponent = () => {
const {user} = this.props;
if (user) {
return <WrappedComponent {...this.props} />
} else {
return <LoginView {...this.props} />
}
};
return WrappingComponent;
};
React中的组件通信
-
react 中提供的 props 与回调函数。
解决父子组件之间的通信。这是React官方提供的方式,父组件通过props向子组件传递数据,子组件向父组件可以使用此前通过props传递的函数以回调形式通信。
-
以事件的形式,比如观察者模式
可用来解决兄弟组件以及远亲组件之间的通信。其中兄弟组件,也可以通过借助共同的父组件按照父子组件的通信方式来通信。观察者模式对于血缘关系远的组件保持通信,并有效解耦。但是会使得数据流向不明朗,同时给debug带来困难。
-
基于redux
如果使用了Redux的话,就可以利用其只有一个全局状态树的特性来进行信息的传递。Redux内部也是使用了观察者模式,对组件间的解耦提供了很多帮助。
//创建一个store function reducer(state={},action){} let store=createStore(reducer); //在组件A中发布 store.dispatch({ type:'child_1', data:'hello' }) //在要通信的组件B中订阅 constructor(props){ super(props); state={ msg:"aaa" } } compoenentDidMount(){ store.subscribe(()=>{ let new_state=store.getState(); if(new_state.type==='child_1'){ this.setState({ msg:new_state.data }) } }) }
react应用性能
给应用性能带来影响的来源:
- 组件中不更新DOM的冗余操作
- DOM比对那些无需更新的叶子节点
意味着每次更新顶级的Props,整个应用的每一个组件都会渲染,导致性能问题,但是每个组件都有shouldComponentUpdate(),当组件需要更新时返回true,不必更新时返回false。
为了获得理想的更新,尽可能在shouldComponentUpdate中返回false
-
加速检查
不做深度检查,只要对象的值发生了改变,就改变对象的引用。在reducer中使用延伸操作符...也可以加快数据对比。
//在这个reducer中我们改变一个item 的descriptionexport default (state, action) { if (action.type === 'ITEM_DESCRIPTION_UPDATE') { const { itemId, description } = action const items = state.items.map(item => { //跟这个action无关的item,不做修改直接返回 if (item.id != itemId) { return item; } //跟这个action相关的item,但是只更改description的值 return {...item, description }; }); return {...state, items } } return state; }
在shouldComponentUpdate方法中只做引用检查
shouldComponentUpdate(nextProps) {
return isObjectEqual(this.props, nextProps); }
isObjectEqual实现的实例:
const isArrayEqual = (array1 = [], array2 = []) => {
if (array1 === array2) {
return true;
}
return array1.length === array2.length &&
array1.every((item, index) => item === array2[index]);
}
const isObjectEqual = (obj1, obj2) => {
if (!isObject(obj1) || !isObject(obj2)) {
return false;
}
//引用是否相同
if (obj1 === obj2) {
return true;
}
//包含的键名是否一致
const item1Keys = Object.keys(obj1).sort();
const item2Keys = Object.keys(obj2).sort();
if (!isArrayEqual(item1Keys, item2Keys)) {
return false;
}
//每个键包含的值是否一致
return item2Keys.every((key) => {
const value = obj1[key];
const nextValue = obj2[key];
if (value === nextValue) {
return true; }
//如果是数组,再检查一个层级的深度
return Array.isArray(value) &&
Array.isArray(nextValue) &&
isArrayEqual(value, nextValue);
}
})
}
-
简化检查
复杂的数据结构会导致众多条件的判断、嵌套层级深。
解决办法:数据扁平化
从而可以使用简单的引用检查
使用immutable.js
除了针对shouldComponentUpdate进行修改,使用不可变数据也是应用提升的方法。
js 中的对象一般是可变的,因为使用了引用赋值,新的对象简单的使用了原始对象,改变新的对象也将影响到原始对象,如:
foo={a:1};bar=foo;bar.a=2
则foo.a也成了2,虽然这样可以节约内存,但是应用复杂之后,带来隐患。一般的解决方法是使用浅拷贝和深拷贝(针对Object /Array复杂对象)避免被修改,但造成了CPU和内存的浪费。
- 浅拷贝:只复制一层对象的属性,而js存储对象都是存地址,所以浅拷贝的对象指向同一块内存地址,会同时修改。
- 深拷贝:递归复制所有层级到新对象。
//方法1:通过递归
function deepCopy(o, c) {
var c = c || {}
for (var i in o) {
if (typeof o[i] === 'object') {
if (o[i].constructor === Array) {
c[i] = []
} else {
c[i] = {}
}
deepCopy(c[i], o[i]);
} else {
c[i] = o[i]
}
return c;
}
}
//方法2:通过json解析
var result = JSON.parse(JSON.stringify(test))
Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。
同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
PropTypes
如果props是复杂类型,使用shape 和arrayOf()
{ text:'hello world', numbers:[1,2,3]}
检测内部对象:
propTypes: {
myObject: React.PropTypes.shape({
text: React.PropTypes.string,
numbers: React.PropTypes.arrayOf(React.PropTypes.number)
})
}
其它
计算条件和判断,交给render方法
能用三元运算符,不用if
动态处理classNames:使用工具classnames拼接
var classNames = reuqire('classnames');
render() {
var btnClass = classNames({
'btn': true,
'btn-pressed': this.state.isPressed
})
return { < span className = { btnClass } > button< /span> }}