浅析组件化时代的前端状态管理(三):数据驱动渲染

为什么要把这一节叫做数据驱动渲染?

在组件化时代, 各大框架都是推崇数据驱动UI渲染的, 才会有状态管理的需求场景, 所以数据驱动渲染是状态管理话题的前置条件

何为数据驱动渲染?

用函数式编程的思维理解就是: view = render(data) 这个公式, 即同样的 data, 通过同样的 render函数, 无论执行多少次, 最终渲染到页面的展示结果是一样的, 具有幂等性

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。
在编程中.一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。
幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。

数据驱动渲染的实现:

假如组件都是纯函数, 那么控制最终渲染结果的是 data, 如果想修改页面展示, 只需要修改 data.

那如何实现数据驱动渲染, 目前很多框架都帮我们实现了, 拿当前最热门的 React Vue 框架来举例,

React的底层会在内存中生成一个 Virtual DOM树, 整个渲染的链路是: Model -> Virtual DOM -> 浏览器DOM
如果要更新页面的展示, 得先修改 Model里面的数据. React里面的 setState方法就是干这事的, 可以参考下面的一个不算太完整的伪代码理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

// 渲染启动函数
const renderRoot = (rootComponent, rootDomNode) => {
// vitual dom 根节点
const virtualDomRoot = {
root: null,
child: null,
parents: null
}
// 递归遍历 rootComponent 下面的所有子节点, 并将节点挂载在 virtualDomRoot 这个树上
const newVirtualDomTree = renderChild(rootComponent, virtualDomRoot)

// 新老 Virtual Dom 对比, 生成一个差异 Dom Node 列表
const diffVirtualDomNodes = diff(newVirtualDomTree, oldVirtualDomTree)

// 将差异 Dom Node 更新到真实的浏览器的 Dom
patch(rootDomNode, diffVirtualDomNodes);
}


// 应用组件
class App extends Component {
constructor() {
super()

// 在这里定义 Model 层
this.state = {
value: '',
}
}
onChange = (event, value) => {
this.setState({
value: value
})
}

// 通过 render方法把内容更新到 Virtual DOM中, React框架会自动计算哪些 Virtual DOM节点是有改动, 有改动的元素才会被更新到浏览器的DOM中
render() {
return (
<div>{this.props.title}</div>
)
}
}

// 渲染入口
renderRoot(<App/>, document.querySelector('#root'))

真实的React的框架比上面的复杂很多, 具体的实现也有点差别, 但是大的方向上确实是这样的
看完上述的例子大概就能理解 Model, Virtual Dom, 浏览器 Dom之前的关联

MVVM模式

MVVM的核心是 将数据与视图绑定起来, 操作数据等同于操作视图, 数据修改后视图自动更新.
MVVM

在React中, View 是浏览器中的真实的 Dom, Virtual Dom 就是其中的 ViewModel, Model 是组件里面的 state
React 和 MVVM有个比较大的区别是, React 只实现单向的数据流绑定. 即 Virtual Dom中改变了,会自动更新到 浏览器额的Dom上. 而 Model中的数据改变,需要通过JSX中设置UI事件监听回调来修改, 并不会自动修改Model的数据

而 Vue则是实现了比较完备的单向绑定和双向绑定的功能, Vue中实现数据驱动UI更新其实是一个更好的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Vue.component('example', {
template: '<span>{{ message }}</span>',
data: function () {
return {
message: '没有更新'
}
},
methods: {
updateMessage: function () {
this.message = '更新完成1'
this.message = '更新完成2'
this.message = '更新完成3' // 这里同一个循环中多次赋值, 只有最后一次的才会在下个循环渲染到dom上
console.log(this.$el.textContent) // => '没有更新'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '更新完成3'
})
}
}
})

从上述例子可以看到, 在Vue中, data 里面的 message 跟 template 里面的 message是绑定的, 只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。
如果同一个 watcher 被多次触发,只会一次推入到队列中。
这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。
然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际渲染操作。
上述的例子只是一个单项数据绑定的例子

Vue如何实现数据双向绑定?

当用户更改了VM层表单控件的数据时,通过v-model自动更新到M层(v-model是对表单控件的事件的封装)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

// Vue 双向绑定例子
<div id='example'>
<input v-model="message" placeholder="edit me">
<p>Message is: {{ message }}</p>
</div>

new Vue({
el: '#example',
data: {
message: 'hello'
}
})


// 使用原生JS等效实现
<input type="text" id="input_0">
<p id="preview_0"></p>

<script>
var obj = {}
var preview = document.querySelector('#preview_0')
var inputEle = document.querySelector('#input_0')
Object.defineProperty(obj, 'message', {
set: function (newVal) {
// 模拟 Vue 驱动页面dom更新
// 在 Vue 中, 并非如此粗暴的直接更新数据到Dom上, 而是要通过Vue的渲染Virtual Dom 和 异步更新队列, 批量更新到浏览器Dom中
inputEle.value = newVal
preview.textContent = newVal
}
})


inputEle.addEventListener('input', function(e) {
// 给obj的message属性赋值,进而触发该属性的set方法, 驱动页面UI更新
obj.message = e.target.value
})
</script>

更多具体的实现原理可以参考 深入Vue响应式原理

数据驱动与MVC

通过上述的例子我们可以了解到数据驱动渲染是什么, 在数据驱动的理念下, 假如我们要搭建一个复杂应用, 可以简单的把工作拆分为:

  1. UI组件/容器组件的(view)
  2. 业务逻辑(controller)
  3. 状态管理(model)

咋一看这个模式不就是MVC的模式嘛 , 确实, 大部分架构师在设计项目架构的时候也会遵循MVC理念, 也有很多框架在设计上遵循MVC理念, MVC核心的理念就是强调职责分离, 各个层只专注做好自己工作

在UI组件上, 有太多的开源组件可以直接复用, 极大的减轻了开发的成本, 那么开发者只需要关注好 业务逻辑 和 模型层就好了

而在 业务逻辑 和 模型层中, 我们要解决的一个最大的问题就是组件间通信, 举个例子:

一个常见的业务场景是搜索的场景, 一般我们会把 搜索输入组件 和 搜索结果分为两个组件,
用户在A组件输入 “xxxx” 一行字符串, 这时候触发 controller层的业务逻辑, 向后端发起请求, 得到搜索结果, 并将搜索结果渲染到 B组件中
这里 A 和 B是两个不同的组件, A组件触发的动作如何更新B组件的内容呢? 这里就是隐含着组件间通信的场景了.

在前端的页面中, 一个地方的更改, 要触发页面其他的地方的场景是在太多了, 在JQuery时代, 大家可能会觉得这都是没有必要拿出来讨论的, 不就是一个$(“#id”).html(content) 就能解决的么?
而这恰恰是 “组件化 + 数据驱动” 的理念与之前直接操作Dom的模式最大的区别, 后面的章节会详细介绍 Redux Mobx Vue 等框架, 是如何玩转 “组件化+数据驱动”