vue双向绑定原理

响应式原理
1、vue2中是使用Object.defindProperty来做的,vue遍历data中的所有属性,并使用Object.defindProperty把这些属性转换为getter/setter
2、当用户访问或者设置某个属性时,会触发对应的getter/setter方法,然后会通知每个组件实例对应的watch方法,实现视图更新
3、Object.defindProperty有以下三个缺点:a、对于复杂对象需要深度监听,计算量非常大,性能不太好;b、对象的新增删除属性,这些操作无法实现监听,要使用Vue.$set和delete来做为辅助;c、需要重写数组的原生方法来实现数组的监听
4、vue3中使用proxy代替Object.defindProperty
优势:a、直接监听整个对象而不需要遍历监听属性,性能会有所提升;b、可以直接监听数组的变化而不需要重写数组原生方法;c、proxy多达13中拦截方法,功能会更强大

依赖收集
1、在源码目录src->core->observer目录

vue双向绑定原理


vue最独特的特征就是其响应式系统,当修改数据时,视图会相应更新
当把一个js对象传入vue实例作为data选项,vue会遍历该对象的所有property,并使用Object.defindProperty把这些property转换为getter/setter
这些getter/setter对用户不可见,但在内部它们能让vue追踪依赖,在属性被访问和修改时通知变更
每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把接触过的数据property记录为依赖,之后当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染

官方流程图

在解读源码时先看一些概念:
data:vue实例中的数据项
observer:数据的观察者,监控数据的读写操作
dep:消息订阅器,拥有收集订阅者、发布更新的功能
watcher:消息的订阅者,可以订阅dep,之后接受dep发布的更新并执行对应视图或者表达式的更新
dep和watcher的关系:dep是报纸,watcher是订阅报纸的人,如果它们之间建立了订阅关系,那么每当数据有更新的时候,就会通知对应的订阅者们
收集依赖:watcher在自身属性中添加dep的行为
收集订阅者:dep在自身属性中添加watcher的行为
下图是根据源码,自己的一些理解
在这里插入图片描述
源码版本2.6.11
首先找到vue进行数据处理的方法initData:

// 源码目录:src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      //<1>data属性代理
      proxy(vm, `_data`, key)
    }
  }
  //<2>observe数据绑定
  // observe data
  observe(data, true /* asRootData */)
}

<1>处将data上面的属性代理到vm实例上,完成之后可以直接通过vm.key访问data.key。

// 我们的数据对象
var data = { a: 1 }

// 该对象被加入到一个 Vue 实例中
var vm = new Vue({
  data: data
})

// 获得这个实例上的 property
// 返回源数据中对应的字段
vm.a == data.a // => true

如官方文档对vue实例的解释,一个数据对象被加入vue实例中后,vm.a == data.a //true 并且vm ==this //true
此外,只有当实例被创建时就已经存在于 data 中的 property 才是响应式的。也就是说如果你添加一个新的 property,如vm.b = 1,那么b的改动是不会触发更新的。解决办法是设置初始值。
<2>处开始进行数据绑定,asRootData,此处为根数据,接下来会递归进行深层对象的绑定

