接触过 jQuery 的小伙伴们大概在切换到 mvvm 初总不习惯,需要进行开发思维的转换,从事件驱动的角度出发,到从数据驱动的角度出发,也是不小的挑战。

# 事件驱动

# GUI 与事件

GUI(图形用户界面)与事件驱动的渊源可谓不浅。

GUI 应用程序的特点是注重与用户的交互,因此程序的执行取决于与用户的实时交互情况,大部分的程序执行需要等到用户的交互动作发生之后。

由于用户的输入频率并不高,若不停轮询获取用户输入,就有点像 ajax 轮询和 websocket 推送的关系:

  1. 资源利用率低。
  2. 不能真正做到及时同步。

由于 GUI 程序的执行流程由用户控制,并且不可预期,为了适应这种特点,我们需要采用事件驱动的编程方法。普通程序的执行可概括为“启动——做事——终止”,而事件驱动的程序的执行可概括为“启动——事件循环(即等待事件发生并处理之)”。

# 事件驱动编程

# 事件

事件是可以被控件识别的操作,如按下确定按钮,选择某个单选按钮或者复选框。每一种控件有自己可以识别的事件,如窗体的加载、单击、双击等事件,编辑框(文本框)的文本改变事件,等等。

事件(event)是针对应用程序所发生的事情,并且应用程序需要对这种事情做出响应。

# 事件处理

程序对事件的响应其实就是调用预先编制好的代码来对事件进行处理,这种代码称为事件处理程序(event handler)。

事件驱动编程(event-driven programming)就是针对这种“程序的执行由事件决定”的应用的一种编程范型。

# Event loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为 Event Loop(事件循环)。

关于 Javascript 的单线程与 Event Loop,想要了解可以参考《JavaScript 运行机制详解:再谈 Event Loop》 (opens new window)。今天的主角是数据驱动,事件相关的不进行详细说明了。

# 事件驱动思维

在 GUI 和 Javascript 的设计场景下,我们写代码的时候也会代入这样的思维:

用户输入 => 事件响应 => 代码运行 => 刷新页面状态

于是乎,刚开始写应用的思路如下:

  1. 开发静态页面。
  2. 添加事件监听,包括用户输入、http 请求、定时器触发等事件。
  3. 针对不同事件,编写不同的处理逻辑,包括获取事件状态/输入、计算并更新状态等。
  4. 根据计算后的数据状态,重新渲染页面。

通俗地说,事件驱动思维是从事件响应出发,来完成应用的设计和编程。

# 数据驱动

数据驱动,将我们从复杂的逻辑设计带进数据处理的世界。

# 何为数据

数据是什么,官方回答:数据是科学实验、检验、统计等所获得的和用于科学研究、技术设计、查证、决策等的数值。

但其实不管是资料中、生活和工作中,所有的事物我们都可以抽象为数据。像游戏里面的角色、物品、经验值、天气、时间等等,都是数据。游戏其实也算是对真实世界抽象的一种,而抽象之后,最终都可呈现为数据。

我认为,数据是一个抽象的过程。

回到日常写码中,前端写页面,抽象成数据常用的无非是:

  • 列表 => array
  • 状态 => number/boolen
  • 一个卡片 => object
  • 等等

# 事件驱动到数据驱动

# 数据驱动 vs 事件驱动

要对事件驱动和数据驱动进行直观的比较,大概是以下这样:

# 事件驱动

  1. 构建页面:设计 DOM => 生成 DOM => 绑定事件
  2. 监听事件:操作 UI => 触发事件 => 响应处理 => 更新 UI

# 数据驱动

  1. 构建页面:设计数据结构 => 事件绑定逻辑 => 生成 DOM
  2. 监听事件:操作 UI => 触发事件 => 响应处理 => 更新数据 => 更新 UI

其实最大的转变是,以前会把组件视为 DOM,把事件/逻辑处理视为 Javascript,把样式视为 CSS。而当转换思维方式之后,组件、事件、逻辑处理、样式都是一份数据,我们只需要把数据的状态和转换设计好,剩下的实现则由具现方式(模版引擎、事件机制等)来实现。

