Vue2 响应式原理

Vue2 响应式原理

  • Vue2 响应式实现原理
    • Object.defineProperty(obj, prop, descriptor)
    • 源码解析
      • 响应式对象
      • Observer 类(观察者)
      • defineReactive 函数
    • 弊端
    • 总结

Vue 是一个典型的 MVVM 模型。
Vue2 响应式原理_第1张图片

MVVM
View层:在 Vue 中是绑定 dom 对象的 HTML
ViewModel 层:在 Vue 中是实例的 vm 对象
Model层:在 Vue 中是 data、computed、methods 等中的数据
在 Model 层的数据变化时,View层会在ViewModel的作用下,实现自动更新

在 Model 层的数据变化时,View层会在ViewModel的作用下,实现自动更新

Vue 响应式原理的核心:数据驱动视图;属于非侵入式响应式系统。

侵入式:需要刻意地去调用他人的 API,才能改变数据的值。
非侵入式:只是更新数据,并没有其他操作。

Vue2 响应式实现原理

Vue2 的响应式主要依靠 ES5 的 Object.defineProperty 实现(这也是为什么 Vue.js 不能兼容 IE8 及以下浏览器的原因)。

Object.defineProperty(obj, prop, descriptor)

数据劫持/数据代理

obj : 是要在其上定义的对象;
prop: 是要定义或修改的属性的名称;
descriptor: 是将被定义或修改的属性描述符。

descriptor 的相关配置项:

{
*// 可枚举*
  enumerable:true,
*// 可以被配置,比如可以被delete*
  configurable:true,
*// 属性对应的值*
  value: 'abc',
*// 是否可写,为 true 时上面的 value 值才能被改变。*
	writable: 'true',
*// getter*
  get() {
    console.log('你试图访问'+ key+ '属性')
		return val
  },
*// setter*
  set(newValue) {
    console.log('你试图改变'+ key+ '属性', newValue)
		if (val=== newValue) {
			return
		}
    val= newValue
  },
}

可见,Object.defineProperty() 方法中存在一个 getter 和 setter 的可选项,可以对属性值的获取和设置造成影响。 Vue 中会编写了一个 wather 来处理数据,在使用 getter 方法时,总会通知 wather 实例对 view 层渲染页面;同样的,在使用 setter 方法时,总会在变更值的同时,通知 wather 实例对 view 层进行更新。
Vue2 响应式原理_第2张图片

源码解析

在 Vue 的初始化阶段,_init 方法执行的时候,会执行 initState(vm) 方法,它的定义在 src/core/instance/state.js 中。

initState 方法 主要是对 props、methods、data、computed 和 wathcer 等属性做了初始化操作。

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

initProps 方法 是 props 的初始化主要过程,就是遍历定义的 props 配置。遍历调用 defineReactive 方法把每个 prop 对应的值变成响应式,可以通过 vm._props.xxx 访问到定义 props 中对应的属性。

initData 方法 是 data 的初始化,主要是对定义 data 函数返回对象的遍历,通过 proxy 把每一个值 vm._data.xxx 都代理到 vm.xxx 上;并且调用 observe 方法 观测整个 data 的变化,把 data 也变成响应式,可以通过 vm._data.xxx 访问到定义 data 返回函数中对应的属性。

上面两个方法都是遍历 props 和 data 里面的数据添加响应式,那么具体是如何添加响应式的呢。

响应式对象

observe

在 vue 初始化的时候,initState 方法中会调用一个方法 observe,就是用来监测数据的变化。

export function observe(value) {
  *// 如果value不是对象,什么都不做*
  if (typeof value != 'object') return
  *// 定义ob*
  var ob
  if (typeof value.__ob__ !== 'undefined') {
    *// 判断对象是否有__ob__这个属性,可以理解为响应式标志*
    ob = value.__ob__
  } else {
    *//如果没有的话就生成一个实例对象,Observer类的内部去转化生成一个响应式对象*
    ob = new Observer(value)
  }
  return ob
}

observe 方法的作用就是给非 VNode 的对象类型数据添加一个 Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例。

Observer 类(观察者)

