vue响应式原理

简介

响应式系统是vue框架核心的部分,数据对象仅仅是普通的 js对象。当我们改变数据时,视图也会被改变,本文就来探究一下vue 的响应式原理。

Object.defineProperty

vue响应式的核心是使用了es5 新增的API Object.defineProperty(因此vue不支持ie8) 。Object.defineProperty的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 它允许我们为对象的实行设置get和set方法,在对象取值和赋值会调用这两个方法,所以我们可以在赋值和取值时劫持这两个方法,触发更新试视图的操作。

const obj = {
	attr: '测试',
}
let val = '劫持'
Object.defineProperty(obj, 'attr', {
  enumberable: true,
  configurable: true,
  get() {
	return val
  },
  set(newVal) {
	val = newVal
	console.log('更新视图的操作')
  }
})

console.log(obj.attr)
obj.attr = '赋值'
console.log(obj.attr)

发布订阅模式

vue的依赖收集使用了发布订阅模式。

class Dep {
  constructor() {
    // 存放订阅者的数组
    this.subs = []
  }
  // 增加订阅者
  addSub(sub) {
      this.subs.push(sub)
  }
  // 通知订阅者
  notify() {
    this.subs.forEach((sub) => {
      sub.update()
    })
  }
}

const dep = new Dep()

const sub_fn1 = {
  update() {
    console.log('fn1 update')
  }
}

const sub_fn2 = {
  update() {
    console.log('fn2 update');
  }
}

dep.addSub(sub_fn1)
dep.addSub(sub_fn2)
dep.notify()

init

vue在初始化时会条用一系列方法,其中initState对数据data进行了初始化

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

这段代码对props methods data进行了初始化,我们选择initData作为切入点来探究vue的响应式系统。

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
    )
  }
  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)) {
      // 添加代理对象的属性
      proxy(vm, `_data`, key)
    }
  }
  observe(data, true /* asRootData */)
}

这段代码获取了data,先判断了data的类型是不是纯对象,然后判断了data的属性与props和methods是否冲突,接着在vue 实例对象上添加了访问数据代理对象_data的同名属性,最后调用了observe 函数开启了数据响应式系统。

observe

因为observe代码相对比较复杂,有很多边界条件的判断,如避免收集重复的依赖,如何深度观测,如何处理数组与对象等,为了简化理解,我们只关注最核心的逻辑。

export function observe (value: any, asRootData: ?boolean): Observer | void {
  // data是对象且是Vnode实例时,继续
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__ // ob保存 Observer 实例,避免重复观测
  } 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
}

这段代码是observe的全部内容,observe对data进行了一个判断, 如果不是对象或者不是Vnode的实例,不进行监测。然后判断data是否有__ob__ 属性,如果有直接赋值,避免重复监测,因为对象被监测后会添加__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)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

上面是Observer类。this.value = value 是实例化对象时传进来数据的引用,可以看到def(value, ‘ob’, this)这句代码为监测对象添加了__ob__属性,然后判断时纯对象还是数组,分别走不同的监测逻辑。因为对象相对简单,我们选择对象理一下逻辑。当数据是纯对象时,会调用walk函数,遍历data所有可枚举的属性,然后循环将这些属性作为参数调用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
  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()
    }
  })
}

这段代码相对比较复杂,有许多边界条件的判断,我们不必关心实现实践的细节,只关注Object.defineProperty即可。defineReactive 的核心是将数据对象的属性用Object.defineProperty进行数据劫持 const dep = new Dep()定义了依赖收集器。Object.defineProperty定义了get和set方法,当执行赋值时,会调用get函数执行dep.depend()进行依赖收集,当值改变时调用set触发dep.notify(),进行视图更新。

总结

vue响应式系统的核心原理是数据劫持结合发布订阅模式,在vue初始化过程中,vue会对templae编译生成指令对象与订阅者关联,通过Object.defineProperty为数据对象添加getter和setter,并执行gettter,添加订阅者。当数据改变时,触发setter,订阅者执行指令更新,指令是对原生dom对象的封装,从而导致视图的更新。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值