资讯专栏INFORMATION COLUMN

Vue源码学习(三)——数据双向绑定

sevi_stuo / 1010人阅读

摘要:就是用于把变化放入观察,并通知其变化更新。这边数据双向绑定差不多就结束了。下一章节通过数据绑定原理结合来实现数据驱动更新的。

在Vue中我们经常修改数据,然后视图就直接修改了,那么这些究竟是怎么实现的呢?
其实Vue使用了E5的语法Object.defineProperty来实现的数据驱动。
那么Object.defineProperty究竟是怎么实现的呢?
我们先来看一下一个简单的demo


在vue这段小代码中,this.test === this._data.test其实是等价的。这是为什么呢
其实就是通过Object.defineProperty做的一个小代理实现的。
原理如下:

var obj = {
    _data: {
        x: 123,
        y: 467
    }
}
function proxy (target, sourceKey, key) {
    Object.defineProperty(target, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            return this[sourceKey][key]
        },
        set: function(val) {
            this[sourceKey][key] = val
        }
    })
}
proxy(obj, "_data", "x")
proxy(obj, "_data", "y")

console.log(obj.x)  // 123
console.log(obj.y)  //  467
console.log(obj._data.x) // 123
console.log(obj._data.y) // 467

以上一个demo就是对obj对象的一个代理,通过访问obj.x直接代理到obj._data.x。 Object.defineProperty具体用法可以自行搜索。
那么其实数据双向绑定也是根据Object.defineProperty里面的get和set来实现的,通过set的时候去做一些视图更新的操作。

接下来我们就来看一下Vue源码吧。
双向数据绑定,将分为以下3个部分:

1. Observer。这个模块是用于监听对象上的所有属性,即使用Object.defineProperty来实现get和set
2. Watcher。 这个模块是观察者,当监听的数据值被修改时,执行对象的回调函数。
3. Dep。 连接Observe和Watcher的桥梁,每个Observer对应一个Dep,内部维护一个数组,保存与该Observer相关的Watcher
Observer

首先来看下oberser。
路径: src/core/observer/index.js

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 添加__ob__来标示value有对应的Observer
    def(value, "__ob__", this)
    // 对数组的处理
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
    // 处理对象
      this.walk(value)
    }
  }

 // 给每个属性添加getter/setters
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  // 观察数组的每一项
  observeArray (items: Array) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

所以从源码可以看出数据类型主要分为2种,一种是数组,一种是对象。对应的处理方法分别树observe和defineReactive
那我们再来看看这2个函数做了些什么呢?
这2个函数也是在这个文件内部

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
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
    // 这边作出get和set的动态响应的处理
  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()
    }
  })
}

// 这个方法具体是为对象的key添加get,set方法;如果用户传入自己本身传入get和set方法也会保留其方法。它会为每一个值都创建一个Dep,在get函数中 dep.depend做了2件事,一是向Dep.target的内部添加dep,二是将dep。target添加到dep内部的subs数组中,也就是建立关系。
在set函数中,如果新值和旧值相同则不处理,如果不同,则通知更新。
接下来看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 (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

对数组中的每一项进行检测,该方法用于观察一个对象,如果不是对象则直接返回,如果是对象则返回该对象Observer对象。
但是这样紧紧对数组中的每一项的对象进行了观察,如果数组本身的长度修改那么又如何触发呢。Vue专门对数组做了特出的处理。
回过头来看Observer的类中有这么一段,在处理数组的时候,处理了这些代码

const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)

我们来细看这些函数,见如下

function protoAugment (target, src: Object, keys: any) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

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

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

const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original 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)
    // notify change
    ob.dep.notify()
    return result
  })
})

我们可以看到protoAugment很简单,就是执行了一段value._proto_ = arrayMethods
copyAugment中循环把arrayMethods上的arrayKeys方法添加到value上。
arrayMethods又是重写了数组的操作方法["push","pop","shift","unshift","splice","sort","reverse"]。
通过调用数组这些方法的时候,通知dep.notify。 至此Observer部分已经结束

