Vue 响应式原理

深入响应式原理(第四章)

响应式对象:

什么是响应式对象?当我们随手写一个对象的时候:

var obj = {
    a:10,
    b:20
} 

其实这不是响应式对象,因为它没有能力来判断我们是否对对象进行操作。我们举个例子,让我们更加的对响应式对象有一个了解。尤大曾经在它演讲的源码解析中举过一个这样的例子。

我们有一个对象obj。他有两个属性a/b。它们的值都是数字,我们有一个要求,每次属性b输出的时候都是a值的两倍。即:console.log(obj.b)。那么我们该如何做到呢?

var obj = {
    a:10,
    b:20
} 

其实这里有一个隐含的问题,那就是a属性的值是可以随时变化的,我们是不知道的。那么如果让属性a的改变影响到属性b呢?此时我们就需要了解一个函数:Object.defineProperty()

Object.defineProperty()

方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

参数:

  • obj:要定义或修改属性的对象
  • key:要定义或修改属性的名称
  • descriptor:要定义或修改的属性描述符。
descriptor
  • configurable

    当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认为 false

  • enumerable

    当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。 默认为 false

  • value

    该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为 undefined

  • writable

    当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符改变。 默认为 false

存取描述符还具有以下可选键值:

  • get

    属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined

  • set

    属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 undefined

举例:

var obj = {
    a:10
}
Object.defineProperty(obj,'a',{
    get(){
       console.log('你正在访问')
    },
    set(){
        console.log('你正在修改')
    }
})
console.log(obj.a)
obj.a = 20
​
//结果:
你正在访问
undefined 
你正在修改 

你看可以发现,当我们去访问或者修改的时候,obj就知道它被访问或者修改了。那么我们如何让属性b每次输出的时候都是属性a的两倍呢?

var obj = {
    a:10,
    b:20
}
​
function ReactDefineProperty(o){
    const obj = o
    Object.defineProperty(o,'a',{
        set(value){
            obj.b = value*2
        }
    })
}
ReactDefineProperty(obj)
obj.a = 20
console.log(obj.b)
obj.a = 5
console.log(obj.b)
obj.a = 1
console.log(obj.b)
//结果
40
10
2 

其实代码并不难,你会发现:Object.defineProperty将称为响应式原理的关键。

initState()

initState()函数用来处理我们传入的data/props/methods该函数的调用在beforeCreate钩子函数之后,在created钩子函数之前。该函数的代码如下:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)//处理props
  if (opts.methods) initMethods(vm, opts.methods)//处理methods
  if (opts.data) {//处理data
    initData(vm)
  } else {//如果我们没有从传入data,那么就使用一个空对象作为默认的data
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)//处理computed
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)//处理watch
  }
} 

initState()函数对数据做的初始化顺序为:props -> methods -> data -> computed -> watch

我们主要研究的是data/props内部的实现原理。

initData(获取data并判断是否有命名冲突)

该函数的代码如下:

function initData (vm: Component) {
  let data = vm.$options.data//获取data
  
  //获取data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)//如果data是一个函数,那么执行getDate()来拿到内部的data数据
    : data || {}
    
    //判断类型
  if (!isPlainObject(data)) {//对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)//获取data中的key
  const props = vm.$options.props//获取props
  const methods = vm.$options.methods
  let i = keys.length
  
  //判断属性冲突
  while (i--) {//该代码就是这个遍历,看是否methods/props/data中是否有命名冲突
    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)//将我们的data代理到vm._data_上
    }
  }
    
  // observe data
  observe(data, true /* asRootData */)
} 

initData函数的作用也是对data进行一些边界情况的处理兼容,最核心是调用observe()函数。但是在此之前我们要知道initData函数做了哪些事情。首先获取data,因为我们传入的data不一定是对象,所以要处理获取data。然后将data中的keyprops/methods中的key进行比较,看是否有命名冲突。当这些工作全部都准备好之后开始执行observe()函数。

