认识
一、认识
双向数据绑定 基于 MVVM
模型, 包括数据层Model
、视图层View
、业务逻辑层ViewModel
。其中 ViewModel
(业务逻辑层) 负责将数据和视图关联起来, 提供了数据变化后更新视图和视图变化后更新数据这样一个功能,就是传统意义上的双向绑定。
Vue2.x
实现双向绑定(响应式)核心是通过三个模块: Observer
监听器、Watcher
订阅者 和 Compile
编译器。流程如下:
-
组件初始化的过程中: 首先初始化状态, 采用
Object.defineProperty
劫持data
对象中每个属性的get
和set
方法, 为每个属性创建一个Dep
实例, 用于收集依赖; 随后在调用$mount
时, 初始化渲染Watcher
, 每个组件实例都会在渲染时初始化一个watcher
实例,它会将组件渲染过程中所接触的响应式变量记为依赖,并且保存了组件的更新方法update
-
在
$mount
挂载阶段,Compile
编译器会将模板进行编译, 找到里面动态绑定的响应式数据并初始化视图。视图中会用到data
中的某个key
, 这称为依赖, 触发key
对应的getter
, 将当前组件的Watcher
添加到key
对应的Dep
中, 这个过程也叫做 依赖收集。 -
当响应式数据发生变更, 触发对应的
setter
, 找到对应的Dep
,Dep
通知所有的Watcher
,Watcher
接收到监听器的信号就会执行更新函数去更新视图
二、工作
Vue2.x 实现双向绑定(响应式)核心是通过三个模块: Observer
监听器、Watcher
订阅者 和 Compile
编译器。流程如下:
-
Vue
初始化的过程中, 调用initState
初始化响应式数据, 调用observe
为data
添加响应性 -
遍历
data
对象所有属性, 调用defineReactive
为每个属性添加响应性:-
为每个属性实例化
Dep
-
每个属性继续调用
observe
尝试为后代属性添加响应性,observe
函数中会判断属性类型, 只有对象或者数组才会继续添加响应性-
如果
data[xx]
为对象: 继续循环遍历data[xx]
对象中的所有属性, 递归 -
如果
data[xx]
为数组: 重写data[xx]
中的push
、pop
、shift
、unshift
、splice
、sort
、reverse
等七个可以原地改变数组的方法, 然后调用observeArray
遍历数组, 为每个元素调用observe
添加响应性
-
-
为每个属性通过
defineProperty
添加setter/getter
拦截函数, 后续在访问或者设置值时可以拦截
-
-
访问数据, 触发
getter
函数: 如果当前Watcher
存在的话, 调用dep.depend()
进行依赖收集, 当前Watcher
的newDeps
存储当前dep
, 当前dep
的subs
存储当前Watcher
-
设置数据值, 触发
setter
函数: 调用dep.notify()
, 循环遍历subs
中的所有Watcher
, 执行Watcher
的update
方法。 -
在
update
方法中, 会将此时的Watcher
加入到渲染队列queue
, 通过nextTick
进行批量更新渲染 -
在
nextTick
中,通过promise.then(flushCallbacks)
将批量更新任务放到了微任务队列, 依次执行任务队列中的任务, 每一个任务就是一个Watcher
, 开始执行每一个Watcher
的run
方法 -
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
实现数据响应式时, 只为初始已有的属性添加 getter
和 setter
, 而后续动态添加的属性没有 getter
、setter
, 不具备响应式。因此, 需要以下三种方式为动态添加的属性具备响应式:
-
Vue.set()
: 通过Vue.set()
向响应式对象中添加一个property
, 为新的property
添加响应式 -
Object.assign()
: 创建一个新的对象, 合并原对象和动态添加的属性 -
$forceUpdate
: 使用$forceUpdate
迫使Vue
实例重新渲染, 仅影响实例本身和插入插槽内容的子组件,而不是所有组件