[关闭]
@frank-shaw 2019-11-30T10:19:08.000000Z 字数 5683 阅读 1453

vue源码阅读(二):数据响应式与实现

vue 源码 数据响应式


这节课我们来详细了解数据响应式的原理与具体实现。聊到Vue的数据响应,很多人都会对其非侵入式的数据响应系统津津乐道,大概都知道它是通过数据劫持的方式(修改Object.defineProperty())来轻量化实现数据响应式。

所谓轻量化,指的是:Vue中的数据模型仅仅是一个个普通的javascript对象,当使用者修改它们的时候,对应的页面UI会进行更新。无需直接操作DOM元素的同时,对于数据模型的状态管理也变得简单明了。

原理

回顾上节课,我们讲到了Watcher: 它与Vue组件实例之间是一一对应的关系。说点题外话:实际上啊,在Vue1.x系列的时候,每一个响应式变量都会有一个Watcher。开发者发现这样的粒度太细了,于是在Vue2.x的时候,就变成了更高粒度的划分:一个Vue组件实例对应一个Watcher。

Vue的数据化响应的具体实现,实际上依赖的还有两个重要成员:Observer与Dep。Observer、Dep、Watcher三者之间的关系在Vue2.x中可通过下图简单展示:

屏幕快照 2019-11-20 上午7.53.04.png-63.9kB

Observer、Dep、Watcher之间,通过发布-订阅模式的方式来进行交互。在数据初始化的时候,Watcher就会订阅对应变量的Dep。当有数据变化的时候,Observer通过数据劫持的方式,将数据的变更告知Dep,而Dep则会通知有关联关系的Watcher进行数据更新。正如上一节课讲到的,Watcher的notify过程中调用了updateComponent,其包含了两个重要步骤:render与update。这两个步骤最终会更新真实页面。

在一个Vue组件实例中,Watcher只有一个。而实例中的每一个响应式变量都会有一个Dep。于是,一个组件中的Watcher与Dep之间的关系,是一对多的关系。

而现实应用中,Vue组件肯定不止一个啊。组件内部还会嵌套组件,而响应式变量有可能会与多个组件产生关联。于是,在这个层面上,Dep会对应多个Watcher。

综上,Watcher与Dep之间,是多对多的关系。

源码解析

我们的目标是:尝试通过阅读源码的方式,将整个知识点串起来。

沿着上节课的引子,我们可以在src/core/instance/init.js文件的initState()函数中查看:

  1. function initState (vm: Component) {
  2. vm._watchers = []
  3. const opts = vm.$options
  4. if (opts.props) initProps(vm, opts.props)
  5. if (opts.methods) initMethods(vm, opts.methods)
  6. if (opts.data) {
  7. initData(vm)
  8. } else {
  9. observe(vm._data = {}, true /* asRootData */)
  10. }
  11. if (opts.computed) initComputed(vm, opts.computed)
  12. if (opts.watch && opts.watch !== nativeWatch) {
  13. initWatch(vm, opts.watch)
  14. }
  15. }

initState中有很多需要初始化的属性:props/methods/coumputed。我们此时只关注data部分。留意到observe()方法,进入src/core/observer/index.js(与数据响应式相关的代码都是src/core/observer/内)可知:

  1. function observe (value: any, asRootData: ?boolean): Observer | void {
  2. if (!isObject(value) || value instanceof VNode) {
  3. return
  4. }
  5. let ob: Observer | void
  6. if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
  7. ob = value.__ob__
  8. } else if (
  9. shouldObserve &&
  10. !isServerRendering() &&
  11. (Array.isArray(value) || isPlainObject(value)) &&
  12. Object.isExtensible(value) &&
  13. !value._isVue
  14. ) {
  15. ob = new Observer(value)
  16. }
  17. if (asRootData && ob) {
  18. ob.vmCount++
  19. }
  20. return ob
  21. }

Observer

