vue中对象的双向数据绑定
1、变化侦测
vue.js会自动通过状态生成DOM,并显示在页面上,这个过程叫渲染。vue.js渲染过程是声明式的,我们通过模板来描述状态与DOM之间的映射关系。通常,在运行时应用内部会不断发生变化,需要不停的渲染,此时需要变化侦测来确定状态中发生了什么变化。
变化侦测一种是“推”,一种是“拉”。Angular和React中的变化侦测都属于“拉”,即当状态发生变化时,它不知道具体哪个状态变了,只知道有状态变化,然后会发送一个信号告诉框架,框架内部收到信号后,会进行一个暴力比对来找出哪些DOM节点需要重新渲染。这在Angular中是脏检查的流程,在React中使用的是虚拟DOM。而Vue.js的变化侦测属于“推”。一个状态绑定着很多依赖,每个依赖表示一个具体的DOM节点,当状态发生变化时,向这个状态的所有依赖发送通知,让它进行DOM更新操作。从VUE2.0开始,引入了虚拟DOM,一个状态所绑定的依赖不再是具体的DOM节点,而是一个组件。这样状态变化后,会通知到组件,组件内部在使用虚拟DOM进行比对,从而大大降低依赖的数量,降低依赖追踪所消耗的内存。
2、Object的变化侦测
侦测对象的变化有两种方法:Object.defineProperty和ES6的proxy,这里只讨论使用Object.defineProperty的情况。
2.1 如何追踪变化?
我们首先使用一个函数defineReactive来对Object.defineProperty进行封装。其作用是定义一个响应式数据,即在函数中进行变化追踪,每当从data的key中读取数据时,get函数被触发;每当往data的key中设置数据时,set函数被触发。
2.2 如何收集依赖?
只是对Object.defineProperty进行封装,其实没有什么用处,真正有用的是收集依赖。我们之所以要观察数据,其目的就是当变量的属性发生变化时,可以通知那些使用到该属性的地方。
总之: 在get中收集依赖,在set中触发依赖。
例如:
<template>
<h1>{{name}}</h1>
</template>
该模板中使用了变量name,所以当它发生变化时,要向使用了它的地方发送通知。所以首先要收集依赖,即把用到变量name的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍就好了。
2.3 依赖收集在哪里?
我们在getter中收集依赖,依赖收集在哪里?假设依赖是一个函数,保存在window.target上,我们可以新增一个dep数组,用来存储被收集的依赖。然后在set被触发时,循环dep以触发收集到的依赖。我们把依赖收集的代码封装成一个Dep类,专门管理依赖。使用这个类,可以收集依赖、删除依赖或者向依赖发送通知。
export default class Dep{
constructor(){
this.subs = []
}
addSub(sub){
this.subs.push(sub)
}
removeSub(){
remove(this.subs, sub)
}
depend(){
if(window.target){
this.addSub(window.target)
}
}
notify(){
const subs = this.subs.slice()
for(let i=0,l=subs.length; i<l;i++){
subs[i].update()
}
}
}
function remove(arr, item){
if(arr.length){
const index = arr.indexOf(item)
if(index > -1){
return arr.splice(index, 1)
}
}
}
之后再改造下defineReactive:
function defineReactive(data, key, val){
let dep = new Dep() // 修改
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function(){
dep.depend() // 修改
return val
},
set: function(newVal){
if(val === newVal){
return
}
val = newVal
dep.notify()
}
})
}
2.4 依赖是谁?
在上面的代码中我们收集的依赖是window.target,它到底是什么?
收集谁也就是当属性变化的时候通知谁?我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知只通知它一个。接着,他在负责通知其他地方,即watcher。
2.5 什么是watcher?
watcher是一个中介角色,数据发生变化时通知它,然后再通知其他地方。
关于watcher,先看下例:
vm.$watch('a.b.c', function(newVal, oldVal){
// 做点什么
})
// 当data.a.b.c的值发生变化时,触发第二个参数中的函数
上面代码表示当data.a.b.c的值发生变化时,触发第二个参数中的函数,怎么实现呢?可以把这个watcher实例添加到data.a.b.c属性的Dep中就可以。然后,当data.a.b.c的值发生变化时,通知watcher。接着Watcher在执行参数中的这个回调函数。代码如下:
export default class Watcher{
constructor(vm, expOrFn, cb){
this.vm = vm
// 执行this.getter()就可以读取data.a.b.c的内容
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
get(){
window.target = this
// 修改this指向,并将this.vm作为参数传入getter
let value = this.getter.call(this.vm, this.vm)
window.target = undefined
return value
}
update(){
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
const bailRE = /[^\w.$]/
export function parsePath(path){
if(bailRE.test(path)){
return
}
// 先按.来分割成数组,然后一层层遍历读取数据
const segments = path.split('.')
return function(obj){
for(let i=0; i<segments.length; i++){
if(!obj) return
obj = obj[segements[i]]
}
return obj
}
}
这段代码可以将data.a.b.c主动添加到Dep中。首先在get方法中把window.target设置成this,也就是当前的watcher实例,然后再读取data.a.b.c的值,此时触发getter。触发getter就会触发收集依赖,会从window.target中读取一个依赖并添加到Dep中。依赖注入之后,每当data.a.b.c的值发生变化,就会让依赖列表中所有的依赖循环执行update方法,也就是Watcher中的update方法。update方法执行参数中的回调函数将value和oldValue传到参数中。
2.6 递归侦测所有的key
目前为止我们已经实现了变化侦测的功能,但是只能侦测数据中的某一个属性,我们希望把数据中的所有属性都侦测到,所以要封装一个Observer类。这个类的作用就是将一个数据内的所有属性(包括子属性)都侦测到,所以要封装一个Observer类。这个类的作用就是将一个数据内的所有属性(包括子属性)都转换成getter/setter形式,然后追踪他们的变化:
export class Observer{
constructor(value){
this.value = value
if(!Array.isArray(value)){
this.walk(value)
}
}
// walk会将每一个属性都转换成getter/setter的形式来侦测变化
// 这个方法只有在数据类型为 Object时被调用
walk(obj){
const keys = Object.keys(obj)
for(let i=0; i<keys.length; i++){
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
function defineReactive(data, key, val){
if(typeof val === 'object'){ // 修改
new Observer(val)
}
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function(){
dep.depend()
return val
},
set: function(newVal){
if(val === newVal){
return
}
val = newVal
dep.notify()
}
})
}
2.7 关于Object的问题
Vue.js通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性,这是因为在ES6之前没有提供元编程的能力,无法侦测到一个新属性被添加到了对象中,也无法侦测到一个属性从对象中删除了。
2.8 总结
下图给出了Data、Observer、Dep和Watcher之间的关系。

Data通过Observer转换成getter/setter的形式来追踪变化。
当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
当数据发生变化时,会触发setter,循环遍历Dep中的依赖(Watcher),Watcher接收到通知后,会向外界发送通知,变化通知到外界后会触发视图更新。

浙公网安备 33010602011771号