Vue源码之数据的observer

所以我们打开 core/instance/state.js 文件 找到 initState 函数

export function initState (vm: Component) {
  vm._watchers = [] //储存watcher对象
  const opts = vm.$options  //options引用
  if (opts.props) initProps(vm, opts.props)  //初始化props
  if (opts.methods) initMethods(vm, opts.methods) //初始化方法
  if (opts.data) {
    initData(vm)     //初始化data
  } else {
    observe(vm._data = {}, true /* asRootData */) //这里是data为空时observe 函数观测一个空对象:{}
  }
  if (opts.computed) initComputed(vm, opts.computed) //初始化computed
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }  //初始化watch
}

现在要看的是这一段

if (opts.data) {
    initData(vm)     //初始化data
  } else {
    observe(vm._data = {}, true /* asRootData */) //这里是data为空时observe 函数观测一个空对象:{}
  }

初始化data的过程 首先判断 opts.data 是否存在,即 data 选项是否存在,如果存在则调用 initData(vm) 函数初始化 data 选项,否则通过 observe 函数观测一个空的对象,并且 vm._data 引用了该空对象。其中 observe 函数是将 data 转换成响应式数据的核心入口,

在core/instance/state.js 文件,initData 函数的一开始是这样一段代码

let data = vm.$options.data  //定义data对象
data = vm._data = typeof data === 'function'
  ? getData(data, vm)  执行这个函数
  : data || {}    
  //这里是初始化data第一步就是执行data的构造函数 因为之前都是包装成函数的 但是为什么还要判断呢
  因为 beforeCreate 生命周期钩子函数是在 mergeOptions 函数之后 initData 之前被调用的,如果在 beforeCreate 生命周期钩子函数中修改了 vm.$options.data 的值,那么在 initData 函数中对于 vm.$options.data 类型的判断就是必要的了
export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}  这里的pushTarget(),popTarget()其实是在props数据和data初始化时的收集冗余依赖的 等到后面再说

总直到目前位置正常情况就是通过getData函数获取data选项的数据对象 然后回到initData中

data = vm._data = getData(data, vm) 这里重写了data和vm实例上的_data

接着是个if

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
  )
}

判断data对象是否是个纯对象不是就在生产环境打出警告

再往下是这样的一串代码

const keys = Object.keys(data) //获取data所有的键
const props = vm.$options.props  //获取props引用
const methods = vm.$options.methods  //获取methods引用
let i = keys.length   //遍历key
while (i--) {
  const key = keys[i]
  if (process.env.NODE_ENV !== 'production') {
    if (methods && hasOwn(methods, key)) {  //methods不能和data重名
      warn(
        `Method "${key}" has already been defined as a data property.`,
        vm
      )
    }
  }
  if (props && hasOwn(props, key)) {
  //props不能和data重名
    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)  //isReserved判断key中是否带有$,_这些保留字符防止Vue自生属性方法冲突
    //proxy就是对data的代理访问
  }
}

其中关键点在于 proxy 函数,该函数同样定义在 core/instance/state.js 文件中,其内容如下:

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)
}  通过 Object.defineProperty在_vm上定义和data数据字段同名的访问器属性,其实就是代理到vm._data上的
比如当我们通过 vm.a访问其实就是vm._data.a

经过一系列处理 终于来到关键

observe(data, true /* asRootData */)

给数据添加依赖,即观察者

到这里initData的作用就出来了

  1. 根据 vm.$options.data 选项获取真正想要的数据(注意:此时vm.$options.dat是函数)
  2. 校验得到的数据是否是一个纯对象
  3. 检查数据对象 data 上的键是否与 props 对象上的键冲突
  4. 检查 methods 对象上的键是否与 data 对象上的键冲突
  5. 在 Vue 实例对象上添加代理访问数据对象的同名属性
  6. 最后调用 observe

这里先理解一下Vue的数据响应思想 在Vue中我们一啊不能可以用$watch来监听一个数据

const obj = new Vue({
  data: {
    a: 1
  }
})

obj.$watch('a', () => {
  console.log('修改了 a')
})
obj.a = 2 //console.log('修改了 a')

所以这里的watch就是一个监听实现函数

function $watch('要监听的数据','依赖即数据变化后的回掉'){
}

