VUE源码解析之变化侦测(一)

 
  变化侦测主要分为两种类型,一种是“推”(push),另一种是“拉”(pull)。
  Angular和React中的变化侦测都属于“拉”,这就是说当状态发生变化时,它不知道那个状态变了,只知道状态有可能变,然后会发送一个信号告诉框架,框架内部接收到信号后,会进行一个暴力比对来找出哪些DOM节点需要重新渲染。这在Angular中是脏数据检查的流程,在React中使用的是虚拟DOM。
  而Vue.js的变化侦测属于推。当状态发生变化时,Vue.js立刻就知道哪发生了变化。“拉”的粒度是较大的,而“推”需要绑定的依赖相对较多,所以粒度较小,开销也相对较大。所以vue2之后引用虚拟DOM,将粒度调整为中等粒度。

 

Vue侦测系统的核心

Vue侦测系统的三个核心:Observer、Dep、Watcher

Observe:遍历data中的属性,使用object.defineProperty的get/set方法对其进行数据劫持。
Dep:每个属性拥有自己的消息订阅器Dep,用于存放所有订阅了该属性的依赖。
Watcher:中介对象。通过Dep实现对相应属性的监听,监听到结果后,会通知其它地方进行相应操作。

这三个是主要核心,看不懂没关系,下面再慢慢道来。
 

如何追踪变化

很显然,JS给我们提供了两个方法,一是使用Object.defineProperty的get/set方法,二是使用ES6中的proxy。总所周知VUE3.0采用了proxy实现数据侦测,从18到20年了vue3还是没正式发布,所以我们这先讨论使用object.defineproperty实现方式,其原理也是一样的。

function defineReactive(data, key, val) {
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            return val
        },
        set: function (newVal) {
            if (val === newVal) {
                return 
            }
            val = newVal
        }
    })
}

  通过defineReactive函数将object.defineproperty进行封装,通过传递data、key和val来对传递的data对象的key属性进行变化追踪。当data的key数据读取时,get函数则触发,当给data的key赋值时,set函数则触发。
  现在我们知道如何检测一个属性发生变化了,当没什么用啊,我们得知道哪些地方用到这属性,当变化的时候告诉它啊!所以我们要收集依赖。
 

依赖收集------Dep

  我们需要把用到的依赖都收集起来,然后等属性发生变化时,把之前收集好的依赖遍历触发一遍就好了。在这里,我们定义了一个数组Dep,用来保存依赖对象。假设依赖是一个函数,保存在window.target上,然后我们改造下之前的defineReactive函数。

function defineReactive(data, key, val) {
    let dep = [] //收集的依赖
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.push(window.target)  //添加依赖
            return val
        },
        set: function (newVal) {
            if (val === newVal) {
                return
            }
            for (let i = 0; i < dep.length; i++){  //遍历触发依赖事件
                dep[i](newVal,val)
            }
            val = newVal
        }
    })
}

  这里我们可以看到,当这个依赖读取该属性时,会自动触发get方法,将其添加到依赖数组里面,当该属性发生变化时,会自动触发set方法遍历dep数组从而通知每个订阅者。
这样写耦合度较高,我们将Dep单独封装成一个类:

export default class Dep {
    constructor() {
        this.subs = []
    }
    addSub(sub) {
        this.subs.push(sub)
    }
    removeSub(sub) {
        this.removeSub(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)
        }
    }
}

function defineReactive(data, key, val) {
    let dep = new Dep() //实例化Dep
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.depend() //添加依赖/订阅者
            return val
        },
        set: function (newVal) {
            if (val === newVal) {
                return
            }
            dep.notify() //遍历触发依赖事件
            val = newVal
        }
    })
}

  从上面我们知道我们假设收集的依赖是window.target,但这个window.target到底是什么?watcher其实是一个中介角色,数据发生变化时,通知它,它再通知其他地方。
 

中介------Watcher

  我们要知道用到数据的地方,而使用这个数据的地方很多,而且类型不同,即有可能是模板,也有可能是用户写的watch,所以我们需要抽象出一个能集中处理这些情况的类。我们在收集依赖的时候只收集这个封装好的类,通知也只通知它一个,接着,它再负责通知其他地方。
看一个经典的使用方式:

//keypath
vm.$watch('a.b.c',function(newVal,oldVal){
   //触发事件
})

当a.b.c属性发生变化的时候,触发第二参数中的函数。结合上面我们我可以定义一个watcher方法,我们把这个watcher实例添加到data.a.b.c属性的Dep中就行了。

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.getter()
    }
    get() {
        window.target = this
        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)
    }

}

  因为在get方法中先把window.target设置为this,也就是当前watcher实例,然后执行this.getter读取一下data.a.b.c的值,这就会触发defineReactive中的get方法,将this添加到Dep中,完成依赖收集。之后再将window.target初始化为undefined。
  依赖注入到Dep之后,每当触发data.a.b的值发生变化时,就会让依赖列表中所有的依赖循环触发update方法,也就是Watcher中的update方法。而update方法执行参数中的回调函数,将value和oldValue传到参数中。那这里有个问题:当我们访问侦测属性的时候都会自动加入Dep,当多次访问后,Dep里面不就有好几个相同的Watcher?这个问题留到后面再聊。其实处理起来也很简单,只是作下判断即可。
 

递归遍历key------Observer

  在上面我们已经实现了变化侦测的功能,现在只需要封装一个Observer类,递归遍历数据中所有的属性(包括子属性)就好了。这个类的作用是将一个数据中所有属性都转换为getter/setter的形式,然后去追踪它们的变化:


//Object类回附加到每一个被侦测的object上
//一旦被附加上,Observer会将object的所有属性转换为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()
        }
   }) 
}

  通过定义Observer类将一个正常的object转化为被侦测的object。判断它的类型,如果是object继续递归遍历。在这里,我们只介绍了object的遍历,数组的遍历之后章节再聊!

 

关于Object的问题

  前面我们知道object类型的变化侦测原理,但是其只能侦测到属性的修改,有些语法是侦测不到的,如下:

var vm = new VTTCue({
    el: '#el',
    template: '#demo-template',
    methods: {
        action1() {
            this.obj.age = 12
        },
        action1() {
            delete this.obj.name
        }
    },
    data: {
        obj: {
            name:"binguo"
        }
    }
})

  我们新增或者删除某个属性的时候,是不会被侦测到的。这也是object.definePorperty的缺陷,它只能侦测到属性是否被修改,无法追到新增或者删除属性。
  为了弥补这缺陷,vue.js也给我们提供了两个API:vm.$setvm.$delete

总结

1.Data通过Observer遍历每个属性通过getter/setter方法进行数据的侦听。
2.当外界通过Watcher读取数据时,会触发getter从而将watcher添加到依赖中。
3.当数据变化时,会触发setter,从而向Dep中的依赖(Watcher)发送通知。
4.Watcher接收到通知后,会向外界发送通知,外界进行相应操作。

VUE源码解析之变化侦测(一)_第1张图片

注:本文主要参考自《深入浅出Vue.js》

你可能感兴趣的:(vue)