w3ctech

使用语义化的代码

从 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条回复

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

    回复此楼