乐趣区

关于前端:浅析-vue2-数据响应式原理

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 = 1
Object.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)

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

退出移动版