数据响应式原理

发布于 2021-12-27  419 次阅读


数据响应式

所谓数据响应式,不过是当依赖发生变化的时候,目标(视图)自动更新。


学习目标

  1. 搞清楚什么是响应式
  2. Vue怎么知道我们数据更新了
  3. 模拟数据响应
  4. 通过阅读Vue2源码,理解Vue的双向数据绑定原理,可以跟面试官拉扯
  5. 什么是Watcher
  6. 什么是Dep

学习过程

所谓数据响应式,不过是当依赖发生变化的时候,目标(视图)自动更新。

所以,要想理解数据响应式,我们先来尝试一下怎么让数据自动发生变化。

1. 让一个数据会变

模拟数据响应式,即当数据的依赖发生变化的时候,target也发生变化。如:

let c = a+b;

这里我们把c称为targeta,bc的依赖,因为c是根据ab得出来的。

那么我们怎么让c在a或者b发生变化得时候,c也跟着发生变化呢?

1.1 我首先想到的是,将c放在一个函数里面,当a或者b发生变化的时候,我们调用一下那个函数,让c回炉重造不就可以变化了吗?

让我们来测试一下我的想法。

"use strict";

let a = 1
let b = 2
let c = null;

function fn(){
  c =a+b;
}
console.log('1-',a,b,c)         // 1- 1 2 null
// 明显这里c 为null

// 调用一下函数,让c得到初始化
fn()

console.log('2-',a,b,c)         // 2- 1 2 3
// 到了这里c=3

// 依赖发生变化
a = 9
console.log('3-',a,b,c)         // 3- 9 2 3
// 这里a发生变化, 但是c并没有发生变化

fn()
console.log('4-',a,b,c)         // 4- 9 2 11
// 调用fn之后c=11,发生了变化

很明显到了这里,我们手动调用函数target确实会更新,但是老是手动的话,感觉怪怪的,那有没有什么办法,可以让他自动调用呢?


到了这里我们要解决的问题有两个:

1. 怎么知道自动数据发生变化
2. 怎么自动调用一个特定函数

1.2 我想到的办法是红宝书里面看到的Object.defineProperty(),利用Object.defineProperty()getter以及setter的拦截特性, 让我们来测试一下。

"use strict";

function say() {
  console.log('hello')
}

function defineReactive(obj, key, val) {
return Object.defineProperty(obj, key, {
get() {
  console.log('get->', key)
  return val
},

set(newVal) {
  if (newVal === val) return;
  console.log(set ${key} from ${val} to ${newVal})

  // 数据发生变化,我们就调用函数
  say()
  val = newVal
}
})
}

let source = {}
defineReactive(source, 'a', 1)
console.log(source.a)
source.a = 99
console.log(source.a)

结果:

image-20210916201641080

可以看到我们对a的get以及set 都被识别到了,而且say函数也被成功调用了。

那我们怎么复现c = a + b 这个例子呢?如下:

"use strict";
function defineReactive(obj, key, val) {
   return Object.defineProperty(obj, key, {
       get() {
          console.log('get->', key)
          return val
       },

       set(newVal) {
          if (newVal === val) return;
          console.log(set ${key} from ${val} to ${newVal})
          // 数据发生变化,我们就调用函数
          fn()
          val = newVal
       }
  })
}

let source = {}
defineReactive(source, 'a', 1)
defineReactive(source, 'b', 2)

let c;
function fn(){
   c = source.a + source.b;
   console.log('c自动变化成为',c)
}

// 初始化一下c
fn()

source.a = 99

image-20210916202724599

我们发现fn虽然被自动调用了,但是c的值依然是3,那应该怎么解决呢?

经过查看发现是set里面的问题(先调用了fn导致val还没来得及发生变化。)

set(newVal) {
     if (newVal === val) return;
     console.log(set ${key} from ${val} to ${newVal})
     // 数据发生变化,我们就调用函数
     val = newVal
     fn()
}

上面的问题就解决了。

2. 让一个对象会变

因为对象操作比较复杂,所以我们先实现对对象操作的拦截,比如对象的获取与设置我都知道。

2.1 拦截到对对象属性的获取与设置

新加observe对一个对象的属性遍历进行重新定义(类似于定义一个数据可变)

"use strict";
/**
 * 这里的defineReactive实际上是一个闭包,
 * 外面的对面引用着函数内的变量,导致这些临时变量一直存在
 */
function defineReactive(obj, key, val){
    // 2. observe 避免key的val是一个对象,对象里面的值没有响应式
    observe(val)
    // 利用getter setter 去拦截数据
    Object.defineProperty(obj,key, {
        get(){
            console.log('get', key)
            return val
        },
        set(newVal){
            if( newVal !== val){
                console.log(`set ${val} -> ${newVal}`)
                val = newVal
            }
        }
    })
}

