vue.js的响应式原理,理解为什么修改数据视图会自动更新

5PU4JO.md.jpg

如何追踪变化

在js中,有两种方法可以侦测到数据的变化:Object.defineProperty和Es6的Proxy。这里讨论的是vue2的响应式原理,所以就说Object.defineProperty,在vue3中使用的是Proxy,还没有开始看呢。
那么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
        }
      })
    }

上面将Object.defineProperty进行了封装,封装好之后,每当data中值被读取就会触发get;每当设置data中的数据时,set函数就会触发

那么在哪里收集呢

把用到数据的地方收集起来,等到数据变化的时候再将收集好的依赖循环触发一次。(在getter中收集依赖,在setter中触发依赖)

依赖被收集在哪里

在每个key上面设置一个数组,专门去收集使用到key的地方,即依赖。然后这个数组我们取名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 in dep) {
            dep[i](newVal, val)
          }
          val = newVal
        }
      })
    }

然后将dep封装成一个Dep类,专门管理依赖:

    export default class Dep {
      constructor() {
        this.subs = []
      }

      addSub(sub) {
        this.subs.push(sub)
      }

      removeSub(sub) {
        remove(this.subs, sub)
      }

      depend() {
        if(window.target) {
          this.addSub(window.target)
        }
      }

      notify() {
        const subs = this.subs.slice()
        for(let i in subs) {
          subs[i].update()
        }
      }
    }

    function remove(arr, item) {
      if(arr.length) {
        const index = arr.indexOf(item)
        if(index > -1) {
          return arr.splice(index, 1)
        }
      }
    }

依赖是什么?

上面说,依赖是window.target,那么它又是什么呢?我们究竟要收集什么东西?
收集谁,就是当属性发生变化时,通知谁。我们要通知使用数据的地方,但是使用数据的地方有很多,类型也不同,可能是模板也可以是其他地方,所以我们要封装一个能处理所有情况的类,就叫做Watcher。所以,依赖就是Watcher,我们收集的也是Watcher。

什么是Watcher

Watcher是一个中间角色,数据发生变化时通知它,它再通知其他地方。
关于Watcher,先来看一个经典的使用方式:

vm.$watch('a.b.c', function(newVal, oldVal) {
  //do someting
})

当data.a.b.c发生变化时,触发第二个参数中的函数。
那么怎么去实现这个功能呢?首先要把这个watcher实例添加到data.a.b.c的Dep中去,然后当它改变的时候,通知watcher,watcher再执行里面的回调函数就行了。
那么实际代码如下:

    export default class Watcher {
      constructor (vm, expOrFn, cb) {
        this.vm = vm
        //执行一下this.getter就能读取到data.a.b.c的内容
        this.getter = parsePath(expOrFn) //读取字符串的keyPath
        this.cb = cb
      }

      get() {
        window.target = this //将window.target设成当前watcher实例
        let value = this.getter.call(this.vm, this.vm) //触发getter,将window.target添加到dep中
        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.$]/
    function parsePath (path) {
      if(bailRE.test(path)) {
        return
      }
      const segments = path.split('.')
      return function (obj) {
        for(let i in segments) {
          if(!obj) return
          obj = obj[segments]
        }
        return obj
      }
    }

4rrbfU.md.jpg

递归侦测所有key

上面已经能实现变化侦测的功能了,但是只能侦测数据中的一个属性,我们希望能把数据中的所有属性都侦测到,所以需要封装一个Observer类。这个类的作用就是将数据中的所有属性都转换成getter/setter的形式,然后去追踪它们的变化。

    export class Observer {
      constructor (value) {
        this.value = value
        if(!Array.isArray(value)) { //判断是不是数组,数组需要单独进行特殊处理
          this.walk(value)
        }
      }

      //walk会将每一个属性转换成getter/setter,并且只有在数据类型是对象才会调用
      walk(obj) {
        const keys = Object.keys(obj)
        for(let i in keys) {
          defineReactive(obj, keys[i], obj[keys[i]])
        }
      }
    }

然后再将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中的回调函数去通知所有的依赖
        }
      })
    }

现在就大功告成了!!!

关于Object的添加和删除问题

有一些语法中即使数据发生了变化,vue.js也追踪不到。
比如,向object添加属性:

    var vm = new Vue({
      el: '#el',
      template: '#demo-template',
      methods: {
        action() {
          this.obj.name = 'berwin' //不能实时监听
        }
      },
      data: {
        obj: {}
      }
    })

再比如,删除一个属性:

    var vm = new Vue({
      el: '#el',
      template: '#demo-template',
      methods: {
        action() {
          delete this.obj.name  //不能实时监听
        }
      },
      data: {
        obj: {name: 'sifan'}
      }
    })

Object.defineProperty只能追踪一个数据是否被修改,无法追踪新增属性和删除属性,所以才会出现上面的问题。
但是也不用担心,vue.js提供了两个API——vm.\(set和vm.\)delete,之后再更它们两个的原理。

posted @ 2021-09-25 10:46  卿六  阅读(632)  评论(0编辑  收藏  举报