其实原理都知道 就是Object.defineProperty去修改访问器属性

Object.defineProperty(data, 'a', {
  set () {
    console.log('设置了属性 a')
  },
  get () {
    console.log('读取了属性 a')
  }
})

即属性拦截,所以大致的思想就有了 收集依赖 添加拦截器

const dep = [] //存储依赖
Object.defineProperty(data, 'a', {
  set () {
    // 当属性被设置的时候,将队列里的依赖都执行一次
    dep.forEach(fn => fn())
  },
  get () {
    // 当属性被获取的时候,把依赖放到队列里
    dep.push(fn)
  }
})

但是新问题出现了 怎么在访问a的时候获取fn呢 重点是当我们watch时是能够知道 监听的是谁

const data = {
  a: 1
}

const dep = []
Object.defineProperty(data, 'a', {
  set () {
    dep.forEach(fn => fn())
  },
  get () {
    // 此时 Target 变量中保存的就是依赖函数
    dep.push(Target)
  }
})

// Target 是全局变量
let Target = null
function $watch (exp, fn) {
  // 将 Target 的值设置为 fn
  Target = fn  
  // 读取字段值,触发 get 函数
  data[exp]
}

这样就能简单实现一个watch,但是我们怎么能实现多个属性呢 即遍历data

for (const key in data) {
  const dep = []
   let val = data[key] // 缓存字段原有的值
  Object.defineProperty(data, key, {
    set () {
     // 如果值没有变什么都不做
      if (newVal === val) return
      // 使用新值替换旧值
      val = newVal
      dep.forEach(fn => fn())
    },
    get () {
      dep.push(Target)
      return val  // 将该值返回
    }
  })
}

这样就差不多可以了,可是还是有点问题,比对象嵌套呢

a: {
    b: 1
  }
}

故我们要递归定义

function walk (data) {
  for (let key in data) {
    const dep = []
    let val = data[key]
    // 如果 val 是对象,递归调用 walk 函数将其转为访问器属性
    const nativeString = Object.prototype.toString.call(val)
    if (nativeString === '[object Object]') {
      walk(val)
    }
    Object.defineProperty(data, key, {
      set (newVal) {
        if (newVal === val) return
        val = newVal
        dep.forEach(fn => fn())
      },
      get () {
        dep.push(Target)
        return val
      }
    })
  }
}

walk(data)

但是这里的watch就不能实现了 如果按照watch('a.b',fn),这样 data['a.b']是访问不到属性的 故我们需要小小的改造一下下

function $watch (exp, fn) {
  Target = fn
  let pathArr,
      obj = data
  // 检查 exp 中是否包含 .
  if (/\./.test(exp)) {
    // 将字符串转为数组,例:'a.b' => ['a', 'b']
    pathArr = exp.split('.')
    // 使用循环读取到 data.a.b
    pathArr.forEach(p => {
      obj = obj[p]
    })
    return
  }
  data[exp]  
}

其实watch就是一只访问到你定义的字段,然后触发get添加依赖 那要是 exp这里是个函数 函数里有data某个属性呢比如

funciotn render(){
   document.write(`姓名:${data.name}; 年龄:${data.age}`)
}
watch(render,render)

所以还要加上函数判断

if (typeof exp === 'function') {
    exp()
    return
  }

第二个参数依然是 render 函数其实就是将数据相应依赖到dom上去 大致如此

接下来看看observe工厂函数的实现

observe 工厂函数

回到initData函数最后

observe(data, true /* asRootData */)

打开core/observer/index.js 找到observe函数

export function observe (value: any, asRootData: ?boolean): Observer | void {
   //接受两个参数vlue数据 asRootData 数据是否是根级数据  返回空或者检查者对象
  if (!isObject(value) || value instanceof VNode) {
    return    //观测的数据不是一个对象或者是 VNode实例
  }
  let ob: Observer | void  //定义observer实例 并最后返回
  //hasOwn 函数检测数据对象 value 自身是否含有 __ob__ 属性,
  并且 __ob__ 属性应该是 Observer 的实例。如果为真则直接将数据对象自身的 __ob__ 属性的值作为 ob 的值:ob = value.__ob__
  if (hasOwn(value, '__ob__') && value.__ob__    instanceof Observer) {
    ob = value.__ob__  //即value已经被监听了
  } else if ( 
    shouldObserve &&  //shouldObserve core/observer/index.js里 定义变量其实是一个开关 
    !isServerRendering() &&  //用来判断是否是服务端渲染
    (Array.isArray(value) || isPlainObject(value)) && //只有当数据对象是数组或纯对象的时候
    Object.isExtensible(value) &&  //对象可扩展 即不是冻结对象等等
    !value._isVue //不是Vue实例 
  ) {
    ob = new Observer(value)  //创建一个 Observer 实例
  }
  if (asRootData && ob) {
    ob.vmCount++  
  }
  return ob
}