可以看到,observe()的作用就是返回一个Observer对象。于是重点来了:

  1. export class Observer {
  2. value: any;
  3. dep: Dep;
  4. vmCount: number; // number of vms that have this object as root $data
  5. //值得留意:Observer对象在一个Vue组件实例中存在多个,取决于data数据嵌套了几个Object对象或数组
  6. constructor (value: any) {
  7. this.value = value
  8. this.dep = new Dep()
  9. this.vmCount = 0
  10. def(value, '__ob__', this)
  11. //如果是数组
  12. if (Array.isArray(value)) {
  13. if (hasProto) {
  14. protoAugment(value, arrayMethods)
  15. } else {
  16. copyAugment(value, arrayMethods, arrayKeys)
  17. }
  18. this.observeArray(value)
  19. } else {
  20. //如果是对象
  21. this.walk(value)
  22. }
  23. }

只看是对象的情况,于是进入到walk()方法:

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

这里就可以知道,Observer的作用就是:针对data对象的每一个属性,分别对其进行数据响应化处理。值得留意:Observer对象在一个Vue组件实例中存在多个,取决于data数据嵌套了几个Object对象或数组。

Observer是如何与Dep、Watcher关联起来的?我们先来看看Dep、Watcher长啥样子,然后再来进入到最核心的defineReactive()

Dep

看看Dep的结构吧:

  1. export default class Dep {
  2. static target: ?Watcher;
  3. id: number;
  4. subs: Array<Watcher>;
  5. constructor () {
  6. this.id = uid++
  7. this.subs = []
  8. }
  9. addSub (sub: Watcher) {
  10. this.subs.push(sub)
  11. }
  12. removeSub (sub: Watcher) {
  13. remove(this.subs, sub)
  14. }
  15. depend () {
  16. if (Dep.target) {
  17. Dep.target.addDep(this)
  18. }
  19. }
  20. notify () {
  21. // stabilize the subscriber list first
  22. const subs = this.subs.slice()
  23. for (let i = 0, l = subs.length; i < l; i++) {
  24. subs[i].update()
  25. }
  26. }
  27. }

从内部变量属性可知,其包含的静态变量target是一个Watcher,其包含的常规变量subs是Watcher数组。其内部主要的两个方法:depend()关联对应的Watcher,notify()通知对应的Watcher进行update操作。

Watcher

Watcher的结构如下:

  1. export default class Watcher {
  2. vm: Component;
  3. expression: string;
  4. cb: Function;
  5. id: number;
  6. deep: boolean;
  7. user: boolean;
  8. lazy: boolean;
  9. sync: boolean;
  10. dirty: boolean;
  11. active: boolean;
  12. deps: Array<Dep>;
  13. newDeps: Array<Dep>;
  14. depIds: SimpleSet;
  15. newDepIds: SimpleSet;
  16. before: ?Function;
  17. getter: Function;
  18. value: any;
  19. get () {
  20. pushTarget(this)
  21. let value
  22. const vm = this.vm
  23. try {
  24. value = this.getter.call(vm, vm)
  25. } catch (e) {
  26. if (this.user) {
  27. handleError(e, vm, `getter for watcher "${this.expression}"`)
  28. } else {
  29. throw e
  30. }
  31. } finally {
  32. // "touch" every property so they are all tracked as
  33. // dependencies for deep watching
  34. if (this.deep) {
  35. traverse(value)
  36. }
  37. popTarget()
  38. this.cleanupDeps()
  39. }
  40. return value
  41. }
  42. addDep (dep: Dep) {
  43. const id = dep.id
  44. if (!this.newDepIds.has(id)) {
  45. this.newDepIds.add(id)
  46. this.newDeps.push(dep)
  47. if (!this.depIds.has(id)) {
  48. dep.addSub(this)
  49. }
  50. }
  51. }
  52. update () {
  53. /* istanbul ignore else */
  54. if (this.lazy) {
  55. this.dirty = true
  56. } else if (this.sync) {
  57. this.run()
  58. } else {
  59. queueWatcher(this)
  60. }
  61. }
  62. }

这里对Watcher的代码进行了一定的简化。通过声明可知:deps变量是Dep数组。其核心方法有这三个:addDeps()与Dep之间相互关联,get()调用updateComponent方法,update()执行批量更新操作。

Dep中的subs为Watcher数组,Watcher中的deps为Dep数组。也验证了之前的描述:

Watcher与Dep之间,是多对多的关系。

defineReactive

此刻,我们进入到最核心的defineReactive()

  1. export function defineReactive (
  2. obj: Object,
  3. key: string,
  4. val: any,
  5. customSetter?: ?Function,
  6. shallow?: boolean
  7. ) {
  8. //一个key 一个dep(),一一对应
  9. const dep = new Dep()
  10. ...//忽略
  11. //val如果是对象或者数组,会递归observe
  12. //每一个对象或数组的出现,都会出现一个新的Observer
  13. let childOb = !shallow && observe(val)
  14. Object.defineProperty(obj, key, {
  15. enumerable: true,
  16. configurable: true,
  17. get: function reactiveGetter () {
  18. const value = getter ? getter.call(obj) : val
  19. //
  20. //值得注意的是,在initState的时候,并没有触发Dep.target,因为还没有Watcher生成,
  21. //Watcher的产生是在第一次$mounted的过程中生成的
  22. //而以后每次触发Dep.pushTarget的时候,都会将Dep.target再次被引用到具体的Watcher
  23. //比如:
  24. // watcher.js中的 get()
  25. // state.js中的 getData()
  26. // lifecycle.js中的 callHook()
  27. if (Dep.target) {
  28. //depend()是相互添加引用的过程
  29. //一个Vue实例只有一个Watcher,一个key就有一个Dep
  30. //在单一Vue组件实例中,Watcher与Dep之间,是一对多的关系
  31. //考虑到Vue实例存在嵌套(或用户手写了watch表达式),Dep中会保存多个Watcher(存在subs数组中)
  32. //这样,当key发生变化时,对应的Watcher的notify()方法就会被触发,对应Vue实例就会更新页面
  33. dep.depend()
  34. if (childOb) {
  35. childOb.dep.depend()
  36. if (Array.isArray(value)) {
  37. dependArray(value)
  38. }
  39. }
  40. }
  41. return value
  42. },
  43. set: function reactiveSetter (newVal) {
  44. const value = getter ? getter.call(obj) : val
  45. ...//忽略
  46. if (getter && !setter) return
  47. if (setter) {
  48. setter.call(obj, newVal)
  49. } else {
  50. val = newVal
  51. }
  52. //特别处理:如果最新赋值是对象,该对象仍然需要响应化处理
  53. childOb = !shallow && observe(newVal)
  54. //Dep通知更新
  55. dep.notify()
  56. }
  57. })
  58. }

从代码可知:

  1. 每一个data变量都会有一个Dep。
  2. get data变量的时候,会触发dep.depend(),将Dep与Watcher之间进行关联。
  3. set data变量的时候,会触发dep.notify(),通知Dep对应的Watcher进行对应的更新操作。

关联上节课讲到的,Watcher更新的过程会触发updateComponent,于是会重新执行$._render()函数与$._update()函数,生成虚拟DOM,进而更新真实DOM操作。

于是,这个针对对象的数据响应化过程,就基本走通了。下一课,我们来看看,针对数组的数据响应化过程是怎样的,它与对象的响应化过程有何不同?

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注