Vue 2.5 数据绑定实现逻辑(二)Dep,Observer 和 Watcher 的协同配合

上一节说到了 Dep, Observer, Watcher 这三个对象的作用,即 Dep 是数据依赖,Observer 用来处理成员对象的增删,Watcher 负责在数据发生变动的时候执行回调函数。

这一节则主要来说:

  1. Watcher 何时将 Dep 添加到自己的 deps 数组中。
  2. Dep 何时将依赖于自身的 Watcher 添加到自身的 subs 数组中
  3. Observer 中的 Dep 什么时候将相关的 Watcher 添加到自身的 subs 数组中

Dep 和 Watcher 的 depend 方法及全局 Watcher 栈

全局 Watcher 栈

在 src/core/observer/dep.js 中,存在一个全局的栈,用来储存当前被触发且正在运行的 Watcher,同时 Dep 还有一个静态成员 target ,在这里可以看成是栈顶元素。

Dep.target = null
const targetStack = []

export function pushTarget (_target: Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}
Dep 的 depend 方法

Dep 的 depend 方法是从栈中获取栈顶的 Watcher 并添加到自己的 subs 数组中。
其中运行了 Watcher 的 addDep 来将该 Dep 添加到 Watcher 的 newDeps 数组中(之后会进行处理,就会将所有新的依赖添加到真正的 deps 数组中,这里的 newDeps 是起缓冲的作用的,而这个缓冲起了什么作用,我还有待研究),在这个 addDep 数组中又运行了 Dep 对象的 addSub 方法来将该 Watcher 添加到自己的 subs 数组中, 形成一种你中有我,我中有你的状态。

addSub (sub: Watcher) {
    this.subs.push(sub)
  },
...
depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
addDep (dep: 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)
      }
    }
  }
Watcher 的 depend 方法

Watcher 的 depend 方法是运行 deps 数组中所有的 Dep 对象的 depend 方法,比较好理解。

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

全局 Watcher 栈何时执行压栈和出栈

上节中说到 Watcher 在建立的时候会接收并保存一个 getter 函数, 在 Watcher 的 get 函数中则会运行这个 getter 并获取可能的返回值赋值给 value。

在这段代码中可以看到,在执行 getter 之前,运行了 pushTarget(this) 来将自己(现在正在运行的 Watcher)压入全局 Watcher 栈,而在 getter 执行结束后运行 popTarget 执行出栈操作。

get () {
    pushTarget(this) // 压栈
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget() //出栈
      this.cleanupDeps()
    }
    return value
  }

defineReactive,get 和 set 钩子函数

在 defineReactive 函数中,首先做了两个比较重要的操作:

  1. 新建一个 Dep 对象,通过闭包将其保存下来。
  2. 对该 value 执行 observe 函数,递归建立起 value 及其以下所有需要建立的 Observer 对象,同时为所有属性运行 defineReactive 函数设置钩子。
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  ......
}

接下来就是建立钩子函数。

在 get 中,主要检测全局 Watcher 栈的栈顶是否有元素,如果有则运行之前建立的 Dep 对象的 depend 来建立该依赖和 Watcher 的关联关系, 同时建立该 value 的 Observer 中的 Dep 对象和 Watcher 的关联关系(如果该 value 是对象或者数组)。 之前说过,只有在某个 Watcher 的 getter 函数中在运行中的时候(中间几个语句的执行可以忽略)才会将这个 Watcher 压栈且其出于栈顶位置,所以在这个时候如果运行了 get 钩子函数,则基本可以肯定此时运行的 getter 函数是和这个 value 有关系的,即这个依赖和栈顶的 Watcher 有关联。

在 set 中, 首先检测新值和旧值是否相等,如果相等则直接返回。之后又运行了一遍 observe 函数,因为赋的新值有可能是一个数组或者对象,即需要建立新的一个或多个 Observer。最后运行了 notify 来通知这个依赖相关联的所有 Watcher 去运行回调函数。

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })

vm.$set 和 vm.$delete

Vue 实例对象的 $set 方法对应的是 set 方法, $delete 对应的是 del。

// 位置: src/core/instance/state.js
Vue.prototype.$set = set
Vue.prototype.$delete = del

逻辑其实很简单,就是给一个对象添加成员时先检测这个对象是不是 Vue 实例,是的话则直接返回(set 了也没用)。之后获取该对象的 Observer 并运行其中 Dep 对象的 notify 方法通知 Watcher,还要设置好新成员的钩子函数。删除对象成员时也是相似了逻辑。

// 位置: src/core/observer/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
......
export function del (target: Array<any> | Object, key: any) {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid deleting properties on a Vue instance or its root $data ' +
      '- just set it to null.'
    )
    return
  }
  if (!hasOwn(target, key)) {
    return
  }
  delete target[key]
  if (!ob) {
    return
  }
  ob.dep.notify()
}

总结

简单来说,就是 Dep 和 Watcher 在 get 钩子函数中建立关联(实际上在初始化计算属性的时候也建立了关联,但是放在以后说), 这个过程中有一个全局的 Watcher 栈做辅助,可以说这个设计相当巧妙。在 set 钩子函数和调用 vm.$set 和 vm.$delete 时运行 notify 来通知 Watcher。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容