Observer 构造函数

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) {
    // ...
  }

  walk (obj: Object) {
    // ...
  }
  
  observeArray (items: Array) {
    // ...
  }
}

我们应该知道一个Observer有三个属性 value,dep,vmCount 两个方法 walk,observeArray 首先看看构造函数

constructor (value: any) {
  this.value = value  //引用数据对象
  this.dep = new Dep()  //创建一个dep对象,这个dep是之前想到的收集依赖的嘛 其实不是
  this.vmCount = 0 //实例对象的vmCount初始化为0
  def(value, '__ob__', this) //__ob__给数据定义了__ob__ 其实就是当前的Observe实例
  //const data = {
  //a: 1,
  // __ob__ 是不可枚举的属性
  //__ob__: {
    //value: data, // value 属性指向 data 数据对象本身,这是一个循环引用
    //dep: dep实例对象, // new Dep()
    //vmCount: 0
  //}
//}
  if (Array.isArray(value)) {  
    const augment = hasProto  //判断是数组还是一个纯对象
      ? protoAugment
      : copyAugment        //数组处理之后讲
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {   纯对象  
    this.walk(value)       //调用walk方法
  }
}
walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
} //遍历可枚举属性然后为每个属性 defineReactive

这个defineReactive在 core/observer/index.js 其核心心是将数据对象的数据属性转换为访问器属性数据对象的属性设置一对 getter/setter

export function defineReactive (//5个参数
  obj: Object,  //一个对象
  key: string, //对象的key  //在walk里只用了这两个
  val: any, 
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()  //创建dep实例 在之前创建Observer时其也有一个 __ob__.dep 这两个作用是不同的
  //这里dep才是我们之前想到的dep
  //首先通过 Object.getOwnPropertyDescriptor 函数获取该字段可能已有的属性描述对象讲该对象放入property 
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return //若是存在且不能修改 就返回
  }
 //取得之前的get 和set引用
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  //当传递两个参数时 并且没有getter或者有setter 这里就是一个特殊情况 
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  //childOb !shallow 情况下 observe(val)递归调用深度监听
  
  //非深度观测的场景,即 initRender 函数中在 Vue 实例对象上定义 $attrs 属性和 $listeners 属性时就是非深度观测
  //defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true) // 最后一个参数 shallow 为 true
//defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)

//observe(val) 深度观测数据对象时,这里的 val 未必有值所以必须在满足 没有get但是有set 且参数为2时才能够取到val值
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true, //可枚举
    configurable: true, //可修改
    get: function reactiveGetter () {
      // 省略...
    },
    set: function reactiveSetter (newVal) {
      // 省略...
    }
  })
}

这里之后数据data对象变成了什么呢

const data = {
  // 属性 a 通过 setter/getter 通过闭包引用着 dep 和 childOb
  a: {
    // 属性 b 通过 setter/getter 通过闭包引用着 dep 和 childOb
    b: 1
    __ob__: {a, dep, vmCount}
  }
  __ob__: {data, dep, vmCount}
}  //a里的 childOb ===data.a.__ob__ b的childOb就是undefined

这里Observer 已经执行完了,因为set,get是之后才能触发的 先来看看get

get: function reactiveGetter () {
  const value = getter ? getter.call(obj) : val //正确取到原值 从缓存的getter或者是直接获取到的val 保证getter正常运作
  if (Dep.target) {    //if里收集依赖  Dep.target其实就是 保存的观察者
    dep.depend() //将 依赖收集到本dep中
    if (childOb) {   //子实例存在
      childOb.dep.depend()   
      if (Array.isArray(value)) {  //若读取值是一个数组 就dependArray依次读取每个元素依赖   对象和数组是不同处理的
        dependArray(value)
      }
    }
  }
  return value
}

