Title:浅析 vue2 数据响应式原理

Date:2022-04-28

Source:ITgo

什么是数据响应式?对象自身对象属性被读和写的时候,咱们须要晓得该数据被操作了,并在这过程中执行一些函数,例如:render函数,而这一过程我把它定义为数据响应式。

那么vue具体是如何实现数据响应式的呢?接下来咱们通过vue的源码探索一下响应式数据的始末。

响应式数据的源码在./src/core/observer上面

在具体实现下面,vue用到了4个核心部件:

  1. Observer
  2. Dep
  3. Watcher
  4. Scheduler

Observer

Observer的目标很简略,它次要就是把一个一般的对象转换成响应式的对象。

那Observer到底是如何做到把一个一般对象转换成响应式对象的呢?

为了实现这一点,Observer通过object.defineProperty将一个一般对象包装成一个带有getter/setter属性的非凡对象,当拜访属性的时候会调用getter,批改属性的时候会调用setter,这样一来,咱们就能够晓得数据什么时候被读写了。

晓得实现逻辑了,那咱们就来实现一个简略的响应式数据吧!

首先咱们先定义一个一般对象

const obj = {    a: 1,    b: 2}console.log(obj);

很显然,这个对象它并不具备响应式,从控制台输入就能够看得出来

接下来咱们通过Object.defineProperty来改写下面的对象

let obj = {  b:2}let val = 1Object.defineProperty(obj, 'a', {    enumerable: true,    configurable: true,    get() {        console.log('a被读取了')        return val    },    set(newVal) {        console.log('a被批改了')        obj.a = newVal    }})

这下就很显著了,a属性和b属性齐全不同了,当对a读写对时候会就回登程相应的getter/setter办法

Observer 的外围代码如下

