学习vue2.5源码之第三篇——数据的双向绑定(数据响应式原理)

数据的双向绑定(数据观测)

上一章我们在学习initState的时候发现初始化的时候,initPropsinitData都涉及到observer文件夹中的一些方法(defineReactiveobserve),以及initComputedinitWatch也涉及到Watcher对象和$watch方法等等,这些都有是和数据的双向绑定(也叫数据观察)有关,这一章我们就来学习一下vue关于数据双向绑定的代码。核心代码主要是在src/core/observer文件夹中。

一些概念

vue是由render函数来生成虚拟dom,再映射到页面上。Observer将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为 getter/setter,通过Dep达到追踪依赖的效果。当我们访问属性时会添加依赖,当我们修改属性时将会通知Watcher重新计算,从而致使它关联的组件得以更新。

  • Observer: 观察者,对实例对象的所有属性进行监听

    • 思维路径 walk() => defineReactive() => Object.defineProperty() => Dep...
  • Watcher: 当数据变化时执行回调函数cb,还有一些与dep操作相关的函数

    • 思维路径 update() => sync ? (run() => cb.call()) : queueWatcher()
  • Dep: dependence依赖的缩写,依赖收集器,Observer和Watcher之间的桥梁,存放watch数组,绑定数据getter时添加依赖,更改数据setter时通知watcher更新

observe

还记得initData是通过observe()作为入口进行数据绑定的吗,我们就从这里开始看起吧~先看一下这个函数:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    observerState.shouldConvert && // 为真时,改变后的新值也会进行双向绑定
    !isServerRendering() && // 非服务端渲染
    (Array.isArray(value) || isPlainObject(value)) && // 是一个数组
    Object.isExtensible(value) && // 或者是一个单纯的可拓展的对象
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++ // 以此对象作为根节点$data的vue实例数量加一
  }
  return ob
}

第一个参数是一个data对象,假如它拥有__ob__属性的话证明该对象已经进行绑定了一个observer,就不用再次创建了;假如没有__ob__属性,而且满足以上条件(注释中有标明)的时候,我们将为它创建一个Observer对象。ob = new Observer(value),并且执行vmCount++。我们转到Observer看看:

Observer

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; 

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 定义__ob__属性
    def(value, '__ob__', this)
    // 若是数组对象则另作处理(关于数组原型方法的数据监听)
    if (Array.isArray(value)) {
    const augment = hasProto
      ? protoAugment
      : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
    } else {
    // 其他途径走walk()
    this.walk(value)
    }
  }

  ...
  
}

每一个Observer对应一个Dep依赖,并且为该数据对象添加__ob__属性,值为这个Observer,之前在observe函数中可以看到假如已经存在__ob__属性就不需要再次创建Observer对象了。

接下来要判断传入的值是否是一个数组,假如value不是一个数组而是一个普通对象,那么执行walk()函数。

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

walk()中遍历对象对每个属性执行defineReactive,我们又看看defineReactive

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
  }

  const getter = property && property.get
  const setter = property && property.set

  let childOb = !shallow && observe(val)  
  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
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

该方法先是为每个属性创建一个Dep,这样做的原因是每个属性对应自己的依赖集,当有属性发生变化时只触发相应属性的更新而不用更新全部属性,导致资源的浪费。

vue允许用户自己定义getter和setter,假如用户自己设置了的话,vue就使用用户所定义的getter和setter。

现在重头戏来了,vue的数据双向绑定就是通过Object.defineProperty这个原生的方法实现的,Object.defineProperty 是 ES5.1 规范中提供的方法,用来修改对象属性的属性描述对象,通过 Object.defineProperty 函数,我们可以通过定义对象属性的存取器(getter/setter)来劫持数据的读取,实现数据变动时的通知功能。

回到源码中来,通过Object.defineProperty我们重写了对象的属性描述符,在getter中添加dep.depend(),让数据和watcher建立关系,并且对子对象的observer也进行依赖绑定childOb.dep.depend(),如果数组元素也是对象,那么它们的observe过程也生成了__ob__实例,那么就让__ob__也收集依赖。

