Vue响应式原理(看这一篇就够了)

你肯定听说过Object.denfineProperty或是Proxy\reflect,这的确是在VUE响应式原理中起重要作用的一部分代码,但这远远不能代表整个流程的精妙。上图:

                           

不懂没关系,请往下看~

一、initData

见名知意,这里是获取你输入数据开始的地方。通过 vm.$options.data(vm是viewModel,也就是连接view和model(js)的vue桥梁)获取到你定义的data,但是这里在真正对你的数据进行处理之前需要进行异常处理例如:

1)常见的将data写为data:{name:'zhangkankan'}的形式(考虑到组件复用问题,所以选择的是return对象方式不断开辟存储空间存储同一component的不同数据,防止组件之间相互影响)

2)遍历每一个data中你传入的key防止key重复。

3)比较你的data中是否有和props重名的key。

等等.......

通过以上的异常检查,才是认可了你的这个data(){return{内容}}形式。于是现在开始拿到return的内容对象对其经行处理。进入到observe(data, true /* asRootData */)

二、Observe()

现在将刚才的data对象传入,请注意这里:

  if (!isObject(value) || value instanceof VNode) {
    return
  }

在这里我们采用逆向思维哈:1)刚传入的data已经是对象了为什么还要判断?2)vnode和这里没关系为什么还要判断?

第一个问题说明这段代码(即observe函数)是被重复使用的,也就是说会有不是对象的数据传来(后面会讲),同时要知道return的data中的array也算是obj类型后面对于这个return的类型会产生两种不同做法,后面有讲);第二个问题,vnode是指一个虚拟的node节点,通俗讲就是js根据类制造出来的一个node实例,里面有很多可以描述dom的属性。node组成dom,vnode组成vdom。通过将每一个状态变为每一个不是真正dom但却包含dom内容可为树状的实例结构,于是便可以用js实例的方式不断new出一整个dom树,也就是

虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼 —— vue 官网。

某个vnode如下表示:

{
    sel: 'div',// 表述标签,如p、sapn
    data: {
        
    },
    children: undefined,
    text: 'virtual dom', //标签内的文本,
    ele: undefined,
    key: undefined
}

这样的做法便于,如果存在数据更改需要重新渲染视图的时候,不是将之前新和旧dom整个树对比(全部查找一遍,找到需要修改的部分),而是只改这个node节点的实例所对应部分即可。(从侧面说明,对每一个vnode的改变是有observe的,一旦vnode里面属性变化,就重新绘制,并且只改这个部分,这也是为什么这里要排除当前对象为VNode的实例,因为当前是对data进行监控而不是vnode)。

接着主线向下

再排除是其他类型且不是vnode之后,会进行判断。

if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
//如果你这个data对象有_ob_属性并且这个属性也是一个实例?
//因为new observer后会对每一个对象上加一个_ob_,并且def(value, '__ob__', this),
//即将value的值赋给_ob_,这样做的目的是防止同一个data对象杯多次new观测,说明这个data已经被注册过了,于是拿出之前的那个data属性对这个ob赋值(实际上就是同一个),等于没有操作
  } else if (
    //巴拉巴拉一堆判断
  ) {
    ob = new Observer(value)//对这个对象创建监测
  }
if (asRootData && ob) {
    ob.vmCount++  // number of vms that have this object as root $data 就是用了几次
  }

三、Observer类(重点)

 排除了一系列干扰,现在终于是拿到了data对象对他进行处理了。在这个类中,首先我们会用value保存data的值,同时对data对象开启一个Dep,准备对凡是用到data数据的依赖进行收集。这里开始有分支,如果当时return的是Array类型怎么处理,如果是Object类型怎么处理,两种数据结构构造不同,处理不同(主要是对依赖收集方式不同产出两条分支)。

   if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }

