双向数据绑定
从页面到数据都是通过事件监听的方式,即v-on事件监听。
- 对于表单,就是v-bind:value+v-on:input
- 对于单复选框,就是v-bind:value+v-on:change
- 对于select,将value作为props传给options,用change监听
从数据到页面就是数据绑定,主要是响应式数据绑定,v-on事件监听只是去改变数据,而数据如何响应还是依赖的响应式数据。
整体
MVVM 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:
目前前端框架基本上都采用了MVVM模式实现双向数据绑定,Vue也不例外。但是各个框架实现双向绑定的方法各有不同,目前大概有三种实现方式:
- Angular 脏检查机制
- 发布订阅
- 数据劫持
而Vue采用的是发布订阅和数据劫持结合的方式实现双向绑定,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
实现双向数据绑定的流程为:
- 通过Object.defineProperty对数据进行劫持,并结合观察者模式实现
- 创建一个Observer来劫持监听data中的所有属性,并把所有属性包括子属性都添加了getter/setter
-get方法会将读取该属性的watcher(编译时根据模板和表达式创建的)添加到dep对象的依赖数组中
-当值改变时, 会触发set方法, set方法中会调用dep对象的通知watcher的方法通知每个watcher对象调用更新方法进行更新在初始化MVue实例时,对data中每个属性劫持监听,同时进行模板编译,指令解析,最后挂载到相应的DOM中。
- Vue在组件渲染过程中(即模板编译)遇见使用data中数据的表达式, 会为其建立一个watcher, 并把所对应的Watcher通过getter收集到Dep中,作为依赖。
- 当数据发生变化时,通过触发属性的setter调用Dep通知所对应的Watcher
- Watcher调用更新的回调函数来更新节点
数据绑定
一旦更新了data中的某个属性数据,所有界面上直接或间接使用此属性的节点都会更新。
数据劫持
所谓数据劫持,指的是在访问或者修改data中的某个属性数据时,通过代码拦截进行额外的操作或者修改返回结果。数据劫持是vue中用来实现数据绑定的一种技术,
基本思想:
通过defineProperty()来监视data中所有属性(任意层次)数据的变化,一旦变化就去更新界面
四个重要对象:
Observer 观察者
- 通过隐式递归调用实现所有层次属性的监视/劫持
- 给data中所有属性重新定义属性描述, 添加 setter / getter
- 为data中的每个属性创建对应的dep对象
Dep(Deppend)订阅器
- data中的每个属性(所有层次)都对应一个dep对象
- 创建的时机:
- 初始化data中各个属性时创建对应的dep对象
- 在data中的某个属性值被设置为新的对象时
- 对象的结构
Dep{
id, //每个dep都有一个唯一的id
subs //包含n个对应Watcher的数组(subscribe的简写)
}
- sub属性说明:
- 当一个Watcher被创建时,内部会将当前Watcher对象添加到对应dep对象的subs中
- 当此data属性的值发生改变时,所有subs中的Watcher都会收到更新的通知,调用update 从而最终更新对应的界面
Compile解析器
- 用来解析模板页面的对象(一个实例)
- 利用compile解析模板页面
- 插值语法和一般指令语法都会调用bind ,bind 中会创建 watcher
- 每解析一个表达式(非事件指令)都会创建一个对应的Watcer对象,并建立Watcher与Dep的关系
- compile与watcher关系:一对多关系
Watcher订阅者
初始化编译模板时遇见读取data中的属性时建立
-
模板中每个非时间指令或表达式都对应一个Watcher对象
-
监视当前表达式数据变化
-
对象的组成:
Watcher{
this.vm=vm, //vm对象
this.exp=exp, //对应的指令表达式
this.cb=cb, //当表达式所对应的数据发生改变的回调函数,即用于更新页面的回调
this.value=this.get(), //表达式当前的值
this.depIds={depId,dep} //表达式中各级属性所对应的dep对象的集合对象,即相关的n个dep对象
//属性名为dep的id,属性值为dep
}
总结:
dep与watcher的关系:多对多
-
dep通过data中属性的get()建立或初始化的解析模板中的表达式创建watcher对象时
-
一个data中的属性对应一个dep,一个dep可能包含多个watcher(模板中n个表达式使用相同属性 {{name}} v-text=“name”)
Dep ==> n个watcher
- 模板中一个非事件表达式(大括号表达式)对应一个watcher,一个watcher中可能包含多个dep(表达式中包含n个data属性 a.b.c)
watcher ==> n个dep
-
数据绑定使用到2个核心技术
-
defineProperty()
-
消息订阅与发布
Object.defineProperty的不足
虽然我们通过Object.defineProperty方法实现了对object数据的可观测,但是这个方法仅仅只能观测到object数据的取值及设置值,当我们向object数据里添加一对新的key/value或删除一对已有的key/value时,它是无法观测到的,导致当我们对object数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新,需要手动进行 Observe,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。
所以,在使用 Vue 给 data 中的数组或对象新增 / 删除属性时,需要使用 vm.$ set / vm.$delete才能保证新增 / 删除的属性也是响应式的。
Array型数据劫持
由于Object.defineproperty的不足,对于数组的添加和删除无法监测到,需要对Array型数据设计一套另外的变化侦测机制
Array型数据还是在getter中收集依赖。只是在Vue中创建了一个数组方法拦截器,它拦截在数组实例与Array.prototype之间,在拦截器内重写了操作数组的一些方法,并将这些方法赋值给了数据的 __ proto __ 上,因为原型链的机制,找到对应的方法就不会继续往上找原生的方法了,当数组实例使用操作数组方法时,其实使用的是拦截器中重写的方法,而不再使用Array.prototype上的原生方法。拦截器生效以后,当数组数据再发生变化时,我们就可以在拦截器中调用 dep 通知所对应的 watcher。编译方法中会对一些会增加索引的方法(push,unshift,splice)进行手动 observer。在 Array 中,用拦截器代替了setter
关于watcher和更新侦测粒度
-
Vue的更新是由于监视属性变化自动通知所有的Watcher,Watcher再调用自己的update进行更新
-
现在的watcher是组件级别的, 一个组件实例只有一个watcher, 而不是一个节点对应一个watcher, 为了避免创建的watcher过多内存占用过多。
Vue1.0的Watcher是节点级别的,这就导致粒度过细,每一个绑定都会有一个对应Watcher来观察状态的变化,这样就会使得开销过大。Vue2.0的Watcher是组件级别的,也就是说即便一个组件内有10个节点使用了某种状态,但其实也只有一个Watcher来观察这个状态的变化。当这个状态发生变化时,只是通知到组件,然后组件内部通过虚拟DOM去进行比对与渲染。
-
更新时只是调用当前组件的update更新,只diff当前组件, 对于子组件,会更新其props和listener, 并不会深入去操作子组件更新, 由于监视的作用, 子组件会自动触发更新。
变化检测相关的API原理
所有以vm.$开头的方法都是在Vue.js的原型上设置的。
vm.$watch(P31)
基本
用于监测传入的data, 变化后会调用传入的回调函数,** **回调函数调用时,会从参数得到新值和旧值。表达式只接受以点分隔的路径。对于更复杂的表达式,用一个函数取代。
- vm.$watch(expOrFn, callback, {deep, immediate})
- {string | Function} expOrFn,表示被监视的数据,可以是表达式/函数
- {Function | Object} callback,监视的数据变化后执行的,参数为(newval, oldval) => {}
- {Object} [options]
- {boolean} deep,为了发现对象内部值的变化,可以指定deep:true
- {boolean} immediate,立即以表达式当前值触发回调,可以指定immediate:true
- 返回值:{Function} unwatch,取消观察函数
注意: 在变更 (不是替换) 对象或数组时,旧值将与新值相同,因为它们的引用指向同一个对象/数组。Vue 不会保留变更之前值的副本。
原理
vm.$watch其实是对Watcher的一种封装,但vm. $watch中的参数deep和 immediate 是 watcher中所没有的。
具体实现如下:
- 首先new watcher(vm,exporFn,cb,options)实现基本的监视功能, 其中expOrFn可以为表达式或者函数, 当为函数时, 函数里涉及到的属性都会被监视, 即watcher对应了多个dep, 任意属性值改变都会触发回调函数
- 接着判断是否存在immediate参数,如果且值为true,会立即执行cb
- 使用depIds来判断如果当前watcher已经订阅了该Dep,则不会重复订阅
- 判断是否存在deep, 如果值为true的话, 会对传入的值进行递归的监视
强调的一点是,一定要在window.target = undefined之前去触发子值的收集依赖逻辑 如果在window.target = undefined之后去触发收集依赖的逻辑,那么其实当前的watcher并不会被收集到子值的依赖列表中,也就无法实现deep的功能。
traverse(value)- 判断是否是array, object以及是否冻结, 如果满足则直接返回, 并不会继续递归监视
- 将对象对应的dep的dep.id添加到一个Set中避免重复添加
- 递归对象或数组的每一个属性, 触发了getter 就将该watcher添加到对象的dep中
- 返回一个用于取消监视的函数, 本质上是调用了watcher的teardown()
Vue.prototype.$watch = function (exporFn,cb,options) {
const vm = this
options = options || {}
const watcher = new watcher(vm,exporFn,cb,options)
//判断是否存在immediate参数
if (options.immediate) {
cb.call(vm,watcher.value)
}
//返回一个用于取消监视的函数
return function unwatchFn () {
watcher.teardown( )
}
}
export default class watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm
//判断是否存在deep
if (options) {
this.deep = !!options.deep
}else {
this.deep = false
}
//dep与Watcher是多对多的关系,dep增加依赖知道依赖了哪些watcher,同样watcher也要知道自己都订阅过哪些Dep
this.deps =[]
this.depIds = new Set()
//判断exporFn是函数还是表达式
/*
如果 exporFn是函数,则直接将它赋值给getter;如果不是函数,再使用parsePath函数来读取keypath中的数据。
*/
if (typeof exporFn === "function " ) {
this.getter = exporFn
} else {
this.getter = parsePath(expOrFn)
}
this.cb = cb
this.value = this.get()
}
//将对象对应的dep的dep.id添加到一个Set中避免重复添加
addDep (dep) {
const id = dep.id
if (!this.depIds.has(id)) {
this.depIds.add(id)
this.deps.push(dep)
dep.addSub(this)
}
}
//一定要在window.target = undefined之前去触发子值的收集依赖逻辑
get () {
window.target = this
let value = this.getter.call(vm,vm)
if (this.deep) {
traverse(value)
}
window.target = undefined
return value
}
}
const seen0bjects = new Set()
export function traverse (val) {
_traverse(val,seenObjects)
seenobjects.clear()
}
function _traverse (val,seen) {
let i, keys
const isA = Array .isArray(val)
//判断是否是array, object以及是否冻结, 如果满足则直接返回, 并不会继续递归监视
if (( !isA && !isobject(val)) || object.isFrozen(val)) {
return
}
//将对象对应的dep的dep.id添加到一个Set中避免重复添加
if (val.__ob_) {
const depId = val._ob_.dep.id
if ( seen.has( depId)) {
return
}
seen.add( depId)
}
//递归对象或数组的每一个属性, 触发了getter 就将该watcher添加到对象的dep中
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
}else {
keys = object.keys( val)i = keys.length
while (i--) _traverse(val[ keys[i]], seen)
}
}
vm.$set(P38)
基本
在 object 上设置一个属性,如果 object是响应式的,Vue.js 会保证属性被创建后也是响应式的,并且触发视图更新。
vm.$set在原型上设置的,但具体实现其实是在observer中抛出的set方法。
vm.$set( target,key,value )
-
参数:
{object | Array} target
{string | number} propertyName/index
{any} value -
返回值:设置的值
原理
Array:
- 判断 key是否合法,先原数组的length变化
target. length = Math.max(target.length,key)
- 通过 splice将原数组对应的值修改。
使用splice时数组拦截器会侦测到target 的变化,并自动把val转换成响应式的 - 最后返回val即可
export function set (target,key , val) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target. length = Math.max(target.length,key)
target.splice( key, 1, val)
return val
}
}
Object:
- 非响应式数据:直接修改对象的值target[key] = val
- 响应式数据:
- key存在target中
由于key已经存在于target 中,说明这个key已经被侦测变化,那么直接修改key属性。修改数据的动作会被Vue.js侦测到,所以数据发生变化后,会自动向依赖发送通知。
- key存在target中
if (key in target && !(key in Object.prototype)) {
target[ key] = val
return val
}
- 处理新增属性
- 使用_isVue/ob.vmCount来判断target不属于Vue.js实例或Vue.js实例的跟数据对象
什么是根数据?this.$data就是根数据
- 使用defineReactive重新设置对象的getter和setter,同时调用该对象的dep的notify方法通知watcher更新
完整代码:
export function set (target, key, val) {
if (Array.isArray(target) && isvalidArrayIndex( key ) ) {
target.length = Math.max(target. length, key )
target.splice( key , 1, val)
return val
}
if ( key in target && ! (key in object.prototype)) {
target[ key] = val
return val
}
const ob = target._ob_
//判断target不属于Vue.js实例或Vue.js实例的跟数据对象
if (target._isVue || (ob && ob.vmCount)){
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data'+
'at runtime - declare it upfront in the data option.'
)
return val
}
//判断是否为响应式数据
if (!ob) {
target[key] = val
return val
}
//将新增属性转为响应式
defineReactive(ob.value,key, val)
ob.dep.notify()
return val
}
vm.$delete
删除数据中的某个属性。如果对象是响应式的,需要确保删除能触发更新视图。
基本
vm.$delete( target, propertyName/index )
- 参数:
- {Object | Array} target
- {string | number} propertyName/index
原理
Array:只需使用splice将参数key所指定的索引位置的元素删除即可
Object:
- 与vm.$set一样,vm. $delete也不可以在Vue.js实例或Vue.js实例的根数据对象上使用
- 如果删除的这个key不是target自身的属性,就什么都不做,直接退出程序执行
- 如果删除的这个key在target 中根本不存在,那么其实并不需要进行删除操作,也不需要向依赖发送通知
- 判断target是否为响应式数据,只有响应式数据才需要发送通知,非响应式数据只需要执行删除操作即可
export function del (target,key) {
if (Array.isArray(target) && isValidArrayIndex ( key)) {
target.splice(key,1)
return
}
const ob = target._ob_//新增
if (target._isVue || (ob && ob. vmCount)) {
process.env. NODE_ENV !== ' production' && warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
' just set it to nul1."
return
}
//如果key不是target自身的属性,则终止程序继续执行
if(!hasOwn(target,key)) {
return
}
//如果ob不存在,则直接终止程序
if(!ob) {
return
}
delete target[key]
ob.dep.notify()
}
如果大家有什么意见或建议希望大家在评论区多多交流,谢谢😊