export default class Observer {
  constructor(value) {
    *// 每一个Observer的实例身上,都有一个dep*
    this.dep = new Dep()
    *// 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)添加了__ob__属性,值是这次new的实例*
    def(value, '__ob__', this, false)
    *// 不要忘记初心,Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object*
    *// 检查它是数组还是对象*
    if (Array.isArray(value)) {
      *// 如果是数组,要非常强行的蛮干:将这个数组的原型,指向arrayMethods*
      Object.setPrototypeOf(value, arrayMethods)
      *// 让这个数组变的observe*
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  *// 遍历*
  walk(value) {
    for (let k in value) {
      defineReactive(value, k)
    }
  }
  *// 数组的特殊遍历*
  observeArray(arr) {
    for (let i = 0, l = arr.length; i < l; i++) {
      *// 逐项进行observe*
      observe(arr[i])
    }
  }
}

Observer 的构造函数逻辑很简单,首先实例化 Dep 对象,接着通过执行 def 函数把自身实例添加到数据对象 value 的 ob 属性上,def 函数就是简单的 Object.defineProperty 的封装,给对象添加__ob__ 属性。

Observer 接下来会对 value 做判断,对于数组会调用 observeArray 方法,否则对纯对象调用 walk 方法。可以看到 observeArray 是遍历数组再次调用 observe 方法,而 walk 方法是遍历对象的 key 调用 defineReactive 方法.

调用数组方法

由于 Object.defineProperty 不支持数组的检测,所以当数据是数组时,为了让其也变成响应式添加__ob__属性,因此需要特殊处理一个数组数据。本质上就是 重写数组上面的七个方法

*// 得到Array.prototype*
**const** arrayPrototype **=** Array.prototype

*// 以Array.prototype为原型创建arrayMethods对象,并暴露*
**export** **const** arrayMethods **=** Object.create(arrayPrototype)

*// 要被改写的7个数组方法*
**const** methodsNeedChange **=** [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse',
]

methodsNeedChange.forEach((methodName) => {
  *// 备份原来的方法,因为push、pop等7个函数的功能不能被剥夺*
  **const** original **=** arrayPrototype[methodName]
  *// 定义新的方法*
  def(
    arrayMethods,
    methodName,
    **function** () {
      *// 恢复原来的功能*
      **const** result **=** original.apply(**this**, arguments)
      *// 把类数组对象变为数组*
      **const** args **=** [...arguments]
      *// 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么已经被添加了?因为数组肯定不是最高层,比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层的时候,已经给g属性(就是这个数组)添加了__ob__属性。*
      **const** ob **=** **this**.__ob__

      *// 有三种方法push\unshift\splice能够插入新项,现在要把插入的新项也要变为observe的*
      **let** inserted **=** []

      **switch** (methodName) {
        **case** 'push'**:case** 'unshift'**:**inserted **=** args
          **breakcase** 'splice'**:***// splice格式是splice(下标, 数量, 插入的新项)*
          inserted **=** args.slice(2)
          **break**}

      *// 判断有没有要插入的新项,让新项也变为响应的*
      **if** (inserted) {
        ob.observeArray(inserted)
      }
      *// 新插入的数据也会触发依赖通知watcher更新视图*
      ob.dep.notify()
      **return** result
    },
    **false**)
})

值得注意的是 push,unshift,splice 三个方法会插入新的数据,此时为了使其也成为响应式数据,需要再次调用 Observer 类里面的 observeArray 方法,遍历每一项使其具有响应式。注意 def 的第三个参数是函数,也就是给定义的新方法名定义的函数,只有调用这个方法名的时候才会调用这些新方法。所以这就是初始化 data 中有数组,不直接调用方法的原因。

defineReactive 函数

在 Observer 类中,普通对象的遍历执行 defineReactive 方法并且传入 2 个参数 ,对象和当前的 key。

defineReactive 的功能就是定义一个响应式对象,给对象动态添加 getter 和 setter。

export default function defineReactive(data, key, val) {
  const dep = new Dep()
  // 如果参数只有两个的话,val值就是传入对象中的那个key的值
  if (arguments.length == 2) {
    val = data[key]
  }

  // 子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数、类循环调用
  let childOb = observe(val) //形成递归的关键

  Object.defineProperty(data, key, {
    // 可枚举
    enumerable: true,
    // 可以被配置,比如可以被delete
    configurable: true,
    // getter
    get() {
      console.log('你试图访问' + key + '属性')
      // 如果现在处于依赖收集阶段
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return val
    },
    // setter
    set(newValue) {
      console.log('你试图改变' + key + '属性', newValue)
      if (val === newValue) {
        return
      }
      val = newValue
      // 当设置了新值,这个新值也要被observe
      childOb = observe(newValue)
      // 发布订阅模式,通知dep
      dep.notify()
    },
  })
}

使用 defineReactive 函数不需要设置临时变量了,而是用闭包。接收的第三个参数 val 就是闭包的形式接收变量值的。

defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。最后利用 Object.defineProperty 去给 obj 的属性 key 添加 getter 和 setter。

弊端

vue 是在初始化时,会对 data 中的所有数据进行遍历调用 Object.defineProperty 方法,设置 getter、setter 响应式处理。如果 data 某个对象新增了属性,进行访问和设值都不会触发 get 和 set,即无响应式。

所以弊端就是 Object.defineProperty只对初始对象里的属性有监听作用,而对新增的属性无效。这也是为什么 Vue2 中对象新增属性的修改需要使用 Vue.$set 来设值的原因。

总结

响应式对象最主要的是 Observer 类(观察者),负责把 对象转化成具有__ob__ 属性的响应式对象,将一个正常的 object 转换为每个层级的属性都是响应式(可以被侦测的) 的 object。

  • Vue2 响应式的核心是通过 Object.defineProperty 拦截对数据的访问和设置

  • 响应式的数据分为两类:

    • 对象,循环遍历对象的所有属性,为每个属性设置 getter、setter,以达到拦截访问和设置的目的,如果属性值依旧为对象,则递归为属性值上的每个 key 设置 getter、setter

      • 访问数据时(obj.key ) 进行依赖收集,在 dep 中存储相关的 watcher
      • 设置数据时由 dep 通知相关的 watcher 去更新
    • 数组,增强数组的那 7 个可以更改自身的原型方法,然后拦截对这些方法的操作

      • 添加新数据时进行响应式处理,然后由 dep 通知 watcher 去更新
      • 删除数据时,也要由 dep 通知 watcher 去更新

参考:
https://zhuanlan.zhihu.com/p/499408009
https://blog.csdn.net/weixin_48186771/article/details/108578630
https://juejin.cn/post/6950826293923414047

你可能感兴趣的:(Vue,vue.js,前端,javascript)