跳到主要内容

认识

2024年03月13日
柏拉文
越努力,越幸运

一、认识


Preview

双向数据绑定 基于 MVVM 模型, 包括数据层Model视图层View业务逻辑层ViewModel。其中 ViewModel(业务逻辑层) 负责将数据和视图关联起来, 提供了数据变化后更新视图视图变化后更新数据这样一个功能,就是传统意义上的双向绑定。

Vue2.x 实现双向绑定(响应式)核心是通过三个模块: Observer监听器Watcher订阅者Compile编译器。流程如下:

  1. 组件初始化的过程中: 首先初始化状态, 采用 Object.defineProperty 劫持data对象中每个属性的 getset 方法, 为每个属性创建一个 Dep 实例, 用于收集依赖; 随后在调用 $mount 时, 初始化渲染Watcher, 每个组件实例都会在渲染时初始化一个 watcher 实例,它会将组件渲染过程中所接触的响应式变量记为依赖,并且保存了组件的更新方法 update

  2. $mount 挂载阶段, Compile 编译器会将模板进行编译, 找到里面动态绑定的响应式数据并初始化视图。视图中会用到 data 中的某个 key, 这称为依赖, 触发 key 对应的 getter, 将当前组件的 Watcher 添加到 key 对应的 Dep 中, 这个过程也叫做 依赖收集

  3. 当响应式数据发生变更, 触发对应的 setter, 找到对应的 Dep, Dep 通知所有的 Watcher, Watcher接收到监听器的信号就会执行更新函数去更新视图

二、工作


Vue2.x 实现双向绑定(响应式)核心是通过三个模块: Observer监听器Watcher订阅者Compile编译器。流程如下:

  1. Vue 初始化的过程中, 调用 initState 初始化响应式数据, 调用 observedata 添加响应性

  2. 遍历 data 对象所有属性, 调用 defineReactive 为每个属性添加响应性:

    1. 为每个属性实例化 Dep

    2. 每个属性继续调用 observe 尝试为后代属性添加响应性, observe 函数中会判断属性类型, 只有对象或者数组才会继续添加响应性

      • 如果 data[xx] 为对象: 继续循环遍历 data[xx] 对象中的所有属性, 递归

      • 如果 data[xx] 为数组: 重写 data[xx] 中的 pushpopshiftunshiftsplicesortreverse 等七个可以原地改变数组的方法, 然后调用 observeArray 遍历数组, 为每个元素调用 observe 添加响应性

    3. 为每个属性通过 defineProperty 添加 setter/getter 拦截函数, 后续在访问或者设置值时可以拦截

  3. 访问数据, 触发 getter 函数: 如果当前 Watcher 存在的话, 调用 dep.depend() 进行依赖收集, 当前 WatchernewDeps 存储当前 dep , 当前 depsubs 存储当前 Watcher

  4. 设置数据值, 触发 setter 函数: 调用 dep.notify() , 循环遍历 subs 中的所有 Watcher, 执行 Watcherupdate 方法。

  5. update 方法中, 会将此时的 Watcher 加入到渲染队列 queue, 通过 nextTick 进行批量更新渲染

  6. nextTick 中,通过 promise.then(flushCallbacks) 将批量更新任务放到了微任务队列, 依次执行任务队列中的任务, 每一个任务就是一个 Watcher, 开始执行每一个 Watcherrun 方法

  7. Watcher 中的 run 方法调用 Watcher 中的 get 方法, get 方法调用 Watcher 中的 getter 函数, 此时的 getter 函数就是 updateComponent , 用于初始或者更新渲染

三、问题


3.1 组件 data 定义函数与对象的区别?

定义组件的时候, 逻辑如下:

function Component(){

}

Component.prototype.data = {
a: 1,
b: 2
}

创建两个组件实例, 如下所示:

const componentA = new Component();
const componentB = new Component();

修改 componentA 组件中 data 的值, componentB 组件中的值也发生了变化。

因此, 组件 data 定义对象时, 多个组件的 data 使用的是同一个内存地址, 每个组件实例对象的数据受到了其他组件实例对象的数据污染。组件 data 定义函数时, 每一个组件实例会返回全新的 data,使每个组件实例对象不会受到其他实例对象的数据污染。

3.2 Vue2 通过数组下标更改数组视图为什么不会更新

响应式原理:

export class Observer {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 这里对数组进行单独处理
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
// 对对象遍历所有键值
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}

响应式原理解读: 对于对象是通过 Object.keys() 遍历全部的键值,对数组只是 observe 监听已有的元素,所以通过下标更改不会触发响应式更新。

响应式原理结论: 如果 Vue2.0 开放了对数组下标更改数组视图更新的话, 如果数组的键相较对象多很多,当数组数据大的时候性能会很拉胯,造成性能不好,所以不开放,

3.3 动态给 Vue 的 data 添加一个新的属性时会发生什么? 怎样解决?

data 添加一个新的属性, 可能会发生数据更新但视图未更新的场景。原因是 Vue 通过 Object.defineProperty 实现数据响应式时, 只为初始已有的属性添加 gettersetter, 而后续动态添加的属性没有 gettersetter, 不具备响应式。因此, 需要以下三种方式为动态添加的属性具备响应式:

  1. Vue.set(): 通过 Vue.set() 向响应式对象中添加一个 property, 为新的 property 添加响应式

  2. Object.assign(): 创建一个新的对象, 合并原对象和动态添加的属性

  3. $forceUpdate: 使用 $forceUpdate 迫使 Vue 实例重新渲染, 仅影响实例本身和插入插槽内容的子组件,而不是所有组件