都2022了,还不懂Vue的响应式原理?

352 阅读3分钟

响应式对象

大家都知道Vue.js实现响应式是用Object.defineProperty函数,那么我们通过下面例子看看怎么变成了响应式对象。

const app = new Vue({

  el: '#demo',
  template: `
    <div>{{message}}</div>
  `,
  data: {
    message: 'hello Vue!',
  },
  watch: {
    'message': function(val, oldVal) {
      console.log(val, oldVal)
    }
  },
  mounted: function () {
    this.message = 'hello world!'
  },
})

app.$mount()

在Vue初始化阶段,我们重点分析一下data属性的处理。

initData

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : 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
  observe(data, true /* asRootData */)
}

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

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

initData函数做了2件事情

  1. 通过proxy函数,将data的属性代理到vm实例上,所以我们可以通过this.message访问到data上的属性。
  2. observe(data)

observe函数new了一个Observer实例

observe

export function observe (value: any, asRootData: ?boolean): Observer | void {
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

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
    def(value, '__ob__', this)
    this.walk(value)
  }

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

export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

Observer构造函数做了2件事:

  1. def函数给value对象添加了__ob__实例
  2. 遍历value调用defineReactive函数,给我们value对象添加上响应式

我们分析下defineReactive源码,是如何给data函数返回的对象,添加上响应式的

defineReactive

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      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 (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

data函数返回的对象,我们通过defineReactive函数遍历,给对象上的每个属性添加上getter,setter方法,使对象变成一个响应式的对象,如果有子对象,通过observe方法递归子对象,使无论多么复杂的对象,我们访问data的属性,都能触发到gettersetter

每个key都创建了dep实例,在gettersetter函数里面用来触发和收集依赖,如何触发和收集依赖,下面来讲。

收集依赖和派发更新

在了解收集依赖和派发更新之前,根据我们的例子,先问自己几个问题:

  1. 什么时候创建dep实例?
  2. 什么时候创建watcher实例?
  3. dep实例和watcher实例之间的逻辑关系是什么?

如果能把这几个问题解释清楚,我们对响应式的了解就比较清楚了。

第一个问题,什么时候创建了dep实例

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      ...
      if (Dep.target) {
        dep.depend()
      }
    },
    set: function reactiveSetter (newVal) {
      ...
      dep.notify()
    }
  })
}

当我们将对象变成响应式时,会给对象的每个属性创建一个dep实例,同时利用闭包的方法,在gettersetter方法中访问dep实例。
根据我们的例子,message属性拥有一个dep(id是3)实例。

image.png

第二个问题,什么时候创建了watcher实例

根据我们的例子:

 watch: {
    'message': function(val, oldVal) {
      console.log(val, oldVal)
    }
  },

我们定义的watch监听,会创建一个watcher(id是1)的实例。如下图:

image.png 当我们app.$mounted()时,会调用mountComponent函数。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  let updateComponent
  updateComponent = () => {
      vm._update(vm._render(), hydrating)
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  return vm
}

我们先说下这个函数的作用

  1. new Watcher会执行updateCompontent函数
  2. vm._render()会生成渲染的VNode,在这个过程中触发vm上的数据访问,触发数据对象的getter
  3. vm._update()将dom树更新为新的节点

根据我们的例子,我们就创建出来一个监控template模板的watcher(id是2)实例 image.png

我们梳理一下逻辑,我们创建了2个watcher实例,针对message属性,会触发两次getter函数,对应的dep收集两次依赖。

depwatcher之间的逻辑关系

我们先看下简化后的DepWatcher源码:

Dep

Dep类比较简单,有一个容器subs数组,可以添加和移除watcher实例,dependnotifyWatcher类一起讲。

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 () {
    if (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()
    }
  }
}

Watcher

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    ...
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
      }
    }
    this.value = this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    popTarget()
    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)
      }
    }
  }
  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
      this.run()
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      const value = this.get()
      // set new value
      const oldValue = this.value
      this.value = value
      this.cb.call(this.vm, value, oldValue)
    }
  }
}

首先根据我们我们监听watch这个例子来讲

watch: {
    'message': function(val, oldVal) {
      console.log(val, oldVal)
    }
  },

new watcher的时候,会调用watcher(id是1)的get方法

 get () {
    pushTarget(this)
    let value
    const vm = this.vm
    value = this.getter.call(vm, vm)
    popTarget()
    return value
  }

pushTarget(this)popTarget()函数,这两个函数在Dep类中定义

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    ...
  }

}
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

根据我们的例子

  1. Dep.target等于watcher(id是1)实例。
  2. value = this.getter.call(vm, vm)触发messagegetter方法。
  3. message属性的dep(id是3)实例调用depend函数
  4. Dep.target(watcher(id是1)的实例)调用addDep函数
  5. dep(id是3)将watcher(id是1)添加到subs数组中

最终的结果如下

image.png

在根据template中对message的调用分析watcher的过程

 template: `
    <div>{{message}}</div>
  `,

mountComponent函数中,new watcher的时候,会调用watcher(id是2)的get方法

过程同上:

  1. Dep.target等于watcher(id是2)实例。
  2. value = this.getter.call(vm, vm)触发messagegetter方法。
  3. dep(id是3)实例调用depend函数
  4. Dep.target(watcher(id是2)的实例)调用addDep函数
  5. dep(id是3)将watcher(id是2)添加到subs数组中

结果如下图: image.png 这样,针对message属性,我们对应的dep实例(id是3)的依赖已经收集完成。

当在mounted修改message属性的时候,触发messagesetter函数。

mounted: function () {
    this.message = 'hello world!'
  },

根据我们的例子:
messagedep(id是3)实例,会将subs数组中存的2个watcher实例,调用自身的update函数。接着会执行watchercb函数和更新template的视图。

总结

首先我们将data函数返回的对象变成一个响应式对象,对每个属性创建一个dep实例容器,针对watcher,computedmount分别创建watcher实例,在getter函数中,将watcher收集到对应dep容器中,修改当前属性时,触发setter函数,将dep容器中的所有watcher进行更新。