使用语义化的代码

从 React 说起

对接触过 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微信

扫码关注w3ctech微信公众号

共收到1条回复

  • 如果我猜错的话,您应该是好搜的王佳裕工程师,对吗?

    回复此楼

AltStyle によって変換されたページ (->オリジナル) /