// 2. 观察一个对象,让这个对象的属性变成响应式
function observe(obj){
    // 希望传入的是一个Object

    if( typeof obj !== 'object' || typeof(obj) == null){
        return ;
    }
    Object.keys(obj).forEach(key=>{
        defineReactive(obj, key, obj[key])
    })
}

let o = { a: 1, b: 'hello', c:{age:9}}
observe(o)

o.a
o.a = 2
o.b
o.b = 'world'

image-20210916212224806

2.2 让对象属性变化

为了简化程序,我们只看一层的对象

"use strict";
const { log } = console;

let target = null;
let data = { a: 1, b:2 }
let c, d;

// 依赖收集,每个Object的key都有一个Dep实例
class Dep{
    constructor(){
        this.deps = []
    }
    depend(){
        target && !this.deps.includes(target) && this.deps.push(target)
    }
    notify() {
        this.deps.forEach(dep=>dep() )
    }
}

Object.keys(data).forEach(key=>{
    let v = data[key]
    const dep = new Dep()

    Object.defineProperty(data, key, {
        get(){
            dep.depend()
            return v;
        },
        set(nv){
            v = nv
            dep.notify()
        }
    })
})

function watcher(fn) {
    target = fn
    target()
    target = null
}

watcher(()=>{
    c = data.a + data.b
})

watcher(()=>{
    d = data.a - data.b
})

log('c=',c)
log('d=',d)
data.a = 99
log('c=',c)
log('d=',d)

/**
c= 3
d= -1 
c= 101
d= 97 
 */
  • 简述一下这个过程:

对data对象里面的每一个key利用defineProperty进行数据拦截,在get里面进行Dep依赖收集,在set里面通知数据更新。

依赖收集实则是将watcher实例加入deps队列,当接到通知更新的时候,对队列里面的函数遍历执行,达到数据自动更新的效果。

3. 源码阅读

在阅读源码的时候,为了我们方便寻找入口,我们先来看看官网对数据响应式的阐述。

image-20210919120423474

看完官方给的图,我们可以明确知道,Watcher的粒度是组件,也就是说,每一个组件对应一个Watcher

那么Watcher究竟是什么呢?Dep又是什么? Observer又是做什么用的?下面让我们到源码中去寻找答案吧。

测试代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src='../dist/vue.js'></script>
</head>
<body>
    <div id="app">
        {{a}}
    </div>

    <script>
        const app = new Vue({
            el:'#app',
            data: {
                a: 1
            },
            mounted(){
                setInterval(()=>{
                    this.a ++
                }, 3000)
            }

        })
    </script>
</body>
</html>

跟踪调用栈

image-20210919161332436

自己画的一个流程图

Vue响应式

talk is cheap, show me the code

特别声明,为简化流程,所有的源码展示,均经过删减

src\core\instance\index.js

一个入口文件

function Vue (options) {
  /**
   * 初始化
   */
  this._init(options)
}
/**
 * 以下通过给Vue.prototype挂载的方法,混入其他方法
 */
initMixin(Vue)    
/** 
 * initMixin
通过该方法,给Vue提供__init方法, 初始化生命周期
initLifecycle   -> initEvents  -> initRender
-> callHook(vm, 'beforeCreate') -> initInJections
-> initState  -> initProvide  
-> callHook(vm, 'created')
-> if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
*/

stateMixin(Vue)  
/**
stateMixin
$data   -> $props   -> $set   -> $delete    -> $watch
 */

eventsMixin(Vue)   
/** eventsMixin
$on  $once  $off  $emit
 */

lifecycleMixin(Vue) 
/** lifecycleMixin
 * _update(), $forceUpdate, $destroy 
 * 
 */
renderMixin(Vue)
/**
 * $nextTick, _render, $vnode
 *  
 * */     

export default Vue

src\core\instance\init.js

/**

* initMixin

通过该方法,给Vue提供__init方法, 初始化生命周期

initLifecycle -> initEvents -> initRender

-> callHook(vm, 'beforeCreate') -> initInJections

-> initState -> initProvide

-> callHook(vm, 'created')

-> if (vm.$options.el) {

vm.$mount(vm.$options.el)

}

*/

export function initMixin (Vue: Class<Component>) {

    /**
     * @重要 数据初始化,响应式
     * $set  $delete $watch
     * 在reject之后,初始化数据,达到去重的效果
     */
    initState(vm)

    /**
     * 调用created钩子函数
     */
    callHook(vm, 'created')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

src\core\instance\state.js\initState

对data进行预处理

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  /**
   * state的初始化顺序
   * props -> methods -> data -> computed -> watch
   */
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) { initData(vm) }
  if (opts.computed) initComputed(vm, opts.computed)
}

src\core\instance\state.js\initData

对data进行observe,对数据的getter,setter拦截

function initData (vm: Component) {
  let data = vm.$options.data
  // proxy data on instance
  /**
   * 数据代理
   */
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
      /**
       * @代理
       */
      proxy(vm, `_data`, key)

  }
  /**
   * @响应式操作
   */
  observe(data, true /* asRootData */)
}

