vue 设计原则 — 数据绑定

create on in source_code with 0 comment and 481 view

之前在研究 virtual-dom 时,有提到过数据监听,但是并没有深入了解,后来发现,关于数据监听这一块还是挺有趣的,其涉及的知识点也比较广泛。

为什么要数据绑定呢,数据和谁进行绑定?
从开发者的角度来看,我们知道,数据和视图进行了绑定,当数据发生变化时,视图则自动更新;视图发生更新,数据也发生更新。

那么,这里的视图使用的数据的本质是什么呢?
若没有了解其原理,第一感觉很可能认为是 data 的引用,因为从表面特征看来,它们一一映射,非常符合人们对事物的主观认知。而本质上,vue 在其中做了很多处理,才达到这样的效果。

带着疑问,现在看一下 Vue.js 官网介绍响应式原理的这张图。
vue 数据绑定

这张图清晰的展现了整个流程。

  1. 通过首次渲染会触发 Data 的 getter,然后进行依赖收集。
  2. Data 发生更改,触发 setter, 通知 watcher。
  3. watcher 通过回调通知重新渲染的函数,之后通过 diff 算法来决定是否发生视图的更新。

总的来看, 数据绑定需要解决的问题有collect as Dependency(依赖收集),watcher(监听数据),notify(通知)。

依赖收集

当我们定义 Data 时,其实并非所有的 Data 属性都会被视图所使用,举个例子:

<template> <div> <h2> {{name}} </h2> <p> {{content}} </p> </div> </template> <script> export module { data(){ return { name: 'luo', content: 'hello world', other: 'it is nothing' } } } </script>

我们在 .vue 模块中定义了 data, 其中,视图使用了 name, content 属性,没使用 other 属性,所以当 other 发生改变时,对于视图来说没有任何的影响,为了节省性能,我们可不用监听 other 的改变。
所以我们要做的是,模块在初始渲染时,将视图用到属性作为依赖收集起来,然后对其进行监听。

那么如何进行依赖收集呢?

在 vue 中,借用了Object.defineProperty的存取描述符对数据属性的操作进行预截取,在每次当访问对象的属性时,都会触发 getter。
所以,在初始化 Data 时,我们只需在 get 方法中,注入依赖收集操作即可。

let dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { // 依赖收集 dep.depend() if (childOb) { // 子对象进行依赖收集 (存于 dep 属性中,当子对象<数组>发生改变时,通过 该dep 去通知 watcher) childOb.dep.depend() } ... return value }, set(newVal) { // notify })

注意:依赖(dep)会存于两个地方,分别是操作描述符的闭包内,以及 observer 实例的 dep 属性中。

除了模板视图中访问 data 时会触发 getter,其它的地方访问时也会触发 getter,这样就导致收集的依赖不纯粹,除此之外,很多情况下我们有针对某一个属性进行监听的需求,例如: name 属性每次发生更改,为其添加前缀 mr-

为了解决这些问题,我们需引用订阅者模式。
其中,data的属性作为可观察者(observer),依赖列表为发布者,视图渲染为订阅者的回调

Vue在初始化组件数据时,在生命周期的 beforeCreate 与 created 钩子函数之间实现了对data、props、computed、methods、events以及 watch 的处理。

在 生命周期的 beforeMount 和 mounted 之间,开始进行模板的首次渲染,此时会将视图渲染的订阅者作为全局 watcher,凡是加入依赖列表的可观察者,都会被该全局 watcher 进行监听,当模板渲染完成后,全局 watcher 将置为 null。

数据监听

在 vue 中,借用了Object.defineProperty的存取描述符来监听数据,但使用它还完全不够,因为它只能监听普通对象的属性值变化,不能监听新增删除属性,数组对象成员改变等操作。
对于一个深层嵌套的对象,它的变化有哪些类型呢?
举个栗子,有一个 data 结构如下:

let data = { name: 'luo', info: { sex: 'male', age: '22', tag: ['joke', 'goodness'] }, date: [{ val: '2019-01-01', record: 'travel' }, { val: '2019-03-01', record: 'birthday' }] }

对于这样的 data, 变化的情况有:

  1. 普通对象属性的增、改
  • data 添加 location 属性
  • data.info 添加 birth 属性
  • data.info.age 改为 23
  1. 数组成员的增、删、改
  • data.date 添加成员 {val: '2019-01-02'}
  • data.info 删除成员
  • data.date 第一个成员内容修改 为 {val: ‘2019-01-01’,record: ‘travel’}
  1. 数组长度更改
  • data.date 长度改为 3

其中,对于新增的location 属性来说,该属性并没有被依赖收集,所以无需监听它的改变;data.info.age 改为 23 会被 data.info.age 属性 setter 捕捉到;
监听数组改变是一个麻烦事,就现在的 ES 语法来说,还未存在监听数组改变的语法糖,要实现监听需要对其进行特殊处理,vue 通过重写数组的方法来实现监听数组的改变:每当这些方法被调用时,除了实现方法自身的功能外,还需通知订阅者进行响应式处理。

修改的方法有:push,pop,shift,unshift,splice,sort,reverse

值得注意的是,使用下标去修改某个数组成员时,vue 是不能感应到的,只能通过调用 splice 方法来修改,在vue 文档中也有相关说明。
数组更新检测,实现的方法也很简单,附上源码传送门

总的来说,vue 在初始化处理 data 时,会深度遍历 data ,将 data 及其子对象都变为可观察对象(observer)。首先,将 data 对象变成一个 observer(使用__ob__属性存储 observer 实例的方式进行标记),接着遍历对象的属性,对每个属性注入一个依赖实例(dep),依赖实例会存储该属性的订阅者,每当属性发生改变时,会通知这些订阅者;

注意: 只有 普通对象 和 数组 才能进行 observer,否则直接 return。

let data = { name: 'luo', info: { sex: 'male', age: '22', tag: ['joke', 'goodness'] }, }

若属性值是普通对象或数组类型, 则进行递归操作。
对于普通对象类型来说,如 data.info,若 data.info.set 值修改为 ‘female’, 意味着 data.info.set 属性发生改变,而不是 data.info 发生更改(对象的引用不变)。

data.info.set 属性改变,其描述符 setter 会触发闭包中的 dep,通知订阅者。

对于数组类型来说,如 data.info.tag ,除了将其变为 observer 外,还需改变该数组对象的原生方法,以达到监听效果,若该数组成员改变,则将其看成 data.info.tag 属性值发生了改变(实质上,数组引用是不变的)。

与属性值实质上发生改变不同的是,数组内容发生改变,去触发数组对象的 __ob__.dep 属性,而不是闭包中的dep。

最后,遍历数组成员,将数组的每一项递归 observer。

视图渲染

视图渲染是订阅者(watcher)的回调操作,当订阅者收到通知时,则调用 re-render 回调函数,执行 dom-tree, diff-tree, reader-tree等操作,最后完成 dom 的更新。

为了有更深入的了解,我实现的一个简化版的 数据绑定,欢迎参考。

😁😂😃😄😅😆😇😈😉😐😑😒😓😔😕😖😗😘😙😠😡😢😣😤😥😦😧😨😩😰😱😲😳😴😵😶😷😸😹🙀🙁🙂🙃🙄🙅🙆🙇🙈
🙂