vue双向绑定原理

vue双向绑定

vue数据双向绑定是通过数据劫持结合发布者-订阅者模式来实现的。首先要对数据进行劫持监听,所以需要设置一个监听器observer,用来监听所有的属性。如果属性发生了变化,就要告诉订阅者watcher看是否需要更新,因为订阅者很多,所以我们需要有一个消息订阅器容器dep专门收集订阅者,然后在监听器observe和订阅者watcher之间进行统一管理。接着需要一个指令解析器compile,对每一个节点元素进行扫描分析,将相关指令对应初始化成一个订阅者watcher,并替换掉模板数据或者绑定相应的函数。当订阅者watcher接收到相应属性发生变化就会执行对应的更新函数,从而更新视图。
1.实现一个监听器observer,用来劫持并监听所有属性,如果有变动的,通知订阅者。
2.实现一个订阅者watcher,可以接收到属性的变化,通知并执行相应的函数,进行更新视图。
3.实现一个解析器compile,可以扫描和解析每个节点的相关指令,并根据初始模板去初始化相对应的订阅器
4.核心是Object.defineProperty,通过definePropertyset方法来提醒订阅者watcher属性发生了变化,需要去更新视图,通过definePropertyget方法去替换相应模板数据。

Object.defineProperty
js 提供的 Object.defineProperty 可以轻松通过 gettersetter 方法得知数据何时发生了改变。
vue2源码 Observer 类,提供了将数据全部转化为可观测的、响应式的 object。
谁用到了这个数据"称为"谁依赖了这个数据",我们给每个数据都建一个依赖数组。
在创建响应式 object 时,使用了 defineReactive 方法在 getter 里使用了 Dep 添加依赖,setter 里使用了Dep通知依赖更新。总结就是在 getter 中收集依赖,在setter 中通知依赖更新。

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
}

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)
  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()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

array 的数据检测实现
在组件data中数组一般这么写:

data(){
  return {
    arr:[1,2,3]
  }
}

Array 也还是在 getter 中收集依赖的,比如 arr 这个数据始终都是在一个 object 里面。而从 object数据对象中获取 arr 数据自然就会触发 arr 的 getter,所以我们就可以在 getter 中收集依赖。

Array 想要检测数据的变化,必然是操作了 Array,而 js 中提供的方法就那几种,只要把这些方法重写一遍,在不改变功能的前提下,增加其他的功能就可以。

在Vue中创建了一个数组方法拦截器,它拦截在数组实例与Array.prototype之间,在拦截器内重写了操作数组的一些方法,当数组实例使用操作数组方法时,其实使用的是拦截器中重写的方法,而不再使用Array.prototype上的原生方法。源码重的拦截器如下:

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

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]      // 缓存原生方法
  Object.defineProperty(arrayMethods, method, {
    enumerable: false,
    configurable: true,
    writable: true,
    value:function mutator(...args){
      const result = original.apply(this, args)
      return result
    }
  })
})

在上面的代码中,首先创建了继承自Array原型的空对象arrayMethods,接着在arrayMethods上使用object.defineProperty方法将那些可以改变数组自身的7个方法遍历逐个进行封装。最后,当我们使用push方法的时候,其实用的是arrayMethods.push,而arrayMethods.push就是封装的新函数mutator,也就后说,实标上执行的是函数mutator,而mutator函数内部执行了original函数,这个original函数就是Array.prototype上对应的原生方法。 那么,接下来我们就可以在mutator函数中做一些其他的事,比如说发送变化通知。

我们把拦截器做好还不够,还要把它挂载到数组实例与Array.prototype之间,这样拦截器才能够生效。

其实挂载不难,我们只需把数据的__proto__属性设置为拦截器arrayMethods即可。

深度监听: 对于Array型数据,调用了observeArray()方法,该方法内部会遍历数组中的每一个元素,然后通过调用observe函数将每一个元素都转化成可侦测的响应式数据。


