vue核心原理-监测数据变化

我们实际开发中发现,在 data 中定义的所有数据,后续无论是在 template 中使用,还是在 methods 中使用,都能随着数据的变化而变化。为了理解这其中的原理,研究源码后整理出这篇文章,欢迎大家及时指正。

第一步:数据注册监听

vue 2.x 版本使用的是 Object.defineProperty 详细API文档见 Object.defineProperty

用于绑定 Object 类型数据,比如定义一个 person :

let person = {
    name: 'usm',
    age: 12
}
复制代码

现在希望 personnameage 发生改变时,可以触发一些操作,就可以通过 Object.defineProperty 实现:

Object.defineProperty(person, 'name', {
    enumerable: true,
    configurable: true,
    get() {
        console.log('get name's value');
    },
    set(val) {
        console.log(`set value ${val}`);
    }
});

person.name             // get name's value
person.name = 'new'     // set value new
复制代码

其中 enumerable 属性表示此属性设置为可枚举, configurable 表示此属性可被修改/删除。

至此, person 对象中的 name 属性发生读/写操作时,都可以被监听到,并执行对应的代码。

回到源码, vue 中实现了一个 Observer 对象,用来对 vue 实例中的每个数据添加监听。

class Observer {
  constructor (value) {
    this.value = value
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  // 遍历Object中每个属性,添加监听
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  // 监听数组类型数据
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
复制代码

其中 def(value, '__ob__', this) 用于给当前对象添加一个 __ob__ 属性,值就是当前的 Observer ,目的时用来标记为已添加监听。

可以看到针对 Object 类型的对象,遍历后对每个属性调用了 defineReactive 方法。

// defineReactive方法部分内容
function defineReactive (obj, key, val) {

  ......

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = val;

      ...

      return value
    },
    set: function reactiveSetter (newVal) {
      const value = val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }

      ...
    }
  })
}
复制代码

getset 中省略部分就是数据发生改变后所做的操作。

其中 set 时做了优化,判断数据是否变化,无变化或无 set 函数时不做操作;当已存在 set 函数时直接执行,避免重复监听。

第二步:绑定监听器

第一步中实现了数据的监听,第二步就要根据数据的变化,来通知对应的 dom 进行更新。所以我们要先知道通知谁,也就是谁依赖了这个数据,由于获取数据时会触发 get 函数,因此我们就在 get 函数中收集依赖。

vue 中实现了一个 Dep 类,用来管理当前数据的依赖,只需要对每个添加监听的数据创建一个 Dep 类,再当“谁”调用了当前数据,就把“谁”添加到 Dep 中,触发 set 时再通知 Dep 中存放的依赖们。

首先实现 Dep 类:

class Dep {
  constructor () {
    this.id = uid++
    this.subs = []
  }

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

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

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
复制代码

Dep 类比较简单,定义了 subs 用于存放依赖数组,收集依赖时,触发 addSub 方法,派发通知时调用 notify 方法,对数组中每个依赖调用 update 方法。

第三步: Watcher

Dep 中,我们发现每个方法都是在处理一个依赖,而这个依赖从何而来,查看源码后发现, vue 还定义了一个 Watcher 类,也就是我们说的依赖。

class Watcher {
  constructor (vm, cb) {
    this.vm = vm
    this.cb = cb
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.value = this.get()
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    popTarget()
    return value
  }

  addDep (dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  update () {
    
    ...

    this.run()
  }

  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

}
复制代码

其中在实例化时,调用了 get 方法获取 value ,这里调用了一个 pushTarget 方法,和一个对应的 popTarget 方法,位于源码中 dep.js 文件中

function pushTarget (target) {
  targetStack.push(target)
  Dep.target = target
}

function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
复制代码

我们看到调用 pushTarget 方法时,将 Dep 的静态属性 target 设置为当前的 Watcher 对象,同时推入一个 target 栈中,调用 popTarget 时再弹栈。

回到 Observer 类的源码中

// 截取Object.defineProperty部分
Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = val
      if (Dep.target) {
        dep.depend()
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      dep.notify()
    }
  })
复制代码

得出整体流程如下,初始化 Watcher 类,调用 get 方法,在 get 方法中,调用数据的 getter ,而在 getter 中,调用了 dep.dependdepend 方法中调用了 Watcher 类的 addDep 方法, addDep 方法最终调用 addSub 方法添加依赖并注册监听。

派发通知时,触发 update 方法,从而更新 DOM

流程图如下:

总结

本文分析了 vue 源码中对 Object 类型数据的绑定过程。

vue

因此当 data 中定义的数据发生变化时,所有用到该数据的地方都能发生变化,也就实现了 vue 中数据绑定的功能。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章