observe()
export function observe (value: any, asRootData: ?boolean): Observer | void {
    
    //对data进一步边缘处理
  if (!isObject(value) || value instanceof VNode) {//如果data不是一个对象,或者说是一个vnode。那么直接返回,不予进行响应式处理
    return
  }
​
  let ob: Observer | void
  
  //判断是否已经处理过了。
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      //判断data是否有__ob__属性,因为这个属性只有经过observe处理过后的对象才具有。
    ob = value.__ob__
  } else if (
    shouldObserve &&//是否要进行observe()处理,默认为true
    !isServerRendering() &&//非服务端渲染
    (Array.isArray(value) || isPlainObject(value)) &&//要么是数组,要么是对象
    Object.isExtensible(value) &&//对象是可扩展的
    !value._isVue//不是Vue构造函数实例
  ) {
      //我们传给Observer()是data对象
    ob = new Observer(value)
  }
​
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
} 

observe函数的核心是调用new Observer()构造函数,但是在此之前对传入的参数进行了处理。首先如果data不是对象或者是一个vnode,那么则不予处理。判断我们要observe的对象是否已经被observe过了。如果没有那么就进行条件验证:shouldObserve/ !isServerRendering()/(Array.isArray(value) || isPlainObject(value))/ Object.isExtensible(value)/ !value._isVue。当这些条件满足之后调用new Observer()函数。

-> new Observer()
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; 
  constructor (value: any) {
      //这里的this指向的是observer实例,也即是说每一个data中的属性对象都会生成一个对应的observer实例对象
      
      //为对象添加标记。这里并没有特指data,因为data中如果还有子对象的话也会走这一步的
    this.value = value
    this.dep = new Dep()//为该属性对象添加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函数
  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])
    }
  }
} 

这里有一点需要注意,为属性对象创建的ob我们在哪里可以获取到呢?在data.__ob__中可以获取,那和组件又有什么关系呢?我们可以通过vm.$data获取到data,然后通过data.__ob__获取到该组件对应的ob。每一个组件都有一个单独的data,每一个data都有一个ob,通过data.__ob__可以访问。不仅每一个data都有一个__ob__,甚至data中的属性如果是一个对象,那么这个对象也有一个__0b__

对于对象的监听,我们先假设监听的是一个没有引用类型数据的对象,例如:{name:"xz",age:13}。然后再假设监听的是一个有引用类型的数据{name:"xz",friends:{name:'hky'}}

我们向Observer构造函数传入的是data对象,但是返回的ob实例是另一个对象,那么就让我们看看Observer函数到底做了什么事情。

首先将我们的data挂载到ob.value上。然后添加ob.dep属性。def(value,'__data__',this)。这行代码的运行结果是在我们传入的data对象上添加一个__ob__属性,而该属性的值就是我们的ob实例。如果你想想就会发现ob/data这两个对象相互引用,形成闭环。这个是题外话。

其实observe函数的参数是一个对象,不一定是根data。原因是当我们的根data有子对象的时候,也会将子对象进行observe处理,也就是说,会深度遍历子属性,将每一个子对象添加一个__ob__属性。如果我们传入的是对象,则会调用walk()函数。

->->walk() 获取keys
 walk (obj: Object) {
     
     //为每一个key都进行响应式处理
    const keys = Object.keys(obj)//获取value中的key
    for (let i = 0; i < keys.length; i++) {//然后将key逐个进行defineReactive处理
      defineReactive(obj, keys[i])
    }
  } 

walk()函数的作用就是获取所有的key,然后将每一个key都进行defineReactive()处理。

->->->defineReactive()
export function defineReactive ( obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean ) {
    
    //为每一个key都创建一个dep用来存放访问该属性的依赖
 const dep = new Dep()
​
  const property = Object.getOwnPropertyDescriptor(obj, key)//获取到key的描述信息
  if (property && property.configurable === false) {//如果该属性是不可配置的,那么将返回
    return
  }
​
  // cater for pre-defined getter/setters
  const getter = property && property.get//获取该属性原生定义
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值