前端演进思考

601 阅读4分钟

0. 从模型说起

前端的基本模型如下:

原生JS/jQuery 这时候开发者需要手动处理 BindChangeUpdate,jQuery 相比原生 JS,简化了操作,屏蔽了浏览器的兼容性。

三大框架 这时候开发者只需处理 BindChange,由框架自动完成 Update。框架使用一套高效的 diff 算法最小化更新 DOM,因此除了简化开发者工作,还带来了前端性能的提升。

1. 组件思想

除此之外,框架另一个突出贡献是引入了 组件 思想,这使我们能将一个复杂的应用拆成多个组件逐个击破,还使我们能用一套基础组件组合出各种各样的应用,以高效的响应业务需求。

但是在大型应用中,这种方式又出现 2个严重的问题

跨组件数据传递 在单向数据流中,Props down & Event up,跨组件的数据传递非常困难,如上图的,中间2个组件即使不关注数据,也必须写上相关代码让数据传下来(到现实中,我们说的组织层级过多导致流程冗杂,也是类似的道理)。

意外数据修改 JS 的数据类型和 Java 类似,除了少数几个基础类型是值,其他的都是 引用,因此在某个子组件里的数据修改,很容易影响到其他地方。尽管框架一再警告 props 应该看作只读的,但受制于语言特性,框架无法阻止意外的修改。

从开发者角度,我们也希望能 在一个地方就看到整个应用的状态,而不是深入到各个组件里去查看。

2. 状态管理

对于跨组件数据共享问题,一个自然的想法是:为啥不写一个公共的模块,把共享的变量都放到里面呢,之前写后端也是这样的呀?

前端还真不能这么干!根本原因在于我们要依赖框架的自动 Update 能力(第一张图),React 中 有且仅有两种情况会触发界面更新

  • state 变化(setState
  • context 变化(Redux)

(说 forceUpdate 的,看我眼神……)

于是 Redux 登场。理想情况下,数据流会被规整成下图那样:

但是 Redux 也不是免费的,它带来两个我一直耿耿于怀的问题:

组件难以复用 组件连接 Redux 以后,即丧失了封闭性,如果页面上出现这样一个组件的两个实例,那这两个实例的状态就会互相影响。

割裂的逻辑 强行将逻辑拆成 actionreducer,一个功能要改3处代码,怎么看怎么蠢。

我也不喜欢用 Mbox,主要是 React 是函数式的,而 Mbox 是响应式的,混用的感觉就像 React 里用 jQuery。如果想要响应式的开发体验,为啥不直接用 Vue 呢?

响应式 强调一个变量改变,相关的变量自动更新。

函数式 是一种映射关系,强调特定的输入一定有特定的输出。

3. Maybe hooks?

较早前就听过 hooks,当时并不以为意,想着无非是给函数组件注入状态而已。数据和方法绑定,扒着指头数也就3种方式:

  • 模块
  • 闭包

类就挺直观的呀,干吗要非要用闭包这种 tricky 方式去做?直到最近读文档,才发现这可能就是我一直心心念的 既有清晰的数据流,又不割裂逻辑 的实现方式,抄 一段代码 感受感受:

class FriendStatusWithCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0, isOnline: null };
    this.handleStatusChange = this.handleStatusChange.bind(this);
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
    ChatAPI.subscribeToFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentWillUnmount() {
    ChatAPI.unsubscribeFromFriendStatus(
      this.props.friend.id,
      this.handleStatusChange
    );
  }

  handleStatusChange(status) {
    this.setState({
      isOnline: status.isOnline
    });
  }
  // ...
function FriendStatusWithCounter(props) {
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

hooks 能不受 class 组件生命周期钩子的限制,将逻辑相关的代码集中到一起了!

4. 其他

state 里该放什么数据 由于我们使用 state 的本质是利用框架的自动更新 DOM 的能力,因此 state 里只需放 界面相关 的数据。

Redux 里该放什么数据 Redux 的职责更多是数据管理,开发实践中,随业务迭代接口数据变动会比较频繁,适宜放 Redux 中;另外跨组件共享的数据也要放 Redux 中。总结起来就是 界面相关 && (服务端的 || 共享的) 应该放 Redux 中。

那岂不是和业务数据相差太大了 只把界面相关的数据放 state 和 redux 里,会不会与业务数据全貌相差太大了呢?我认为不会,在后端“读写分离”的实践中,一般做法只是将查询和写入的服务放到不同的实例上,而更进一步的做法是 查询用的表和写入用的表都是不一样的(CQRS)!state 和 Redux 就是类似这样的读模型,界面无关的数据,完全可以放到一个公共模块里。