// 源码目录:src/core/observer/index.js
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) {  //检测当前数据是否被observe过
    ob = value.__ob__
  } else if (  //检测当前数据是否是普通对象,而不是函数或者是Regexp等情况
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

Observer

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

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)  //给value增加_ob_属性,作为数据已被observer观察的标志
    if (Array.isArray(value)) {  //value为数组
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   * 遍历对象,并调用对应函数defineReactive给属性添加getter和setter
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  } 

  /**
   * Observe a list of Array items.
   * 遍历数组,并调用对应函数observe给item添加getter和setter
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {   
      observe(items[i])
    }
  }
}

observer类有三个属性两个方法,value为传入该类的值,dep: Dep实例,用来收集订阅和发布更新,vmCount: 该实例被调用的次数。walk: 对对象的操作方法,observeArray: 对数组的操作方法
在observer类中,首先给value增加_ob_属性,作为数据已被observer观察的标志
如下
在这里插入图片描述
可以看到该数据已有_ob_标志,表示已经被观察。接下来,对于对象和数组有不同的操作方法,对于对象,会调用walk方法遍历对对象的属性进行defineReactive方法绑定。对于数组,是对它的七个方法进行重写
以下是defineReactive方法具体分析

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  //<1>/*在闭包中定义一个dep对象*/
  const dep = new Dep()
  //返回指定对象上一个自有属性对应的属性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  //如果之前已经定义了getter和setter函数,则将其取出
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) { 
    val = obj[key]
  }

  //<2>对象的子对象递归进行observe,并返回子节点的Observe对象
  let childOb = !shallow && observe(val)
  //<3>
  Object.defineProperty(obj, key, {
    enumerable: true,   //枚举
    configurable: true, //可改属性和可删除属性
    get: function reactiveGetter () {
      //如果原本有getter方法则执行
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {  //<4>
        // 进行依赖收集
        dep.depend()
        if (childOb) {
          //子对象进行依赖收集
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // 如果是数组则需要对每个成员进行依赖收集,如果数组成员还是数组则递归
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      //通过getter方法获取当前值,与新值进行比较,一致则不进行下面的操作
      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对象通知所有观察者
      dep.notify()
    }
  })
}

在代码<1>处,new了一个dep实例;<2>处如果有嵌套对象,则进行递归调用observe,这样能保证每一层都进行observe
代码<3>处是defineReactive的核心内容,通过Object.defineProperty()的get和set方法对属性进行相应操作
对于Object.defineProperty()的get和set方法
get:
属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
默认为 undefined。
set:
属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。
默认为 undefined。
当属性被访问时,即会进入get方法,在get中,核心内容是进行当前的watcher(也就是Dap.target)和dep之间的绑定dep.depend(),如果当前数据有子对象,则对子对象也进行绑定;如果是数组则需要对每个成员进行依赖收集,如果数组成员还是数组则递归
当属性被修改时,即会进入set方法,在set中,核心内容是通过dep.notify()来通知当前dep所绑定的订阅者们数据有更新
数组的处理
在Observer类中,对数组有这样的处理

if (Array.isArray(value)) {  //value为数组
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    }

如果是数组,将修改后可以截获响应的数组方法替换掉该数组的原型中的原生方法,达到监听数组数据变化响应的效果。
如果当前浏览器支持__proto__属性,则直接覆盖当前数组对象原型上的原生数组方法,如果不支持该属性,则直接覆盖数组对象的原型。
arrayMethods是对数组方法的重写,具体源代码:

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

/*取得原生数组的原型*/
const arrayProto = Array.prototype
/*创建一个新的数组对象,修改该对象上的数组的七个方法,防止污染原生数组方法*/
export const arrayMethods = Object.create(arrayProto)

/**
 * Intercept mutating methods and emit events
 */
 /*这里重写了数组的这些方法,在保证不污染原生数组原型的情况下重写数组的这些方法,截获数组的成员发生的变化,执行原生数组操作的同时dep通知关联的所有观察者进行响应式处理*/
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  /*将数组的原生方法缓存起来,后面要调用*/
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {
    // avoid leaking arguments:
    // http://jsperf.com/closure-with-arguments
    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    /*调用原生的数组方法*/
    const result = original.apply(this, args)

    /*数组新插入的元素需要重新进行observe才能响应式*/
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
      
    // notify change
    /*dep通知所有注册的观察者进行响应式处理*/
    ob.dep.notify()
    return result
  })
})

可以看到,对于push,unshift,splice这三个方法,需要observe,其他方法的变更会在当前的索引上进行更新,所以不需要再执行ob.observeArray

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 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、付费专栏及课程。

余额充值