在我们的前端项目里经常会用到级联的select,比如省市区这样。通常这种级联大多是动态的。比如先加载了省,点击省加载市,点击市加载区。然后数据通常ajax返回。如果没有数据则说明到了叶子节点。
针对这种场景,如果我们使用jquery来实现,要考虑很多的问题,数据部分,以及大量的dom操作。比如这个页面上显示了某个区,这时候我切换省,要把市重新初始化数据,然后区域的部分要从页面中删除。这个判断是非常烦躁的。所以之前碰到这个问题时,就直接使用递归删除省下面的所有子,然后再重新生成市。所以jquery的思路是以dom为中心,所以的操作都在围绕着dom的变化来操作。也就是jquery的核心思想是dom可变。写jQuery写长了一个最真实的感受就是,仿佛天天都在写状态机,直到罗列了页面可能出现的所有情况。
这个问题在我学习React的时候思考很久,按照jQuery的思路我完全找不到北。因为React的style中很少有DOM的操作,而且很少直接去操作dom(几乎没有)。那怎么才能实现上面的需求呢?经过苦苦的思考React的函数式的哲学,终于想明白了。
React强烈支持组件式开发,将页面的区域抽象为组件。如,最简单的一个组件
var React = require(‘react’);
var HelloComponent = React.createClass({
render: function() {
return <div>Hello {this.props.name}<div>;
}
});
React.renderComponent(<HelloComponent name=“react!"/>, document.body);
这样我们根据我们的业务需要建立一个组件HelloCompoent,然后通过属性参数实现动态的展现效果。然后将这个组件挂载在document.body之上。
函数式-不可变。
函数式的风格的前端,首先一开始就和jQuery的思路迥异,函数式一上来就认为DOM不可变。那这里就让人很迷糊,纳尼?dom不可变?是的不可变。
DOM不可变意味着什么?意味着当你的状态或者属性发生变化时,DOM部分就该不加任何思索,re-render,是的,完全的重绘,不会像jQuery一样在你上一次的基础上重绘成新的状态,这个工作量太大了。极端的例子,上一次dom展示的是一个cat,现在的数据渲染的是一个dog,我*,这个重绘的工作量就大发了。所以re-render真是最自然最简单地办法。但是亲爱的聪明的你突然想明白了我在忽悠?每次重新re-render会带来页面大量的repaint-reflow。如果操作复杂会把页面性能拖到非常low的一个水平。哎。是的呢。那我们看看React的实现和哲学吧。
React的每个组件包含状态和属性,且状态和属性和dom隔离。dom的部分就是动态模板。
1. React认为页面dom不可变,所以当状态和属性发生变化时会re-render dom。
2. React的会根据新的状态和属性生成新的VirtualDOM Tree然后和旧的VirtualDOM Tree做对比(类似于版本控制的机制)。
3. 通过对比计算出最小的更新代价,然后将这些更新的方法进入队列。
4. 批量更新。
所以通过React实现了超高速的re-render,每秒可以60fps,这是变态的游戏级的水准。
且React强调single data flow,大大的减少了前端的复杂度。
且通过不断的组合组件实现更复杂的组件,这点很爽。
关于VirtualDOM再举个带状态栗子:计时器。
var React = require(‘react’);
var Timer = React.createClass({
getInitialState: function() {
return {secondElapsed: 0};
},
tick: function() {
this.setState({
secondElapsed: this.state.secondElapsed + 1
});
},
componentDidMount: function() {
this.interval = setInterval(this.tick, 1000);
},
componentWillUnmount: function() {
clearInterval(this.interval);
},
render: function() {
return <div>Time elapsed: {this.state.secondElapsed}</div>;
}
});
React.render(<Timer/>, document.body);
当secondElapsed变化时,React会计算出最小更新代价。
好了,绕了这么大一圈,还没有回到故事的原点。那怎么玩出React的Style的感觉的级联Select呢?这个时候我们考虑问题不在以我们的DOM为中心了,我们应该以我们的领域为中心,就是被Angular发扬光大的MDV(Model-Driven-View),模型驱动视图。
好的,省市区是我们的模型
我们的领域的描述:
items: [
//省
{label: ‘省’, data: [{id: 1, name: ‘安徽’}, {id: 2, name: '江苏'}]},
//市
{label: ’市’, data: [{id: 11, name: '蚌埠'}]},
//区
{label: ‘区’, data: [{id: 111, name: '怀远'}]}
];
choose: [1, 11, 111];
So 明白了嘛?
比如items就是我们的省市区领域数据,
1. 第一次加载ajax得到省信息,然后push到items
这时候React发现数据的状态发生了变化,赶紧计算diff,然后re-render,省就被virtualdom渲染出来了。
2. 当点击某个省,ajax获取市信息,然后push到items,且记录选中省的id
React又发现了变化,根据计算diff,发现省的信息没有变嘛,so就重绘了一个市select出来。So 这就是React比较库的地方。
3. 点击区。。。。你懂了。
4. 这时候我点击省了,如果没有选中任何省,只是选中"了请选择“, 我就将市和区的信息全clean掉,React又发现数据变化了,发现省的数据没有变化,不重新渲染,发生市和区数据没有了,re-render之后页面的市和区的select直接被clean掉。
通过上述步骤,我们清晰的看到原本我们是对DOM费事的操作转化为对我们领域的操作。DOM会被re-render,想一想都开心呢。
附上代码:
/**@jsx React.DOM*/
var React = window.React = require('react');
var provData = {
label: '省',
data: [
{id:1, name: '安徽'},
{id:2, name: '江苏'}
]
};
var cityData = [{
label: '市',
pid: 1,
data: [
{id: 10, name: '蚌埠'},
{id: 11, name: '巢湖'},
{id: 12, name: '太湖'}
]
}];
var CascadeSelect = React.createClass({
getInitialState: function() {
return {
items: [],
choose: []
}
},
componentWillMount: function() {
//AJAX call
this.setState({
items: this.state.items.concat([provData])
});
},
_handleChange: function(e) {
e.preventDefault();
var value = e.target.value.split("\:");
var selectIndex = value[0];
var selectValue = value[1];
var newData;
var index = selectIndex;
var nextItems = this.state.items;
var nextChoose = this.state.choose;
if (selectValue != '') {
index++;
nextChoose[selectIndex] = selectValue;
//ajax call
newData = cityData.filter(function(item) {
return item.pid == selectValue;
});
}
nextChoose = nextChoose.slice(0, index);
nextItems = nextItems.slice(0, selectIndex+1);
if (newData!=null && newData.length > 0) {
nextItems.push(newData[0]);
}
this.setState({
items: nextItems,
choose: nextChoose
});
},
_handleClick: function(e) {
e.preventDefault();
alert(this.state.choose.join("=>"));
},
render: function() {
return (
<div>
{this.state.items.map(function(item, i) {
return (
<div key={i}>
<label>{item.label}</label>
<select data-order={i} onChange={this._handleChange}>
<option value={i + ":"}> ==请选择== </option>
{item.data.map(function(data) {
return (<option key={data.id} value={i + ":" + data.id}>{data.name}</option>);
})}
</select>
</div>
);
}, this)}
<button onClick={this._handleClick}>查看选择项</button>
</div>
);
}
});
React.renderComponent(<CascadeSelect/>, document.body);
时间紧,任务重,代码就先草率的写到这了。通过代码和我们预期的一样,大部分的操作在model而不是dom。
我们再进一步抽象数据源,设置url属性,这样下次你在任何的地方想使用这种级联的select都只需要简单地
<CascadeSelect firstLevelUrl=“/provData” secondLevelUrl=‘' />
最后,不是因为我们用了React或者Angular就比jQuery高大上,从来不是这样的。我们的目标不是高大上,而是更好的解决我们的问题。提供更好的服务。我们要学习的是隐藏在React或者Angular背后的设计思想,对问题的抽象架构的方法。
最后和兄弟们共勉:
Dijkstra曰:编程的艺术就是处理复杂性的艺术。设法把代码写的精简从来就不是什么境界。代码架构内部的秩序能有效适应需求变化和化解业务的复杂度易于扩展和维护才是要追求的境界。
React把重点放在可预测性和数据单向同步上,它对控制复杂度很有效。