在setter中若新老值有变化的话就添加dep.notify来通知watcher更新数据,并监听新的值childOb = !shallow && observe(newVal)customSetter在开发过程中用于输出错误。

注意,defineReactive中的depObserver构造函数的dep是不同的,原因是getter和setter只能监听到属性的更改,不能监听到属性的删除与添加,所以Observer构造函数的dep会在vue的setdelete操作中会利用上。

看回Observer构造函数中的第二种情况,假如value是一个数组对象,会做另外的处理。由于传进来的数组对象只是一个引用地址,对于数组一些原有的操作(push、splice、sort等)是无法进行数据监听的,所以vue中有两种操作:1,在环境支持__proto__时,执行protoAugment,改写value的__proto__;2,不支持__proto__时,执行copyAugment,直接在value上重新定义这些方法。

function protoAugment (target, src: Object, keys: any) {
  target.__proto__ = src
}

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

重点是在于来自同级文件array.js的arrayMethods,它是一个改写了数组方法的对象

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 添加数据更新
    ob.dep.notify()
    return result
  })
})

特别对push、unshift、splice参数大于2时的三种情况进行调用ob.observeArray,因为他们都添加了新元素需要进行监听,然后给所有的原型方法都添加了数据更新ob.dep.notify()。然后对原型方法进行重新定义后,遍历这个数组为每个元素执行observe,监听新插入的值。

observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
  observe(items[i])
  }
}

Dep

前面提及了好几次dep中的方法,而且我们也提到了dep的基本概念,就是联系Observer和Watcher的依赖收集器,我们现在看一下Dep对象

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

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

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

  removeSub (sub: Watcher) {
    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中有个静态属性target指向目标Watcher,id作为Dep的唯一标识,subs是用于存放watcher的数组。addSub向数组添加watcher,removeSub从数组中删除watcher,depend是在getter中为目标Watcher添加依赖,调用了watcher的addDep(稍后分析),notify在setter时触发watcher的更新。

Watcher

let uid = 0

export default class Watcher {

  constructor (
  ) {
    this.vm = vm
    vm._watchers.push(this)
    this.id = ++uid
    this.cb = cb
    this.getter = expOrFn
    this.value = this.get()
  }

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

  update () {
    this.run()
  }

  run () {
    const value = this.get()
    this.cb.call(this.vm, value, oldValue)
  }

}

emmm...我们直接看一下超级无敌简化版的watcher,走一下最常见的流程,例如最简单的:vm.$watch("mydata",()=>console.log("我是回调"))。主要是看传来的监听表达式/函数expOrFn(mydata)和响应回调函数cb(()=>console.log("我是回调")),大概流程就是observer监听到属性值更改后(setter)调用dep.notify通知watcher更新updateupdate中调用了函数runrun通过get获取到监听的值expOrFn,最后调用回调函数cb进行响应式更新(大概思路:update => run => get => cb)

现在我们再具体看看那几个函数

get

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 {
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

来个小插曲,什么是dep.target呢?它是Watcher实例,为什么要放在Dep.target里呢?是因为在getter中会执行dep.depend(),执行Dep.target.addDep(this),而在addDep中通过 dep.addSub(this)又把Watcher实例添加到dep的观察集中。

// Dep.js
depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}
// Watcher.js
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)
    }
  }
}
// Dep.js
addSub (sub: Watcher) {
  this.subs.push(sub)
}

看回get()函数,先是执行pushTarget(this),将这个watcher设为target,对传来的表达式expOrFn进行一定的运算得到value,这个过程触发target相关依赖的getter(例如表达式为‘a+b’的话就会触发a和b的getter),也就是说会把target加到各自的dep.subs中,也就是成功地绑定了所有相关的依赖,完成操作后再执行popTarget()将target又恢复为null。每次进行数据监听都会设定当前watcher为新的target,完成更新后又会将target设为空值,这样数据之间就不会互相污染。

