vue2双向数据绑定原理(defineProperty、Observer、Dep、Watcher)

45 阅读4分钟

侦测对象的变化

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
      }
    })
}

data是一个对象,从data 的key 中读取数据时,get 函数被触发;每当往data 的key 中设置数据时,set 函数被触发。

上面的函数对一个key值进行操作,下面的Observer则可以对所有key值进行操作

Observer

作用是将一个数据内的所有属性(包括子属性)都转换成getter/setter的形式,然后去追踪它们的变化

export class Observer{
    constructor(value){
        this.value = value
        if(!Array.isArray(value)){
            this.walk(value)
        }
    }

    walk(obj){
        const keys = Object.keys(obj)
        for(let i = 0;i< keys;k++){
            defineReactive(obj,keys[i],obj[keys[i]])
    }
}

defineReactive改为

function defineReactive (data, key, val) {
    //新增
    if(typeof val === 'object'){
        new Observer(val)
    }
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get: function () {
        return val
      },
      set: function (newVal) {
        if(val === newVal){
          return
        }
        val = newVal
      }
    })
}

上述新增,是对每个对象的值进行递归进行key的getter与setter的绑定。

这种方法,在后续的给对象添加或删除属性,无法追踪到后续添加或删除属性的变化,Vue.js提供了两个API——vm.setvm.set 与vm.delete来解决这个问题

收集依赖Dep

在getter中收集依赖,在setter中触发依赖 。

为了收集每个key的依赖,将引入一个数组,这里定义为dep,假设每个依赖是一个函数,则每个key都对应一个函数依赖,所有依赖都收集到dep里面之后。每一次key所对应的值改变,则去触发dep里面的依赖。所以上方程序则可以改造为:

function defineReactive (data, key, val) {
    if(typeof val === 'object'){
        new Observer(val)
    }
    let dep = [] //新增
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get: function () {
        dep.push(依赖)  //新增
        return val
      },
      set: function (newVal) {
        if(val === newVal){
          return
        }
        //新增
        for(let i = 0; i < dep.length; i++){
            dep[i](newVal,val)
        }
        val = newVal
      }
    })
}

对于上方的代码有一个专门收集依赖的,可以将其封装为专门对依赖处理的一个类,如下代码:

export default class Dep {
    constructor(){
        this.subs = []//用来存储依赖
    }
    //收集依赖
    depend(sub){
        this.subs.push(sub)
    }  
    
    //执行依赖
    notify(){
        const subs = this.subs.slice()
        for(let i = 0;i < subs.length;i++){
            subs[i].update()//执行依赖
        }
    }
}

改造defineReactive函数

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() //修改,触发依赖
      }
    })
}

上方的依赖是谁,即属性发生改变之后,通知谁。通知下方的watcher

Watcher

数据发生变化时通知它,然后它再通知其他地方。

所谓的依赖,其实就是Watcher 。只有Watcher 触发的getter才会收集依赖,哪个Watcher 触发了getter,就把哪个Watcher 收集到Dep 中。当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。

Watcher 的原理是先把自己设置到全局唯一的指定位置(例如window.target ),然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取当前正在读取数据的Watcher ,并把这个Watcher 收集到Dep 中去。通过这样的方式,Watcher 可以主动去订阅任意一个数据的变化。

export class Watcher{
    constructor(vm,expOrFn,cb){
        this.vm = vm,
        this.expOrFn = expOrFn
        this.cb = cb
        this.get()
    }
    //为了收集这个watcher===this,读取一遍这个值this.vm[this.expOrFn],将会触发## Object.defineProperty里面的get函数,在到get函数里面将这个watcher收集Dep中
    get(){
        Dep.target = this
        this.vm[this.expOrFn]
        Dep.target = null
    }
    //执行,当前这个回调函数
    update(){
        const val = this.vm[this.expOrFn]
        this.cb.call(this.vm,val)
    }
}

改造defineReactive函数

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.target&&dep.depend(Dep.target)  //修改,收集Watcher
        return val
      },
      set: function (newVal) {
        if(val === newVal){
          return
        }
        val = newVal
        dep.notify() //修改,触发收集的Watcher依赖
      }
    })
}

Data 、Observer 、Dep 和Watcher 之间的关系。

image.png

Data 通过Observer 转换成了getter/setter的形式来追踪变化。

当外界通过Watcher 读取数据时,会触发getter从而将Watcher 添加到依赖中。

当数据发生了变化时,会触发setter,从而向Dep中的依赖(Watcher )发送通知。

Watcher 接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。

Array 的变化侦测

数组无法用defineProperty进行劫持

解决方法:用自定的方法覆盖原型方法。可以用一个拦截器覆盖Array.prototype 。之后,每当使用Array 原型上的方法操作数组时,其实执行的都是拦截器中提供的方法

image.png Array 原型中可以改变数组自身内容的方法有7个,分别是push 、pop 、shift、unshift 、splice 、sort 和reverse 。

//获取数组原型
const arrayProto = Array.prototype
//复制一个数组原型出来
export const arrayMethods = Object.create(arrayProto)
//遍历数组需要替换的方法
['push','pop','shift','unshift','splice','sort','reverse'].forEach(function(method){=
    Object.defineProperty(arrayMethods,method,{
        value:function muator(...args){
            //这里可以做相应拦截之后的操作,然后讲传进来的值交给原生数组的方法进行处理
            return arrayProto[method].apply(this,args)
        },
        enumerable:false,
        writable:true,
        configurable:true
    })
})

改写Observer,使其对数组进行相应的操作


export class Observer{
    constructor(value){
        this.value = value
        //修改
        if(Array.isArray(value)){
            //用自己封装好的拦截数组的方法替换原有数组原型上的方法
            value.__proto__ = arrayMethods
        }else{
            this.walk(value)
        }
    }

    walk(obj){
        const keys = Object.keys(obj)
        for(let i = 0;i< keys;k++){
            defineReactive(obj,keys[i],obj[keys[i]])
    }
}