变化侦测篇
1. 如何理解数据驱动视图?
其实当作v-model的单向传递就好了,改变data中的值与之对应的页面也变化。
2. vue如何知道data变化了呢?
在vue2.0中,对data的监听是用Object.defineProperty()来实现的,可以简单的理解为vue先把dom节点放入虚拟dom中,然后遍历所有的节点并一个个的赋予defineProperty()的get和set方法进行的监听操作
let car ={
'brand':'BWM',
'price':30000
}
Object.defineProperty(car,'price',{
enumerable:true,
configurable:true,
get(){
console.log('属性被读取了')
return val
},
set(newVal){
console.log('属性被修改了')
val = newVal
console.log(newVal)
console.log(car.price)
}
})
car.price = 50000
根据原例子,当price属性被使用的时候就会触发get函数,打印属性被读取了,当重新设置price的值的时候,set函数就会执行并把值修改
于是vue源码中src/core/observer/index.js位置中定义了一个Oberser的类,对数组和对象进行分别的处理,其中对object进行了递归遍历,确保每个data都种下了监听器defineProperty
class Observer {
constructor (value) {
this.value = value //事实上是vue中的data里面的值
def(value,'__ob__',this) //def是一个函数,为value中种下了监听器defineProperty做个记录
if (Array.isArray(value)) {
// 当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])
}
}
}
function defineReactive (obj,key,val) {
// 如果只传了obj和key两个参数,那么val = obj[key]
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val) //当对象里面还是对象的时候就重新生成Observer
}
//vue实现监听的核心
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
if(val === newVal){
return
}
console.log(`${key}属性被修改了`);
val = newVal;
}
})
}
这个时候再看回car中的两个属性,只需要把car作为observer的子类就可以让car的元素被监听
let car = new Observer({
'brand':'BMW',
'price':3000
})
3. 依赖收集
上面的步骤让我们学会了如何监听data中的属性了,那监听后如何让视图更新?因为每次更新视图都会触发重绘所以只能找到对应的用到了监听的数据的视图进行遍历
所以依赖收集的意思是:找到data更改对应的视图,并对视图进行修改
所以我们找回之前的代码段,因为get是在监听的属性被读取的时候触发的,于是我们在get中定义一个方法用于收集所要更新的视图,然后在set中定义一个方法用于通知视图变化
//vue实现监听的核心
//*********************比上面添加了这一行代码***********************
const dep = new Dep()
//*********************比上面添加了这一行代码***********************
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
//*********************比上面添加了这一行代码***********************
dep.depend() //dep是一个类,然后这里调用了类的depend方法进行收集,下文会指出
//*********************比上面添加了这一行代码***********************
console.log(`${key}属性被读取了`);
return val;
},
set(newVal){
//*********************比上面添加了这一行代码***********************
dep.notify() //dep是一个类,然后这里调用了类的notify方法进行收集,下文会指出
//*********************比上面添加了这一行代码***********************
if(val === newVal){
return
}
console.log(`${key}属性被修改了`);
val = newVal;
}
})
这里可以知道当get触发后可以移步到dep类中看一下,下面先讲一下依赖(也就是视图)是如何收集的
export default class Dep {
constructor () {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
// 删除一个依赖
removeSub (sub) {
remove(this.subs, sub)
}
// 添加一个依赖
//****************这里这里,上面的代码到这里~~**********************
depend () {
if (window.target) { //这里的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()
}
}
}
/**
* Remove an item from an array
*/
export function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
这里的window.target我一开始看也懵了,这个target从哪里出来的,后面发现他藏在待会要讲的watch类中,事实上window.target实际上指的是watch这个类本身的实例,也就是说window.target就是我们要改变的视图,到这个部分依赖的收集就已经完成了,然后就是如何通知视图去改变,就是用在set中的dep.notify()
notify () {
const subs = this.subs.slice() //subs就是视图监听器的数组,也就是我们收集的依赖
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() //这个update方法就是下面代码块中watch类里面的方法
//因为subs中的元素就是视图监听器,也就是Watch的实例,
//所以可以直接使用update的方法,找不到Watch类和Dep类之间的关联的小伙伴可以注意一下,
//这里就是和watch相连的桥梁
}
}
在上面这个notify中,我们把收集到的依赖调用update方法,那么就有请我们最最重要的嘉宾。Watch登场吧
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn) //下面可以找到这个方法
this.value = this.get()
}
get () {
window.target = this; //就是在这里把target定义为当前的Watch的
const vm = this.vm
let value = this.getter.call(vm, vm) //事实上就是获取到了data中修改的值
window.target = undefined; //关于这个undefined下面会有说明
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
/**
* Parse simple path.
* 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
* 例如:
* data = {a:{b:{c:2}}}
* parsePath('a.b.c')(data) // 2
*/
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[segments[i]]
}
return obj
}
}
一开始我是比较绕的,因为文章并不是按代码的执行顺序进行叙述的,所以当我看到在get()方法中把window.target设为this随后又设为undifined的操作十分的不理解
window.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm) //这里是关键
window.target = undefined;
但是要把这个讲清楚得先看到设为undifined之前有一个this.getter,这个参数是调用了parsePath(expOrFn)这个方法,如果你明白了这个方法的作用就很容易知道为什么要设为undifined了
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[segments[i]]
}
return obj //获取到了最深的一个obj
}
其实就是把vue中的data中的值获取到最后的一个对象,这种递归方式用reduce函数也可以做到哦 如果还没有办法理解,可以认为这个函数执行结果得到的值就是我们改变的data值,也就是数据驱动视图中的数据这一环,所以这个代码块中的value事实上就是我们变动的数据!所以这个地方触发了Object.defineProperty中的get
window.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm) //是我们变动的数据,触发了Object.defineProperty中的get
window.target = undefined;
然后在get中把window.target添加给了subs,也就是依赖收集器(这么高深的词来着= =不就是个叫subs的数组。。)
所以整理一下就可以知道,事实上完整的触发顺序应该是这样的
用户的data数据会作为Watch类的实例出现!然后Watch是一个构造函数嘛,就会先执行constructor中的代码,然后就会触发watch类的get函数,get函数就会读取到用户所改变的data值,这个时候就会触发Object.defineProperty中的get,然后就成功的把要改变的Watch放进了subs数组里
把window.target设为undifined的原因是不要重复的添加实例到subs,添加完就可以把这个实例抹去
这个时候事实上就完整了监听方面的步骤了,随后用户如果改变了data中的数据,就会触发setter函数随之执行到watch的upadte方法上进行更新啦~
不足
vue这种实现监听的方式事实上已经很完善了,但是可以看到对object的增加和删除是没有办法监听到的,因为vue只是循环遍历了已存在的data进行监听,当然vue也做了很多其他的方式来弥补这样的不足,像是$set方法,还有watch中的deep(虽然也没办法知道具体变了什么,小声逼逼)如果是初级前端需要面试的小伙伴建议自己码一下理解透彻,别只会说运用了Object.defineProperty()进行监听啦