2.3 数据响应式原理
Vue中最重要的概念就是响应式数据,一方面指衍生数据和元数据之间的响应,通过数据链来实现;另一方面则是指视图与数据之间的绑定。
本节将深入讲解这两方面的内容。
2.3.1 初识数据链
数据链在学术上被定义为连通数据的链路。在这条链路上有一到多个数据起点(元数据),并通过该点不断衍生拓展新的节点(衍生数据),形成一个庞大的网状结构。当你修改数据起点时,所有存在在网上的节点值都将同步更新,如图2.12所示。
图2.12 单一起点的数据链路
图2.12是只有一个起点的数据链,结构比较简单,当链路存在的数据起点越来越多时,结构会变得越来越复杂,如图2.13所示。
图2.13 多起点的数据链路
得益于数据链,在Vue中我们可以通过修改元数据的值来触发一系列数据的更新。当然,我们在做数据结构的设计时,应该尽量降低数据链的复杂度。毕竟,代码是给机器读的,但也是给人看的。
2.3.2 函数式编程
在上一小节中,元数据a和b通过变量声明即可实现:
let a = 3, b = 4
但是衍生数据应该怎样实现从而保证其值只依赖于元数据而不允许被外界修改呢?这里先介绍一下函数式编程的概念。
函数式编程(Functional Programming)是一种结构化编程方式,力求将运算过程写成一系列嵌套的函数调用。
源于JS中“万物皆对象”的理念,函数式编程认定函数是第一等公民,可以赋值给其他变量、用作另一个函数的参数或者作为函数返回值来使用。
由于其作用是处理运算,因此函数体只能包含运算过程,而且必带返回值(在实际开发中,不做I/O读写操作是不可能的,不过要把I/O限制到最小),标准格式如下:
let double = function (num) { return num * 2 }
函数式编程的核心是根据元数据生成新的衍生数据,提供唯一确定的输入,函数将返回唯一确定的输出,它并不会修改原有变量的值。这在运用JS闭包概念进行开发时尤为重要,在函数作用域内调用域外或全局的变量时并不会修改它们的值,安全无污染。
最后一点,使用函数式编程(加lambda表达式之后)可以使代码看起来十分高大上,如以下代码所示:
let x = (x => (x => x * 9)(x) + 3)(5) let y = y => (y => y * 9)(y) + 3 console.log(x) console.log(y(5))
这样的编程方式可使代码变得极其简洁,但也极其难读。上面只是一个基本示例,实际开发中能演化出无限复杂的结构,感兴趣的同学可以推算一下示例的结果并运行验证一下。
通过函数式编程,衍生数据也得以实现。实际上,函数式编程就是建立了一条数据流通的链路,开发者只需要关注输入和输出两端的内容就可以,这是封装复用的一种最佳实践,在高效开发中举足轻重。
2.3.3 Vue中的数据链
Vue实例提供了computed计算属性选项,以供开发者生成衍生数据对象。虽然计算属性以函数形式声明,却并不接受参数,也只能以属性的方式调用。由于计算属性的this指向Vue实例,所以它可以获取实例上所有已挂载的可见属性。下面来看一个示例:
<style> #app { font-family: Roboto, sans-serif; color: #363e4f; } .data-label { display: inline-block; width: 160px; } </style> <div id="app"> <p><strong class="data-label">A</strong><input type="text" v-model="a"></p> <p><strong class="data-label">B</strong><input type="text" v-model="b"></p> <p><strong class="data-label">C=A*2+2</strong>{{ c }}</p> <p><strong class="data-label">D=A+B*2</strong>{{ d }}</p> <p><strong class="data-label">E=B/2</strong>{{ e }}</p> <p><strong class="data-label">F=C+D</strong>{{ f }}</p> <p><strong class="data-label">G=D-E</strong>{{ g }}</p> </div> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.min. js"></script> <script type="text/javascript"> let vm = new Vue({ el: '#app', data () { return { a: 3, b: 4 } }, computed: { // 计算属性 c () { return this.a * 2 + 2 }, d () { return Number(this.a) + this.b * 2 }, e () { return this.b / 2 }, f () { return Number(this.c) + Number(this.d) }, g () { return this.d - this.e } } }) </script>
初始结果如图2.14所示。
图2.14 A和B为初始值时
当修改元数据A和B时,运行结果如图2.15所示。
图2.15 修改A和B的值之后
当然,开发者也可以以函数的形式创建数据链以实现数据之间的响应,如下代码:
methods: { getC (suf) { return this.a * 2 + (suf || 2) } }
2.3.4 数据绑定视图
这是一个含有字符串类型属性profile的对象:
let obj = { pro file: '' }
不过,身为对象属性的profile仅仅只是个字符串吗?笔者在控制台中使用Object API中的getOwnPropertyDescriptor方法将其“内在”打印出来,如图2.16所示。
图2.16 深入对象属性
原来,对象属性内藏乾坤,我们甚至可以使用Object API的defineProperty方法对其配置,属性配置项(描述符)如表2.1所示。
表2.1 对象属性配置表
在这里可以停顿一下,想一想,对象属性被赋值时调用的set有何妙用呢?下面来看一段有关defineProperty的代码:
<span id="harry" style="line-height: 32px;"> </span><br> <input id="trigger" type="text"> <script type="text/javascript"> let harry = document.getElementById('harry') let trigger = document.getElementById('trigger') let key = 'pro file' // 对象属性键名 let store = {} // 辅助get取值 let obj = { // 对象 pro file: '' } Object.de fineProperty(obj, key, { set (value) { harry.innerText = value // 重点:修改DOM节点视图 store[key] = value }, get () { return store[key] } }) trigger.addEventListener('keyup', function () { obj[key] = this.value console.log(obj[key]) }) </script>
上述代码中,笔者在对象属性的setter函数中修改文本节点的值,所以当obj.profile被重新赋值时,节点视图也会同步更新;然后对输入框添加事件监听(addEventListener),当用户事件触发时,输入值将被赋于obj.profile。以此方式,我们实现了数据与视图之间的“双向绑定”,这也是Vue数据与视图绑定的实现原理。
代码运行结果如图2.17所示。
图2.17 数据与视图绑定
在Vue中,当我们把普通的JavaScript对象传给Vue实例的data选项时,Vue将遍历对象属性,并使用Object.defineProperty将其全部转化为getter/setter,并在组件渲染时将属性记录为依赖。之后当依赖项的setter函数被调用时,会通知watcher重新计算并更新其关联的所有组件。
由于Object.defineProperty是ES5中一个无法shim(自定义拓展)的特性,所以Vue应用无法运行在不支持Object.defineProperty的IE8及其以下版本浏览器上。