这里有个重点就是依赖即在本身的dep中也在childOb.dep中

其实就是 data.a.dep和data.a.ob.dep里 第一个”dep“里收集的依赖的触发时机是当属性值被修改时触发,即在 set 函数中触发:dep.notify() 而第二个”dep“里收集的依赖的触发时机是在使用 $set 或 Vue.set 给数据对象添加新属性时触发, 所以就 ob.dep和dep存的是相同的依赖

Vue.set = function (obj, key, val) {
  defineReactive(obj, key, val)
  obj.__ob__.dep.notify()
} //假设set

如上代码所示,当我们使用上面的代码给 data.a 对象添加新的属性: Vue.set(data.a, 'c', 1) 这里就能够触发收集在data.a.ob.dep里的依赖了 也就是data.a的依赖 ob_ 属性以及 ob.dep 的主要作用是为了添加、删除,属性时有能力触发依赖,而这就是 Vue.set 或 Vue.delete 的原理

set如何触发依赖

set: function reactiveSetter (newVal) {
  const value = getter ? getter.call(obj) : val //取的原属性值
  /* eslint-disable no-self-compare */
  if (newVal === value || (newVal !== newVal && value !== value)) {
    return
  }    //新旧值全等或者 新值 旧值自身不等   NaN === NaN // false

  /* eslint-enable no-self-compare */
  if (process.env.NODE_ENV !== 'production' && customSetter) {
    customSetter()   //非生产环境   customSetter就是上面讲的定义$attr里的第四个参数 其实就是打印一下辅助函数说明只读 
  }
  if (setter) {
    setter.call(obj, newVal)   //如果有缓存setter就触发setter
  } else {
    val = newVal   //设置新值
  }
  childOb = !shallow && observe(newVal) //为新值创建observe是实例
  dep.notify() //依次执行依赖
}

这里再提一下前面的代码

if ((!getter || setter) && arguments.length === 2) {
  val = obj[key]
}

该if有两个条件其一是(!getter || setter)其二arguments.length 满足这些条件才会到obj[key]上取值 否则不会触发取值 所以也不会深度观察 对于第二个条件,很好理解,当传递参数的数量为 2 时,说明没有传递第三个参数 val,那么当然需要通过执行 val = obj[key] 去获取属性值。比较难理解的是第一个条件,即 (!getter || setter)要理解这个问题你需要知道 Vue 代码的变更,以及为什么变更。其实在最初并没有上面这段 if 语句块,在 walk 函数中是这样调用 defineReactive 函数的:

walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    // 这里传递了第三个参数
    defineReactive(obj, keys[i], obj[keys[i]])
  }
}

//现在
walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    // 在 walk 函数中调用 defineReactive 函数时暂时不获取属性值
    defineReactive(obj, keys[i])
  }
}

即obj[keys]z在walk内部获取只有在属性没有 get 函数 其实就是当属性值本身有get函数时不触发而等到真正调用时再去触发 而且时如果一属性本身有自己的getter函数才会去深度观测 有两方面的原因,第一:由于当属性存在原本的 getter 时在深度观测之前不会取值,所以在在深度观测语句执行之前取不到属性值从而无法深度观测。第二:之所以在深度观测之前不取值是因为属性原本的 getter 由用户定义,用户可能在 getter 中做任何意想不到的事情,这么做是出于避免引发不可预见行为的考虑

哪为什么还必须要存在set呢 数据对象的某一个属性只拥有 get 拦截器函数而没有 set 拦截器函数时,此时该属性不会被深度观测 是经过 defineReactive 函数的处理之后,该属性将被重新定义 getter 和 setter,此时该属性变成了既拥有 get 函数又拥有 set 函数。并且当我们尝试给该属性重新赋值时,那么新的值将会被观测。这时候矛盾就产生了。

定义响应式数据时行为的不一致:原本该属性不会被深度观测,但是重新赋值之后,新的值却被观测了

为了解决这个问题,采用的办法是当属性拥有原本的 setter 时,即使拥有 getter 也要获取属性值并观测之

你可能感兴趣的:(vue)