四、return的为Object类型进入walk()-->defineReactive()(重点

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

代码不多,基本意思很明确拿到每一个key和value传入defineReactive();下面进入该函数

技巧一:数据劫持

Object.defineProperty的介绍,我们不难发现,当我们访问或设置对象的属性的时候,都会触发相对应的函数,然后在这个函数里返回或设置属性的值

  既然如此,我们当然可以在触发函数的时候动一些手脚做点我们自己想做的事情,这也就是“劫持”操作

  在Vue中其实就是通过Object.defineProperty来劫持对象属性的setter和getter操作,并“种下”一个监听器。

技巧二:发布订阅者模式

  对于每一个key都有一个对应的dep(订阅对象),通过数据劫持可以收集依赖(即每一个watcher实例),所以一般这个为一个set结构存取。之后如果set函数被触发就会通知dep,dep会调用函数对内存储的watchers进行处理。

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
  const dep = new Dep()//创建订阅对象
 
  const property = Object.getOwnPropertyDescriptor(obj, key)//获取obj对象的key属性的描述
  //属性的描述特性里面如果configurable为false则属性的任何修改将无效
  if (property && property.configurable === false) {
    return
  }
 
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
 
  let childOb = observe(val)//创建一个观察者对象
  Object.defineProperty(obj, key, {
    enumerable: true,//可枚举
    configurable: true,//可修改
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val//先调用默认的get方法取值
      //这里就劫持了get方法,也是作者一个巧妙设计,在创建watcher实例的时候,通过调用对象的get方法往订阅器dep上添加这个创建的watcher实例
//这里是更复杂的多情况判断,比如是数组、或者包有其他类型
      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//先取旧值
      if (newVal === value) {
        return
      }
      //这个是用来判断生产环境的,可以无视
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = observe(newVal)//继续监听新的属性值
      dep.notify()//这个是真正劫持的目的,要对订阅者发通知了,通知里面存储的对应watcher需要有变动了
    }
  })
}

    那么以上便是完成了对于一个data方法return一个object类型内所有属性的监听。从宏观角度来说数据结构模式为weakmap(obj1,dep:map;obj2,dep:map),一个类对应一个map,一个map由key和对应dep组成。

回到之前的另一种类型,如果return的是array类型。

五、return的为Array类型(重点

 有一说一我觉得上面讲的那个对于每一个属性的依赖收集方式设计和下面这个对于arr中每一个元素的设计模式真的是响应式最亮眼的地方。

  下面是new Observe类里面的判断

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

首先:我为什么监听数组?因为数组改变了我数据也应该通知视图重新画画。所以我需要做的是对每一个可以改变数组内容的方法中加入我的其他操作。

1)protoAugment(value, arrayMethods):这个方法其实就是上面一句话的体现,也是对return类型arr操作中很关键的一步,即对传入对象__proto__重写。

__proto__(隐式原型)是每个对象都有的属性,但是prototype(显示原型)是函数才有的原型(所以说函数作为一个由function Function(){}new出来的对象,有两原型,__proto__指向Function的prototype,prototype指向构造器对象)

它不是一个规范属性,该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它。大多数情况下,__proto__可以理解为“构造器的原型”,即__proto__===constructor.prototype。(sorry扯远了)回归正题---------------------------

其中传入的arrayMethods便是可以改变arr内容的所有函数方法,现在对其进行重写

onst arrayProto = Array.prototype//通过原型拿到对象
export const arrayMethods = Object.create(arrayProto)
//创建一个新的对象,使用现有的对象来提供新建对象的__proto__,
// 实际上就是改变了array的prototype,方法都写在现式原型上
//之后到处使用这个__proto__

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]//去除每一个原有方法
  //重定义
  def(arrayMethods, method, function mutator (...args) {
        //说明用了之前的arr定义的原来的方法来处理数据
    const result = original.apply(this, args)

    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()//添加的新功能在这里
    return result
  })
})

2)ob.observeArray方法

//这里是new Observe中对于return是arr类型 的处理
if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    }  

//下面是重写arr的代码片段
switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)


细细观察可发现,不仅在上面这个本修改arr原型方法的函数中调用了这个函数,并且也在判断是array类型后用了这个函数。他的目的在于,push\unshift\splice操作会导致有新的数据进入;并且arr里面本身自己可能就有些元素是obj类型,这也就说明改变arr内容不止是arr的数组操作了,也包含了obj的操作,所以需要对这一个obj的属性进行之前的监听。这也是之前提过的,为什么要复用obseve()函数,因为这个函数是对于obj类型监听的一系列操作。所以我们会再次提到这个

 if (!isObject(value) || value instanceof VNode) {
    return
  }

如果传入的是obj类型,那么对arr中的obj元素每一个key再开启dep,重复之前的操作,否则退出。

现在请回头看看我的那张最顶部的图。

关于如果触发dep开始对每一个watcher的调用方法ob.dep.notify()下篇文章再说~

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值