Dep

Dep相对就简单点。
源码路径:src/core/observer/dep.js

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

  constructor () {
    this.id = uid++
    this.subs = []
  }
   // 添加观察者
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
    // 删除观察者
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
    // 调用watcher
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

内部有个唯一的id标识,还有一个保存watcher的数组subs。

Watcher
let uid = 0

export default class Watcher {

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    ...
    this.cb = cb
    this.id = ++uid
    ...
    this.expression = process.env.NODE_ENV !== "production"
      ? expOrFn.toString()
      : ""
    if (typeof expOrFn === "function") {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
      }
    }
    this.value = this.get()
  }

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

   ...

  update () {
    ...
    queueWatcher(this)
  }

  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // set new value
        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)
        }
      }
    }
  }
  ...
}

Watcher就是用于把变化放入观察,并通知其变化更新。
queueWatcher就是把变化者放入数组queue,然后通过nextTick去更换新数组queue中的变化。
在生命周期挂载元素时,就会通过创建Watcher,然后来更新创新模块。

vm._watcher = new Watcher(vm, updateComponent, noop)

这边数据双向绑定差不多就结束了。最后再附上一张简要的流程图来进一步清晰自己的思路。

下一章节通过数据绑定原理结合jquery来实现数据驱动更新的demo。之所以采用jquery操作dom是因为现在Vue源码还没到解析html模板那一步。
所以一步步来。等之后学完模板解析后。再去制作一个MVVM的简易demo。

如果对您有帮助,请点个赞,谢谢!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/99085.html

相关文章

  • Vue原理】VModel - 白话版

    摘要:执行的时候,会绑定上下文对象为组件实例于是中的就能取到组件实例本身,的代码块顶层作用域就绑定为了组件实例于是内部变量的访问,就会首先访问到组件实例上。其中的获取,就会先从组件实例上获取,相当于。 写文章不容易,点个赞呗兄弟专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得...

    keke 评论0 收藏0
  • vue.js源码 - 剖析observer,dep,watch者关系 如何具体的实现数据双向绑定

    摘要:双向数据绑定的核心和基础是其内部真正参与数据双向绑定流程的主要有和基于和发布者订阅者模式,最终实现数据的双向绑定。在这里把双向数据绑定分为两个流程收集依赖流程依赖收集会经过以上流程,最终数组中存放列表,数组中存放列表。 Vue双向数据绑定的核心和基础api是Object.defineProperty,其内部真正参与数据双向绑定流程的主要有Obderver、Dep和Watcher,基于d...

    mo0n1andin 评论0 收藏0
  • Vue双向绑定原理,教你一步一步实现双向绑定

    摘要:储存订阅器因为属性被监听,这一步会执行监听器里的方法这一步我们把也给弄了出来,到这一步我们已经实现了一个简单的双向绑定了,我们可以尝试把两者结合起来看下效果。总结本文主要是对双向绑定原理的学习与实现。 当今前端天下以 Angular、React、vue 三足鼎立的局面,你不选择一个阵营基本上无法立足于前端,甚至是两个或者三个阵营都要选择,大势所趋。 所以我们要时刻保持好奇心,拥抱变化,...

    Labradors 评论0 收藏0
  • 迷你版Vue--学习如何造一个Vue轮子

    摘要:项目地址和的区别其实和最大的区别就是多了一个虚拟,其他的区别都是很小的。 项目地址 Vue1和Vue2的区别 其实Vue1和Vue2最大的区别就是Vue2多了一个虚拟DOM,其他的区别都是很小的。所以理解了Vue1的源码,就相当于理解了Vue2,中间差了一个虚拟DOM的Diff算法 文档 数据双向绑定 Vue主流程走向 组件 nextTick异步更新 MVVM 先来科普一下MVVM...

    isLishude 评论0 收藏0

发表评论

0条评论

sevi_stuo

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<