export class Observer {  value: any;  dep: Dep;  vmCount: number; // number of vms that have this object as root $data  constructor (value: any) {    this.value = value    this.dep = new Dep()    this.vmCount = 0    /*      * 将Observer实例绑定到data的__ob__属性下面去,     * observe的时候会先检测是否曾经有__ob__对象寄存Observer实例了,     * def办法定义能够参考https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js#L16      */    def(value, '__ob__', this)    if (Array.isArray(value)) {      if (hasProto) {        protoAugment(value, arrayMethods)  /*间接笼罩原型的办法来批改指标对象*/      } else {        copyAugment(value, arrayMethods, arrayKeys)  /*定义(笼罩)指标对象或数组的某一个办法*/      }      /*如果是数组则须要遍历数组,将数组中的所有元素都转化为可被侦测的响应式*/      this.observeArray(value)    } else {      /*如果是对象则间接walk进行绑定*/      this.walk(value)    }  }

从Observer的源码能够看出,Observer对对象和数组的响应式解决有所不同,如果是对象就间接调用walk,遍历每一个对象并且在它们下面绑定getter与setter,如果是数组则须要遍历数组,将数组中的所有元素都转化为可被侦测的响应式

1.Object

walk (obj: Object) {    const keys = Object.keys(obj)    /*walk办法会遍历对象的每一个属性进行defineReactive绑定*/    for (let i = 0; i < keys.length; i++) {      defineReactive(obj, keys[i])    }  }function defineReactive (  obj: Object,  key: string,  val: any,  customSetter?: ?Function,  shallow?: boolean) {      //...    /*对象的子对象递归进行observe并返回子节点的Observer对象*/  let childOb = !shallow && observe(val)  Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    get: function reactiveGetter() {       /*如果本来对象领有getter办法则执行*/      const value = getter ? getter.call(obj) : val      // ...      return value    },    set: function reactiveSetter(newVal) {      /*通过getter办法获取以后值,与新值进行比拟,统一则不须要执行上面的操作*/      const value = getter ? getter.call(obj) : val      // ...      val = newVal            /*新的值须要从新进行observe,保证数据响应式*/      childOb = !shallow && observe(newVal)          }  })}

总之就是递归遍历对象的所有属性,以实现深度属性转换

2.Array

如果是数组,vue会重写数组的一些办法,更改Array的隐式原型,之所以要这样做,是因为vue须要监听哪些办法可能扭转数组数据。别离重写了这些办法:push, pop, shift, unshift, splice, sort, reverse

if (Array.isArray(value)) {      if (hasProto) {        protoAugment(value, arrayMethods)  /*间接笼罩原型的办法来批改指标对象*/      } else {        copyAugment(value, arrayMethods, arrayKeys)  /*定义(笼罩)指标对象或数组的某一个办法*/      }      /*如果是数组则须要遍历数组,将数组中的所有元素都转化为可被侦测的响应式*/      this.observeArray(value)}/* 地位:./src/core/observer/array.js * 扭转数组本身内容的7个办法 */const methodsToPatch = [  'push',  'pop',  'shift',  'unshift',  'splice',  'sort',  'reverse']/* * 这里重写了数组的这些办法, * 在保障不净化原生数组原型的状况下重写数组的这些办法, * 截获数组的成员产生的变动, */methodsToPatch.forEach(function (method) {  // cache original method  const original = arrayProto[method]     // 缓存原生办法  def(arrayMethods, method, function mutator (...args) {    const result = original.apply(this, args)  /*调用原生的数组办法*/    /*数组新插入的元素须要从新进行observe能力响应式*/    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)    return result  })})

总结:Observer的指标就是,当对象的属性被读写,数组的数据被增删改时都要被vue感知到。

Dep

Observer只是让vue感知到数据被读写了,然而接下来到底要干什么就须要Dep来解决了。

Dep的含意是Dependency,示意依赖的意思,vue会为对象中的每一个属性,对象自身,数组自身创立一个Dep实例,而每个Dep实例都会做两件事:

  • 收集依赖,即谁在应用该数据,
  • 告诉依赖更新,即当数据产生扭转的时候,告诉依赖更新,

总结一句话就是:在getter中收集依赖,在setter中告诉依赖更新

// ./src/core/observer/index.js/*为对象defineProperty上在变动时告诉的属性*/export function defineReactive (  obj: Object,  key: string,  val: any,  customSetter?: ?Function,  shallow?: boolean) {  /*定义一个dep对象*/  const dep = new Dep()       //...    let childOb = !shallow && observe(val)  Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    get: function reactiveGetter() {           // ...      if (Dep.target) {        /*进行依赖收集*/        dep.depend()        if (childOb) {           /*             * 子对象进行依赖收集,            * 其实就是将同一个watcher观察者实例放进了两个depend中,            * 一个是正在自身闭包中的depend,另一个是子元素的depend            */          childOb.dep.depend()          if (Array.isArray(value)) {            /*是数组则须要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/            dependArray(value)          }        }      }      return value    },    set: function reactiveSetter(newVal) {            // ...            /*dep对象告诉所有的观察者*/      dep.notify()    }  })}
/** * ./src/core/observer/dep.js * A dep is an observable that can have multiple * directives subscribing to it. */export default class Dep {  static target: ?Watcher;  id: number;  subs: Array<Watcher>;  constructor () {    this.id = uid++    this.subs = []  }  /*增加一个观察者对象*/  addSub (sub: Watcher) {    this.subs.push(sub)  }  /*移除一个观察者对象*/  removeSub (sub: Watcher) {    remove(this.subs, sub)  }  /*依赖收集,当存在Dep.target的时候增加观察者对象*/  depend () {    if (Dep.target) {      Dep.target.addDep(this)    }  }  /*告诉所有订阅者*/  notify () {    // stabilize the subscriber list first    const subs = this.subs.slice()    if (process.env.NODE_ENV !== 'production' && !config.async) {      // subs aren't sorted in scheduler if not running async      // we need to sort them now to make sure they fire in correct      // order      subs.sort((a, b) => a.id - b.id)    }    for (let i = 0, l = subs.length; i < l; i++) {      subs[i].update()    }  }}

Watcher

Watcher又是干什么的呢?

dep收集依赖后,当数据产生扭转,筹备派发告诉的时候,不晓得该派给谁,或者说不晓得谁用了该数据,于是就须要watcher了。

当某个函数在执行的过程中,应用到了响应式数据时,vue就会为响应式数据创立一个watcher实例,当数据产生扭转时,vue不间接告诉相干依赖更新,而是告诉依赖对应的watcher实例去执行。

watcher会设置一个全局变量window.targe,让全局变量记录以后负责执行的watcher等于本人,而后在执行函数,在执行的过程中,如果产生了依赖记录dep.depenf(),那么Dep会把这个全局变量记录下来,示意有一个watcher实例用到了这个响应式数据。

watcher外围源代码

export default class Watcher {  constructor (vm,expOrFn,cb) {    this.vm = vm;    this.cb = cb;    this.getter = parsePath(expOrFn)    this.value = this.get()  }  get () {    window.target = this;    const vm = this.vm    let value = this.getter.call(vm, vm)    window.target = undefined;    return value  }  update () {    const oldValue = this.value    this.value = this.get()    this.cb.call(this.vm, this.value, oldValue)  }}/** * Parse simple path. * 把一个形如'data.a.b.c'的字符串门路所示意的值,从实在的data对象中取出来 * 例如: * data = {a:{b:{c:2}}} * parsePath('a.b.c')(data)  // 2 */const bailRE = /[^\w.$]/export function parsePath (path) {  if (bailRE.test(path)) {    return  }  const segments = path.split('.')  return function (obj) {    for (let i = 0; i < segments.length; i++) {      if (!obj) return      obj = obj[segments[i]]    }    return obj  }}

咱们剖析Watcher类的代码实现逻辑:

  1. 当实例化Watcher类时,会先执行其构造函数;
  2. 在构造函数中调用了this.get()实例办法;
  3. get()办法中,首先通过window.target = this把实例本身赋给了全局的一个惟一对象window.target上,而后通过let value = this.getter.call(vm, vm)获取一下被依赖的数据,获取被依赖数据的目标是触发该数据下面的getter,上文咱们说过,在getter里会调用dep.depend()收集依赖,而在dep.depend()中取到挂载window.target上的值并将其存入依赖数组中,在get()办法最初将window.target开释掉。
  4. 而当数据变动时,会触发数据的setter,在setter中调用了dep.notify()办法,在dep.notify()办法中,遍历所有依赖(即watcher实例),执行依赖的update()办法,也就是Watcher类中的update()实例办法,在update()办法中调用数据变动的更新回调函数,从而更新视图。

参考文档

Scheduler

当在setter中调用了dep.notify()办法,在dep.notify()办法中,遍历所有依赖(即watcher实例)时,如果watcher执行重运行对应的函数,就会导致函数频繁执行,从而升高了效率,试想一下,如果一个函数,外面用到了a,b,c,d等响应式数据,这些数据都会记录依赖,于是当这些数据发生变化时会触发屡次更新,例如:

state.a = "new value";state.b = "new value";state.c = "new value";state.d = "new value";...// 每更新一个值触发一次更新

这样显然是不适合的,因而,当watcher收到派发的更新告诉后,watcher不会立刻执行,而是将本人交给一个调度器scheduler

调度器scheduler保护一个执行队列,同一个watcher在该队列中只会存在一次,队列中的watcher不会立刻执行,而是通过nextTick的工具函数执行,nextTick是一个微队列,会把须要执行的watcher放到事件循环的微队列中执行。

nextTick的具体做法是通过Promise实现的,具体实现办法专利临时不探讨,nextTick文档

总结

  1. vue首先通过Observer类,应用Object.defineProperty办法包装了数据,使object变成一个具备getter/setter属性的数据。
  2. 读取数据的时候通过getter办法读取,并在getter办法外面调用了Dep模块的dep.depend()办法收集依赖,并为该依赖创立一个对应的watcher实例。
  3. 通过setter办法扭转数据的时候调用了Dep模块的dep.notify()办法来告诉依赖,即依赖对应的watcher实例,遍历所有的watcher实例。
  4. watcher实例不间接更新视图,而是交给scheduler调度器,scheduler保护一个事件队列通过nextTick执行事件,从而更新视图。

流程图

补充

  1. Observer产生在beforeCreatecreated之间,
  2. 因为遍历时只能遍历到对象的以后属性,因而无奈监测到未来动静减少或删除的属性。

    // html<template>  <div class="hello">    <h1>a:{{ obj.a }}</h1>    <h1>b:{{ obj.b }}</h1>    <button @click="obj.b = 2">Add B</button>  </div></template>
    // js<script>export default {  name: "HelloWorld",  data() {    return {      obj: {        a: 1,      },    };  },};

当点击 Add B动静给obj增加b属性时,obj数据更新了,然而页面没有展现,由此可见之后动静增加和删除的数据不具备响应式个性。

因而vue提供了$set$delete两个实例办法来解决这种状况。

// 新增this.$set(this.obj, b, 2)//删除this.$delete(this.obj, b)

以上仅集体了解,如有不当之处还请不吝赐教