Vue2 响应式对象分析

298 阅读4分钟

本文主要分析 Vue 响应式对象的创建,以 data 为例,Vue 内部是怎么处理的。

data 处理:

1、对 vm._data 设置 getter / setter 拦截,通过 Dep 管理 Watcher

2、vm.key 访问实际是 vm._data.key

function initData(vm: Component) {
  let data = vm.$options.data
  // 处理后的 data 给 vm._data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
  : data || {}

  // ... 校验 data 属性
	// 代理 vm.key -> vm._data.key, 注意 $,_开头不会代理
  proxy(vm, `_data`, key)
  // 将 data(vm._data) 响应式 
  observe(data, true /* asRootData */)
}
// 我们访问 this.msg -> this._data.msg -> this._data.msg 的 getter

image.png

proxy 函数作用将 target.key -> target[sourceKey][key]

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

实例是怎么能访问到 data 中的数据?

data: {msg: 'dxx'},为什么页面能直接 this.msg 访问到?
因为 Vue 内部会 proxy(vm, '_data', 'msg'),将 msg 放到 vm 实例中,所以页面能访问 this.msg

若 msg 改为 _msg 会怎么样呢? 页面如何访问

改为$,_ 开头 proxy 不会执行,页面中通过 this._data._msgthis.$data._msg;
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
if (process.env.NODE_ENV !== 'production') {
  dataDef.set = function () {
    warn(
      'Avoid replacing instance root $data. ' +
      'Use nested data properties instead.',
      this
    )
  }
  propsDef.set = function () {
    warn(`$props is readonly.`, this)
  }
}
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
可以看到访问 $data $props 下的属性其实是 _data _props
而修改$data={} 会给警告 $props 也不能修改

observe 将对象变成响应式

var vm = new Vue({
  el: '#app',
  template: `
      <div>
        {{msg}}
      </div>
      `,
  data() {
    return { 
      msg: {
        age: 10,
      }
    }
  },
})

流程:

data 是个对象 {msg:{age: 10}} 记为 root,msg 的值是个对象 {age:10} 记为 a,age 的值是个基本类型 10,记为 b。

observe(root)
开始
root.__ob__ = Observer1 {value: root, dep1, vmCount=0}
defineReactive(root, 'msg') msg get/set 闭包引用 new Dep2
  observe(a)
  开始
  a.__ob__ = Observer2 {value: a, dep3, vmCount=0}
  defineReactive(a, 'age')  age get/set 闭包引用 new Dep4
     observe(b) return
  设置 a.age get/set  Dep4, childOb没有
  return Observer2
  结束
设置 root.msg get/set  Dep2, childOb 为 Observer2
return Observer1 (vmCount++)
结束

上面 walk 没有加进去,因为对象里面只有一个属性 key;若多个属性 key 会循环调用 defineReactive ;

defineReactive 设置 obj.key 的 get/set,并通过 Dep 管理依赖,get 时收集依赖,set 对值处理,通知收集的依赖处理更新

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 每个属性都有 dep 
  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
  // 注意参数是 2 个,且 getter 没有值,或有 setter
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
	// shallow true 的话不进行深度处理  $attrs $listeners 不进行深层次设置(shallow=true)
  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()
    }
  })
}

往对象上添加新属性,普通添加是不触发页面更新的,Vue 提供 set 处理

var vm = new Vue({
  el: '#app',
  template: `
      <div @click="change">
        {{msg}}
      </div>
      `,
  data() {
    return { 
      msg: {
        age: 10
      }
    }
  },
  methods: {
    change() {
      // this.msg.name = 'dxx'; 页面不会更新
      this.$set(this.msg, 'name', 'dxx')
    }
  }
})

初始化后对象结构

data {
	msg(dep2): {  
		age(dep4): 10,
		__ob__: Observer2 (dep3)
	},
	__ob__: Observer1 (dep1)
}
dep1 dep3 由 Observer 实例时生成
dep2 dep4 由 defineReactive 是对属性拦截时设置 闭包引用

页面仅有一个渲染 Watcher
当页面渲染 msg 时,this.msg -> this._data.msg -> msg.get dep2[Watcher], childOb为 Observer2, dep3[Watcher] dep4[Watcher]

Vue.set 函数源码:

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
  }
  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
}
// set 时的对象需要响应式(defineReactiv设置get/set);对 this.$set(this.$data, 'aa', 'aaa'); 是无效的 data 不能收集依赖 ×
// 非实例 vm ;避免覆盖 this.$set(this, 'aa', 'aaa');×

页面点击时,target 为 msg 对象,key 为 name,val 为 dxx ;msg.__ob__ 为 Observer1; ob.value就是 msg 对象,defineReactive 执行后name(dep5): 'dxx',要重新渲染 msg 对象,通过 msg.__ob__.dep.notify() 渲染 Watcher 执行。data 重新收集依赖。

当对象属性定义 getter 时,data 拦截会怎么设置?

const data = {}
Object.defineProperty(data, 'getterProp', {
  enumerable: true,
  configurable: true,
  get: () => {
    return {
      msg: 1
    }
  }
})
var vm = new Vue({
  el: '#app',
  template: `
      <div @click="change">
        {{getterProp}}
      </div>
      `,
  data() {
    return data
  },
  methods: {
    change() {
      this.$set(this.getterProp, 'ns', Math.random());
    }
  }
})

初始化后的数据结构:

开始 data {
	 getterProp(get): fn
}
执行到 defineReactive 时,因为 getter 有值,没有 setter,所以 val=undefined,childOb = undefined
然后重新设置 getterProp 的 get/set
当页面渲染 getterProp 时,仅 getterProp 的 dep 收集,返回 value {msg: 1}
页面点击
此时 this.getterProp -> {msg: 1} 它的 __ob__ 不存在,页面不会更新。

数组的处理

var vm = new Vue({
  el: '#app',
  template: `
      <div @click="change">
        {{msg}}
      </div>
      `,
  data() {
    return { 
      msg: [1]
    }
  },
  methods: {
    change() {
      this.msg.push(2)
    }
  }
})

初始后

data {
	msg(dep2): [
		1,
		__ob__: Observer2 (dep3)
	]
	__ob__: Observer(dep1)
}

数组的处理,内部重写了 push pop unshift shift splice sort reverse ,主要是添加新元素要将新元素先 observe,然后 notify 通知更新,通知的关键就是上面的 dep3。数组还有注意点就是收集时,相关所有子元素通过 dependArray 都会收集依赖

var vm = new Vue({
  el: '#app',
  template: `
      <div @click="change">
        {{msg}}
      </div>
      `,
  data() {
    return { 
      msg: [1,{name: 'dxx'},[2]]
    }
  },
  methods: {
    change() {
      this.msg.push(2)
    }
  }
})

这个加载后的数据结构:
data {
  msg(dep2):[
    1,
    {
      name(dep5): 'dxx',
      __ob__: Observer3(dep4)
    },
    [
      2,
      __ob__: Observer4(dep6)
    ],
    __ob__: Observer2(dep3)
  ],
  __ob__: Observer1(dep1)
}
Watcher 的收集
dep2 dep3 dep4 dep6 dep5

总结

本文分析 Vue 对 data 的处理,通过 observe 处理,将它们转为响应式对象。Watcher 管理页面渲染,Dep 管理 Watcher,而对象中每个属性都有自己的 Dep。Dep、 Wathcer 关系是由属性的 getter / setter 触发。

参考

Vue 技术内幕