对接触过 React 的朋友来说,jsx 的方式肯定不陌生。比如
<ListItem item-id="123" />
这个组件非常简单,就是名字叫 ListItem,然后传了一个 id 的值。为什么说这个呢?我们换个角度看这个组件。
把它当作一个函数来看
ListItem({itemId: 123})
我们可以看到,其实这个函数返回的结果只受这个 itemId 的影响,换成组件也是一样,组件内部的状态不会影响到父级组件,父级组件的状态除了这个参数,也无法影响组件。极大的方便了组件的稳定性,已经方便单元测试。
对了解过函数式编程的人,肯定对这个点非常了解,对,这就是纯函数。
纯函数非常方便,好处之前也讲了,来看看具体我们可以用这个做什么。
比如给一个数加一:
可能会这么写
var a = 1;
increment = funciton() {
return a + 1;
}
但是按照纯函数的思路去写
increment = function(a) {
return a + 1;
}
这样就不会存在如果外部变量 a 被修改而导致函数无法运行或者出错的情况。
函数式还有很多知识点,函数式只是其中一点,接下来聊聊语义化。
比如这段代码
[1,2,3,4,5].map(function(num) {
if(num % 2 != 0) return;
num *= 3;
num = 'num is ' + num;
return num;
})
这个需求是将数组中的偶数乘 3,然后按照一种格式输出。虽然代码简单,但是不知有没有觉得其实这样的写法并不容易看出来在干嘛,语义化并不好。我们换一种写法。
[1,2,3,4,5].filter(n => n % 2 == 0)
.map(n => n * 3)
.map(n => 'num is ' + n)
为了简洁,使用了 ES6 来描述这段伪代码,这样是不是更加语义化,能够一次读下来明白代码在干嘛。
再换个例子聊,在 酷壳 中有一篇谈函数式编程,其中举了个例子非常形象,不过原文中使用的是 python,我在这里用 js 来实现。
比如,我们有3辆车比赛,简单起见,我们分别给这3辆车有70%的概率可以往前走一步,一共有5次机会,我们打出每一次这3辆车的前行状态。
一般的思路可能是这样的(用伪代码)
time = 5
positions = [1,1,1]
do (time) ->
time -= 1
postions.each (pos, i) ->
if Math.random() > 0.7
positions[i] += 1
console.log '-' + pos
我们在这个基础上继续优化一下,把一些处理独立出来(伪代码)
time = 5
positions = [1,1,1]
move = ->
for pos, i in positions
if Math.random() > 0.3
positions[i] += 1
drawCar = (pos) ->
console.log '-' + pos
run = ->
time -= 1
move()
draw = ->
for pos in positions
drawCar pos
do (time) ->
run()
draw()
这段代码把一些操作都独立出来,不过有个问题是仍然依赖于外部的两个变量,我们在阅读这段代码的时候,需要在大脑中额外思考这两个变量在如何进行变化。
接下来我们要把这个外部变量砍掉,看看是怎么样的效果(伪代码)
move = (positions) -> positions.map (x) -> Math.random() * 0.3 ? x + 1 : x
drawCar = (pos) -> '-' + pos
run = (state) -> { time: state.time - 1, positions: move( state.positions )}
draw = (state) -> state.positions.map( drawCar )
race = (state) ->
draw(state)
if state.time then race(move(state))
race({time: 5, positions: [1,1,1]})
可以看到,所有函数不再使用一个公共变量,函数返回的结果受参数影响,可以把这些参数当作状态来看。这个 race 也可以作为对外接口随意传入初始值,所有的函数可以单独测试。
之前的文章简单聊过这方面,今天还会举一点这方面的例子。
拿自动填充来讲,比如用 jQuery 可能会这么写:
$input.on('input', function() {
if(stop()) {
if(length > 2) {
if(value != lastValue) {
search();
}
}
}
});
实际可能代码更加复杂,那用 Rxjs 来写会如何呢?
Observable
.from($input, 'input')
.map(function(e) {
return e.target.value;
})
.filter(function(text) {
return text.length > 2;
})
.debounce(750)
.distinctUntilChanged()
.flatMapLatest(searchPromise)
.subscribe(function(data) {
show(data);
})
这样一来,代码非常有利于阅读,事件的整个过程可以一眼看出来。如果是第一次接触这段代码就能很容易地理解其中的意思,就是当这个输入框输入的时候,获取他的值然后过滤掉长度小于 2 的情况,然后只有暂停输入的时候,然后和上次不一样,就去处理一下这个值,获取到值之后就展示出来。
个人觉得函数式最大的好处就是能够把代码变得足够语义化,能够把自然语言或者流程图直接转换成代码。
现在我们从一个事件组合来谈代码。
举个大家喜闻乐见的拖拽的例子,先来分析一下拖拽是一个怎么样的事件
当鼠标左键按下时且开始移动,直到左键抬起。
直接可以用代码去描述这个组合事件:
var dragTarget = document.getElementById('dragTarget');
// Get the three major events
var mouseup = Rx.Observable.fromEvent(dragTarget, 'mouseup');
var mousemove = Rx.Observable.fromEvent(document, 'mousemove');
var mousedown = Rx.Observable.fromEvent(dragTarget, 'mousedown');
var mousedrag = mousedown.flatMap(function (md) {
// calculate offsets when mouse down
var startX = md.offsetX, startY = md.offsetY;
// Calculate delta with mousemove until mouseup
return mousemove.map(function (mm) {
mm.preventDefault();
return {
left: mm.clientX - startX,
top: mm.clientY - startY
};
}).takeUntil(mouseup);
});
// Update position
var subscription = mousedrag.subscribe(function (pos) {
dragTarget.style.top = pos.top + 'px';
dragTarget.style.left = pos.left + 'px';
});
就像 Rx 首页介绍自己
ReactiveX is more than an API, it's an idea and a breakthrough in programming. It has inspired several other APIs, frameworks, and even programming languages.
我在受 Rx 影响之后有了一点自己的理解。
前段时间公司业务中写了个抽奖的页面,后面空闲下来了,开始思考抽奖这件事情,抽奖的事件流抽象之后应该是这样:
等待用户操作,期间可能需要检查用户状态,然后用户操作,确认操作结果,过滤不正常的返回数据,展现给用户
直接转换成代码应该是这样:
Lottery
.filter(()=> {
return true;
})
.wait(()=> {
console.log('wait for click');
return true;
})
.until(()=> {
return new Promise((resolve, reject) => {
setTimeout(()=>{
console.log('user clicked');
resolve(true);
}, 2000)
});
})
.filter((e) => {
console.log('event: ' + e);
return true;
})
.lottery(() => {
return new Promise((resolve, reject) => {
console.log('client request');
setTimeout(()=>{
console.log('server respones');
Math.random().toFixed(1) > 0.5 ? resolve({data: 'hello'}): reject('net work error');
}, 2000)
});
})
.done((d) => {
console.log('lottery result: ' + d.data);
return d;
})
.error((e) => {
console.error('error: '+e);
})
.filter((d) => {
console.log('last filter: ' + d);
return true;
})
.end()
所有的这些中间事件应该是可以随时拆卸和组装。
那思考一下如何用 js 来写出这样的接口。
首先这个事件流应该是一个串行操作,如果其中有一个报错或者更多的是返回了 false,应该直接打断事件流。
第一个想到的是 generator,但是 generator 需要自己去写执行器,还需要加上很多判断,我写过一段,发现由于这些操作中可能会返回一个 promise 对象,还是需要两个 generator function,非常蛋疼。所以还是直接上 async 函数,先来实现刚才说的这个接口:
/**
* 抽奖的事件流抽象描述
* 需要把所有的操作变成promise对象
*/
let Lottery = (() => {
let Lottery = {};
let actions = [];
let errorAction;
let actionNames = ['filter', 'wait', 'until', 'lottery', 'done', 'error'];
for(name of actionNames) {
if(name == 'error') {
Lottery[name] = function(action) {
errorAction = action;
return this;
}
} else {
Lottery[name] = function(action) {
actions.push(action);
return this;
}
}
}
Lottery.end = () => {
doActions(actions);
};
let doActions = async (actions) => {
let ret = null;
try {
for(let action of actions) {
ret = await new Promise((resolve, reject) => {
let result = action(ret);
if(typeof result == void 0 || result == false) reject();
resolve(result);
});
}
} catch(e) {
errorAction(e);
}
return ret;
};
return Lottery;
})();
里面还用到了函数式编程的一些技巧,比如懒执行函数,把所有的操作函数先存起来,最后再去执行。
代码还有很多问题,比如不能很好地处理报错,也就是不符合事件流的事件处理,还有暂时还没有加上让这个事件流循环起来的操作,毕竟用户操作不是一次性的,而且似乎不太符合函数式编程的思想,不过至少接口可以跑起来,符合预期了。
原文地址:https://suanlatudousi.com/2015/10/23/use-semantic-code/
扫码关注w3ctech微信公众号
如果我猜错的话,您应该是好搜的王佳裕工程师,对吗?
共收到1条回复