if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
}
function protoAugment (target, src: Object) {
  target.__proto__ = src
}

依赖管理器dep
为每一个数据都建立一个依赖管理器,把这个数据所有的依赖都管理起来。依赖管理器 dep 类应运而生 src/core/observer/dep.js,里面包含了,添加,删除依赖,通知依赖更新等方法。

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 () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Watcher 就是依赖
Vue 的Watcher类,其实就是上文说的依赖。在之后数据变化时,我们不直接去通知依赖更新,而是通知依赖对应的Watch实例,由Watcher实例去通知真正的视图。

分析Watcher类的代码实现逻辑:

1、当实例化Watcher类时,会先执行其构造函数;
2、在构造函数中调用了this.get()实例方法;
3、在get()方法中,通过let value = this.getter.call(vm, vm)获取被依赖的数据,获取被依赖数据的目的是触发该数据上面的getter,上文我们说过,在getter里会调用dep.depend()收集依赖,而在dep.depend()中取到挂载window.target上的值并将其存入依赖数组中,在get()方法最后将window.target释放掉。
4、而当数据变化时,会触发数据的setter,在setter中调用了dep.notify()方法,在dep.notify()方法中,遍历所有依赖(即watcher实例),执行依赖的update()方法,也就是Watcher类中的update()实例方法,在update()方法中调用数据变化的更新回调函数,从而更新视图。

简单总结一下就是:Watcher先把自己设置到一个指定位置,然后读取数据。因为读取了数据,所以会触发这个数据的getter。接着,在getter中就会从全局唯一的那个位置读取当前正在读取数据的Watcher,并把这个watcher收集到Dep中去。收集好之后,当数据发生变化时,会向Dep中的每个Watcher发送通知。通过这样的方式,Watcher可以主动去订阅任意一个数据的变化。

不足
虽然我们通过Object.defineProperty方法实现了对object数据的可观测,但是这个方法仅仅只能观测到object数据的取值及设置值,当我们向object数据里添加一对新的key/value或删除一对已有的key/value时,它是无法观测到的,导致当我们对object数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。

当然,Vue也注意到了这一点,为了解决这一问题,Vue增加了两个全局API:Vue.set和Vue.delete

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue双向绑定原理是通过数据劫持和发布订阅模式相结合的方式来实现的。在Vue中,当用户操作View时,ViewModel会感知到变化并通知Model进行相应的改变;反之,当Model发生改变时,ViewModel也能感知到变化并使View作出相应的更新。双向绑定的核心是使用了Object.defineProperty()方法来实现。 在Vue的初始化过程中,会对data数据进行劫持监听,这个过程由监听器Observe来完成。监听器会监听所有属性,当属性发生变化时,会通知订阅者Watcher来判断是否需要更新。由于订阅者Watcher可能有多个,所以需要一个消息订阅器Dep来统一管理这些订阅者。同时,还需要一个指令解析器Compile,用来扫描和解析每个节点的相关指令,将其初始化为一个订阅者Watcher,并替换模板数据或绑定相应的函数。 当订阅者Watcher接收到属性的变化通知时,会执行对应的更新函数,从而更新视图。整个过程中,监听器Observer负责劫持并监听所有属性,订阅者Watcher负责接收属性的变化通知并执行相应的函数,消息订阅器Dep负责收集订阅者并通知Watcher触发更新,指令解析器Compile负责扫描和解析节点的指令并初始化相应的订阅者。 综上所述,Vue双向绑定原理是通过数据劫持+发布订阅模式相结合的方式来实现的,通过监听器、订阅者、消息订阅器和指令解析器等组件的协作,实现了数据和视图之间的双向绑定。 #### 引用[.reference_title] - *1* *2* [vue双向绑定原理](https://blog.csdn.net/qq_41645323/article/details/123324680)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Vue双向绑定原理](https://blog.csdn.net/weixin_52092151/article/details/119810514)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值