# 数据驱动思维

转换到数据驱动思维后,我们在编程实现的过程中,更多的是思考数据的维护和处理,而无需过于考虑 UI 的变化和事件的监听。

拿一个企业网站来说,里面的很多数据和链接,从前我们常用方式是直接写成 DOM,然后就产生了很长的一段 DOM 代码。

如果说我们将其切换到数据,以对象和数组的方式存储,这时候我们只需要写一段具现方式,将这组数据转成 DOM。这种方式有以下好处:

  • 数据变更方便
  • DOM 结构变轻
  • DOM 结构/样式调整方便
  • 抽象设计
  • 代码量减少,易于维护

# 数据驱动与 mvvm

数据驱动的设计思维或许与 mvvm 没有必然的联系,但是 mvvm 框架提供一些具现方式将数据驱动变得更加轻松。

# mvvm 集成具现化方法

拿 vue 框架来说,有以下一些很方便的具现方法:

  • 模板渲染:数据 => AST => 生成 DOM
  • 数据绑定:交互输入/http 请求响应/定时器触发 => 事件监听 => 数据变更 => diff => DOM 更新
  • 路由引擎:url => 数据(host/path/params 等) => 解析对应页面

当我们使用了这些 mvvm 框架时,它们解决了如何让数据转变成需要的东西,将抽象具象化的问题。在这样的情况下,我们只需要完成两步:

  1. 将产品/业务/设计抽象化,将 UI、交互抽象为数据。
  2. 将一组组的数据用逻辑处理连接起来。

# mvvm 推动数据驱动思维

这里借用 vue,来举两个例子吧。

一、获取 input 输入并更新 实现一个 input 的监听输入,并更新输出到模板,我们能有以下代码的变化:

<!--1. 事件驱动-->
<input type="text" id="input" />
<p id="p"></p>
<script>
  $("#input").on("click", e => {
    const val = e.target.value;
    $("#p").text(val);
  });
</script>

<!--2. 数据驱动 + vue-->
<input type="text" v-model="inputValue" />
<p>{{ inputValue }}</p>

当我们在 vue 中,模板引擎帮我们处理了模板渲染、数据绑定的过程,我们只需要知道这里面只有一个有效数据,即 input 的值。

二、部分更新列表 我们再来看个例子,我们有一组数据,需要渲染成一个列表:

const list = [
  { id: 1, name: "name1", href: "http://href1" },
  { id: 2, name: "name2", href: "http://href2" },
  { id: 3, name: "name3", href: "http://href3" },
  { id: 4, name: "name4", href: "http://href4" }
];
  1. 当我们需要渲染成列表时:
<!--1). 事件驱动-->
<ul id="ul"></ul>
<script>
  const dom = $("#ul");
  list.forEach(item => {
    dom.append(
      `<li data-id="${item.id}"><span>${item.name}</span>: <a href="${
        item.href
      }">${item.href}</a></li>`
    );
  });
</script>

<!--2). 数据驱动 + vue-->
<ul>
  <li v-for="item in list" :key="item.id">
    <span>{{item.name}}</span><a :href="item.href">{{item.href}}</a>
  </li>
</ul>
  1. 当我们需要更新一个列表中某个 id 的其中一个数据时(这里需要更改 id 为 3 的 name 值):
// 1). 事件驱动
const dom = $("#ul");
const id = 3;
dom.find(`li[data-id="${id}"] span`).text("newName3");

// 2). 数据驱动 + vue
const id = 3;
list.find(item => item.id == 3).name == "newName3";

当然这里我们已知list里面有id为 3 的值,若是未知或不确定的数据,则需要做好异常处理,如:

const id = 3;
const item3 = list.find(item => item.id == 3);
if (item3) item3.name == "newName3";

在使用数据驱动的时候,模板渲染的事情会交给框架去完成,我们需要做的就是数据处理而已。

# 结束语

思维的切换和视角的转变,是一件很有意思的事情。从更多的角度去观察,去思考,去总结,才能更好地理解被观察体。

部分文章中使用了一些网站的截图,如果涉及侵权,请告诉我删一下谢谢~
温馨提示喵