update && run

update () {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    // 异步队列
    queueWatcher(this)
  }
}

run () {
  if (this.active) {
    const value = this.get()
    if (
      value !== this.value ||
      isObject(value) ||
      this.deep
    ) {
      const oldValue = this.value
      this.value = value
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

当属性值发生变化时触发setter,就会执行dep.notify,然后调用了watcher的update,在update中有两种情况,同步的话就执行run,异步的话执行queueWatcher。后者我们稍后再讨论,先看run,若新旧值不同的话就执行回调函数this.cb.call(this.vm, value, oldValue)。只有在用户设定了sync:true时才会直接执行数据更新,否则的话都会走另外一条路:异步更新队列。

sync && immediate

说到sync,再说说immediate,设置immediate:true也能直接执行回调,但是和sync是不一样的。代码实现在src/core/instance/state.js中有提及:

if (options.immediate) {
  cb.call(vm, watcher.value)
}

区别在于,当指定immediate:true时,会立即以表达式的当前值触发回调函数,不会等到数据发生变化时才进行回调;而指定sync:true时是数据发生变化时不将watcher放进异步队列中而是立即执行回调。

queueWatcher

简单来说异步更新队列就是开启一个队列,将一个事件循环内的全部数据变动缓冲在其中,并对watcher做去重操作。在下一个tick中我们再将队列中的watcher依次取出并执行更新。这对避免不必要的计算和DOM操作非常重要。

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

flushing变量是正在刷新,当页面不在刷新时,把watcherpush到一个队列尾部,若正在刷新的话则把watcher放到队列相应的位置中(根据id);waiting变量是正在等待,假如没有处于等待状态的话则等到下一个nexttick中执行flushSchedulerQueue

flushSchedulerQueue

function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  queue.sort((a, b) => a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    watcher.run()
  }

  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

此函数将队列中的watcher根据它们的id从小到大排列依次执行它们的run方法,完成后resetSchedulerState重置flushingwaiting等状态,再
callUpdatedHooks(updatedQueue)

callUpdatedHooks

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}

vm._watcher保存的是渲染模板时创建的watcher,如果队列中有该watcher,则说明模板有变化,调用updated生命钩子。

在vue官网中对异步更新队列的解释是

只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会一次推入到队列中。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

set && del

前面提到了使用defineReactive进行数据监听有个缺陷,就是当属性进行删除和新增时,getter和setter并不会触发,所以vue为了弥补这个缺陷,提供Vue.set和Vue.delete方法让我们设置和删除对象的属性。

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
  }
  // Observer构造函数中定义的dep
  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
  }
  // Observer构造函数中定义的dep
  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()
}

还记得Observer构造函数中定义了dep吗,这是为每个数组和对象都创建另一个依赖收集器,挂在它们的__ob__属性上。在依赖收集阶段,watcher 将被同时注册在这两个 dep 上,准备接收响应。setter 操作,通知 set/get 中的 dep;非 setter 操作,如对象 key 添加删除、数组变异方法调用,通知__ob__中的 dep

deep

当我们设置deep: true时即进行深度监听。举个例子,有这么个对象{a:{b:{c:{d:1}}}},当我们访问a.b.c的值时,依次触发了a.ba.b.cgetter,将 watcher 注册进对应的 dep,所以当我们修改a.b.c上游的值时a.b.c会监测到发生改变;但是相反,当我们修改a.b.c的值时a.b.c上游是不会监测到改变的,因为setter只会检测到引用变化,不会监测到对象内部的变化。所以这个时候我们可以设置deep: true,在代码中的实现是traverse(value),当发现依赖目标为一个对象时,递归进去遍历每一个子属性,这就会主动触发了深度属性的getter。实现深度监听的功能~

这就是vue中实现数据双向绑定的整个过程,如有描述不妥或不清晰之处请随时提出,谢谢~

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

推荐阅读更多精彩内容