`src\core\instance\state.js\proxy

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

/**
 * 
  proxy(vm, `_data`, key)   168行
 */
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

src\core\observer\index.js\observe

创建观察者实例

export function observe (value: any, asRootData: ? boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  /**
   * @观察者
   */
  let ob: Observer | void
  ob = new Observer(value)

  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob

}

src\core\observer\index.js\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

    /**
     * @思考为什么在Observer里面声明dep
     * 创建Dep实例
     * Object 里面新增或者删除属性
     * array 中有变更方法
     */
    this.dep = new Dep()
    this.vmCount = 0

    /**
     * 设置一个—个 __ob__ 属性,引用当前Observer实例
     */

    /**
     * 
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
     */
    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 through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

src\core\observer\dep.js\Dep类

依赖收集

subs是一个Watcher队列

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 * dep是一个可观察对象,可以有多个指令订阅它。
 */
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)
  }

  depend () {
    Dep.target && Dep.target.addDep(this)
  }

  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

src\core\observer\watcher.js\Watcher类

/**
*观察者解析表达式,收集依赖项,
*并在表达式值改变时触发回调。
*这用于$watch() api和指令。
 */
export default class Watcher {
  constructor () {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)

    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    ...
    popTarget()
    this.cleanupDeps()
    return value
  }

  /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

学习成果

声明,以下所说的data如果没有特别声明,都是指,定义组件时预定义的data对象。

1. 什么是响应式

c=a+b说明,当a或者b发生变化的时候,c也会跟着发生变化,这就是数据响应式的本质。

2. Vue怎么知道数据更新?

Vue2.x对用户定义的data,利用Object.defineProperty进行重定义然后才挂载到组件上,当组件获取或者更新数据,会触发getter或setter,也就让组件知道了用户对数据进行了“操作”。

3. 什么是Watcher

在Vue里面一个组件对应着一个Watcher,我们称之为“订阅者”。

它的作用是:订阅数据的变化的并执行相应操作(例如更新视图update)。

4. 什么是Dep

组件data对象的每一个key都对应者一个Dep实例。

Dep是一个可观察类,当他被实例化之后,Watcher就可以订阅它。

Vue会在Dep里面进行依赖收集。

Dep有一个类属性,Target,用来存放Watcher订阅者,他是依赖收集的关键。

  1. 当data的属性被get之后,会调用dep.depend()。
  2. 而在dep.depend函数中,如果存在Dep.target,则会通知与之对应的Watcher添加依赖
  3. 当data的属性key被set(也就是更新的时候),调用与之对应dep.notify(),notify会调用所有订阅它的Watcher进行uodate更新

5. 什么是Observer?

当组件调用observe(data)的时候,创建Observer实例,他会将data利用Objce.property重新定义然后挂载到组件上。

我们称之为“观察者”,因为他通过观察data然后将data转化成为一个数据响应式的data,有人开玩笑的说,从此data经过了“社会主义的洗礼”,已经是一个成熟的社会主义接班人。值得注意的是每一个Observer实例,也有一个唯一的Dep实例与之对应。

6.vue里面有多少种不同Watcher?

因为Watcher是用来更新的,所以我们可以想一下Vue里面有多少个场景需要进行数据更新。

比如:

  • 数据变 → 使用数据的视图变
  • 数据变 → 使用数据的计算属性变 → 使用计算属性的视图变
  • 数据变 → 开发者主动注册的watch回调函数执行

三个场景,对应三种watcher:

  • 组件的Watcher,即render-watcher
  • 用户定义的计算属性对应的computed-watcher
  • 用户定义的监听属性对应的Watcher(watch-api或watch属性)

image-20210919174420862

image-20210919174646319

image-20210919174813857

面试题: 请谈谈你对数据响应式原理的理解

首先我们可以看一下c=a+b,在这条等式里面,c是我们的目标数据,a和b都是c的依赖,当a或者b发生变化的时候,c会自动进行更新,就是我所理解的数据响应式。

而在Vue里面跟数据响应式有关的主要有Dep, Watcher,以及Observer三个类。

在我们创建组件的时候,vue会对用户预定义的data进行observe创建一个Observer实例,将data对象利用Object.defineProperty()的getter以及setter进行重定义,使后面对data所做的所有操作,均可被组件察觉。

然后我们获取Watcher实例的时候,会先调用Watcher里面的get函数,这个get函数,会将当前触发的依赖push到targetStack里面,然后触发这个依赖也就是我们前面定义的obj.key的getter,在这个getter里面,如果Dep.target不为空则调用depend进行依赖收集。

当我们再次更次数据的时候,触发的obj.key的setter会调用与之对应Dep实例的notify函数, notify遍历订阅者队列,调用所有订阅了这个key依赖的Watcher的update函数进行数据更新。从而达到数据响应的目的。

请问面试官我的理解是否有错?

image-20210919120423474


见其广知其深