当前位置:   article > 正文

Vue源码阅读(7):将数据转换成响应式的_vue中将数组数据获取长度变为响应式

vue中将数组数据获取长度变为响应式

 我的开源库:

这篇博客主要讲解 Vue 是如何将数据转换成响应式的,将数据转换成响应式是进行依赖收集和变化侦测的基础。

1,对数据响应式处理的入口

第一小节是为了让大家了解响应式处理的代码在整体源码中的位置。

1-1,执行 new Vue(core/instance/index.js)

  1. function Vue (options) {
  2. // 执行 vm 原型上的 _init 方法,该方法在 initMixin 方法中定义
  3. this._init(options)
  4. }
  5. // 写入 vm._init
  6. initMixin(Vue)

vue 里面执行 _init 方法,该方法定义在 initMixin 方法中。

1-2,_init 方法(core/instance/init.js)

  1. export function initMixin (Vue: Class<Component>) {
  2. Vue.prototype._init = function (options?: Object) {
  3. // 初始化 state,包括 props、methods、data、computed、watch
  4. initState(vm)
  5. // 如果配置中有 el 的话,则自动执行挂载操作
  6. if (vm.$options.el) {
  7. vm.$mount(vm.$options.el)
  8. }
  9. }
  10. }

初始化数据的方法是 initState。

1-3,initState 方法(core/instance/state.js)

  1. export 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. // 初始化计算属性
  12. if (opts.computed) initComputed(vm, opts.computed)
  13. // 初始化监听属性
  14. // nativeWatch的作用:Firefox has a "watch" function on Object.prototype...
  15. if (opts.watch && opts.watch !== nativeWatch) {
  16. // 进行侦听属性的初始化过程
  17. initWatch(vm, opts.watch)
  18. }
  19. }

initState 方法内进行了一系列状态的初始化,我们以 initData 为例。

1-4,initData 方法(core/instance/state.js)

  1. // 初始化我们配置中写的 data 对象,传递的参数(vm)是当前 Vue 的实例
  2. function initData (vm: Component) {
  3. // observe data
  4. observe(data, true /* asRootData */)
  5. }

1-5,observe 方法(core/observer/index.js)

  1. export function observe (value: any, asRootData: ?boolean): Observer | void {
  2. // 如果这个值不是一个对象、或者这个值是一个虚拟节点实例的话,在这里直接 return
  3. if (!isObject(value) || value instanceof VNode) {
  4. return
  5. }
  6. // 声明要返回的 ob 变量
  7. let ob: Observer | void
  8. // 如果当前 value 有 __ob__ 属性,且这个属性是 Observer 类的实例的话
  9. // 直接将 value.__ob__ 赋值给 ob
  10. if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
  11. ob = value.__ob__
  12. } else if (
  13. observerState.shouldConvert &&
  14. !isServerRendering() &&
  15. (Array.isArray(value) || isPlainObject(value)) &&
  16. Object.isExtensible(value) &&
  17. !value._isVue
  18. ) {
  19. // 传递进来的 value 并不是响应式的,在这里。通过 new Observer(value) 将其转换成响应式的
  20. // 并且返回 new 出来的实例
  21. ob = new Observer(value)
  22. }
  23. if (asRootData && ob) {
  24. ob.vmCount++
  25. }
  26. return ob
  27. }

在这里通过执行 ob = new Observer(value) 将数据转换成响应式的,所谓响应式的数据是指 Vue 能够侦测到该数据的使用和变更。

2,借助 Observer 类将数据转换成响应式的

响应式的数据是指 Vue 能够侦测到该数据的使用和变更。

侦测数据的使用是为了依赖收集。

侦测数据的变更是为了触发依赖更新

2-1,class Observer(core/observer/index.js)

  1. export class Observer {
  2. // 被处理成响应式的数据,可以是对象类型或者是数组类型
  3. value: any;
  4. dep: Dep;
  5. // number of vms that has this object as root $data
  6. // 将此对象作为根 $data 的 vms 数量
  7. vmCount: number;
  8. constructor (value: any) {
  9. this.value = value
  10. this.dep = new Dep()
  11. this.vmCount = 0
  12. def(value, '__ob__', this)
  13. // 对象类型值和数组类型值有不同的处理,在这里进行 if else 判断。
  14. if (Array.isArray(value)) {
  15. const augment = hasProto
  16. ? protoAugment
  17. : copyAugment
  18. augment(value, arrayMethods, arrayKeys)
  19. this.observeArray(value)
  20. } else {
  21. // 用于将对象中的属性都转换成响应式的
  22. this.walk(value)
  23. }
  24. }
  25. /**
  26. * 该方法用于将 obj 中所有的 key 都转换成响应式的
  27. * 具体的做法是遍历 keys,每个 key 都执行 defineReactive 方法
  28. * defineReactive 方法用于将对象中具体的 key 转换成响应式的
  29. */
  30. walk (obj: Object) {
  31. const keys = Object.keys(obj)
  32. for (let i = 0; i < keys.length; i++) {
  33. defineReactive(obj, keys[i], obj[keys[i]])
  34. }
  35. }
  36. /**
  37. * 用于将数组中的元素都转换成响应式的
  38. */
  39. observeArray (items: Array<any>) {
  40. for (let i = 0, l = items.length; i < l; i++) {
  41. // 对数组中的每个元素都执行 observe 方法
  42. observe(items[i])
  43. }
  44. }
  45. }

Observer 类的构造方法中会判断处理的数据是不是数组类型,对象类型和数组类型的处理方式是不一样的,对象类型会执行 walk 方法。

walk 方法会遍历对象的 key,执行 defineReactive 方法,在 defineReactive 方法中将对象中的 key 都转换成 Object.defineProperty 的形式,这样 Vue 就能监控到对象属性的使用和变更了。

2-2,defineReactive 方法(core/observer/index.js)

  1. export function defineReactive (
  2. // 对象
  3. obj: Object,
  4. // key
  5. key: string,
  6. // 值
  7. val: any,
  8. customSetter?: ?Function,
  9. // 浅的
  10. shallow?: boolean
  11. ) {
  12. // 如果 val 是一个对象类型的话,那么这个 dep 将用于保存 val 的依赖列表
  13. // 数组类型值的依赖列表保存在 observer.dep 中
  14. const dep = new Dep()
  15. const getter = property && property.get
  16. const setter = property && property.set
  17. // 这个 childOb(Observer类的实例)中的 dep 是用来保存数组类型值的依赖的
  18. let childOb = !shallow && observe(val)
  19. Object.defineProperty(obj, key, {
  20. enumerable: true,
  21. configurable: true,
  22. // 在此进行依赖收集
  23. get: function reactiveGetter () {
  24. // 触发执行上面拿到的 getter
  25. const value = getter ? getter.call(obj) : val
  26. / 下面是依赖收集的操作 /
  27. // 如果 Dep 上的静态属性 target 存在的话
  28. if (Dep.target) {
  29. // 向 dep 中添加依赖,依赖是 Watcher 的实例
  30. dep.depend()
  31. if (childOb) {
  32. // childOb.dep 用来存储数组类型值的依赖
  33. childOb.dep.depend()
  34. // 如果值是数组类型的话
  35. if (Array.isArray(value)) {
  36. dependArray(value)
  37. }
  38. }
  39. }
  40. // getter 返回值
  41. return value
  42. },
  43. // 在此进行派发更新
  44. set: function reactiveSetter (newVal) {
  45. // 触发依赖的更新
  46. dep.notify()
  47. }
  48. })
  49. }

在 defineReactive 方法的开头创建了一个 Dep 类的实例,这个 dep 实例是用于存储对象类型值依赖的,我们可以看到在下面的 get 中,执行 dep.depend() 进行依赖的收集,依赖收集的细节,下面再说。

如果数据是数组类型的话,还需要执行数组类型值对应 dep 实例的 depend 方法,数组类型的 dep 实例存储在 Observer 实例上面。关于数组类型值为什么要多这一步处理?以及 dep 为什么存储在 Observer 实例上面,我们在下一小节细讲一下,要不然容易引起困惑。

2-3,数组类型值额外处理的原因

假设上面 defineReactive 方法的 obj 参数为:

  1. let obj = {
  2. names: ['tom', 'jack', 'rose']
  3. }

key 参数为 'names',value 参数为 ['tom', 'jack', 'rose']。

那么上面的这个 names 属性的变更有两种方式:

(1)obj.names = ['小明', '小红', '小山']

(2)obj.names.push('alice')

第一种方式是直接对 obj.names 重新设值,这种变更数据的方式 Object.defineProperty set 是能够侦测到变化的,这在 set 中执行 dep.notify() 触发依赖更新就可以了。

但是第二种方式使用数组原型上的方法变更数据,这种变更方法 Object.defineProperty set 是侦测不到的。所以 Vue 另辟新径,重写了数组的原型方法,如果用户执行数组的原型方法变更数组的话,Vue 就能够在重写的原型方法中执行依赖更新的操作。

Vue 在重写的原型方法中执行依赖更新操作的前提是:在重写的原型方法中能够拿到这个数组对应的 dep 实例,而在 defineReactive 方法中创建的 dep 实例,在重写的原型方法中是获取不到的。Vue 的解决方案是在 Observer 实例上创建一个专门服务于数组收集依赖的 dep 实例,因为Observer 的实例会被定义在值的 '__ob__' 属性上,所以在数组的原型方法中,可以通过 this.__ob__.dep 获取到数组对应的 Dep 实例,这就解决了问题。相关代码如下:

(1)将为数组类型值的 dep 设值到 Observer 实例上

  1. export class Observer {
  2. value: any;
  3. dep: Dep;
  4. constructor (value: any) {
  5. this.value = value
  6. this.dep = new Dep()
  7. def(value, '__ob__', this)
  8. }
  9. }

 (2)在 get 中为数组类型值收集依赖,依赖存储到 childOb.dep 中

  1. // 这个 childOb(Observer类的实例)中的 dep 是用来保存数组类型值的依赖的
  2. let childOb = !shallow && observe(val)
  3. Object.defineProperty(obj, key, {
  4. enumerable: true,
  5. configurable: true,
  6. // 在此进行依赖收集
  7. get: function reactiveGetter () {
  8. // 触发执行上面拿到的 getter
  9. const value = getter ? getter.call(obj) : val
  10. / 下面是依赖收集的操作 /
  11. // 如果 Dep 上的静态属性 target 存在的话
  12. if (Dep.target) {
  13. // 向 dep 中添加依赖,依赖是 Watcher 的实例
  14. dep.depend()
  15. if (childOb) {
  16. // childOb.dep 用来存储数组类型值的依赖
  17. childOb.dep.depend()
  18. // 如果值是数组类型的话
  19. if (Array.isArray(value)) {
  20. dependArray(value)
  21. }
  22. }
  23. }
  24. // getter 返回值
  25. return value
  26. },
  27. // 在此进行派发更新
  28. set: function reactiveSetter (newVal) {
  29. ......
  30. }
  31. })

 (3)在重写的原型方法中,获取 dep,并且触发依赖更新

  1. [
  2. 'push',
  3. 'pop',
  4. 'shift',
  5. 'unshift',
  6. 'splice',
  7. 'sort',
  8. 'reverse'
  9. ]
  10. .forEach(function (method) {
  11. def(arrayMethods, method, function mutator (...args) {
  12. const result = original.apply(this, args)
  13. const ob = this.__ob__
  14. // 通知 ob.dep 中的依赖
  15. ob.dep.notify()
  16. // 在最后,返回 Array 方法执行的结果
  17. return result
  18. })
  19. })

2-4,数组类型数据原型方法的重写

  1. export class Observer {
  2. constructor (value: any) {
  3. this.value = value
  4. this.dep = new Dep()
  5. def(value, '__ob__', this)
  6. if (Array.isArray(value)) {
  7. const augment = hasProto
  8. ? protoAugment
  9. : copyAugment
  10. augment(value, arrayMethods, arrayKeys)
  11. } else {
  12. // 用于将对象中的属性都转换成响应式的
  13. this.walk(value)
  14. }
  15. }
  16. }

hasProto 方法用于判断当前的浏览器环境支不支持 __proto__。

如果支持原型的话,就将重写的数组原型方法赋值到数组的 __proto__ 属性上。

  1. function protoAugment (target, src: Object, keys: any) {
  2. /* eslint-disable no-proto */
  3. target.__proto__ = src
  4. /* eslint-enable no-proto */
  5. }

如果不支持 __proto__ 的话,直接将重写的原型方法赋值到数组值上面。

  1. function copyAugment (target: Object, src: Object, keys: Array<string>) {
  2. for (let i = 0, l = keys.length; i < l; i++) {
  3. const key = keys[i]
  4. def(target, key, src[key])
  5. }
  6. }

加下来看看 Vue 是如何重写原型方法的,上面的 arrayMethods 就是包含重写原型方法的对象。

  1. // 拿到 Array 的 prototype 原型对象
  2. const arrayProto = Array.prototype
  3. // 利用 Object.create() 创建一个新的对象,并且这个新的对象的原型链(__proto__)指向 arrayProto。
  4. // 这样的话,我们只需要将一些需要改写的方法定义到 arrayMethods 对象中即可。
  5. // 这样的话,我们既可以访问到 arrayMethods 对象中已经改写了的方法,也能访问到 arrayProto 对象中未改写的方法
  6. // ^o^ 完美!
  7. export const arrayMethods = Object.create(arrayProto)
  8. // 能够改变数组内容方法的数组
  9. ;[
  10. 'push',
  11. 'pop',
  12. 'shift',
  13. 'unshift',
  14. 'splice',
  15. 'sort',
  16. 'reverse'
  17. ]
  18. // 进行遍历处理
  19. .forEach(function (method) {
  20. // 缓存原生的相应方法
  21. const original = arrayProto[method]
  22. // 定义该 method 对应的自定义方法
  23. def(arrayMethods, method, function mutator (...args) {
  24. // 执行原生方法拿到执行结果值,在最后将这个结果值返回
  25. const result = original.apply(this, args)
  26. // 这里的 this 是执行当前方法的数组的实例。在 Vue 中,每个数据都会有 __ob__ 属性,这个属性
  27. // 是 Observer 的实例,该实例有一个 dep 属性(Dep 的实例),该属性能够收集数组的依赖
  28. const ob = this.__ob__
  29. // 数组有三种新增数据的方法。分别是:'push','unshift','splice'
  30. // 这些新增的数据也需要变成响应式的,在这里,使用 inserted 变量记录新增的数据
  31. let inserted
  32. switch (method) {
  33. // 如果当前的方法是 push 或者 unshift 的话,新增的数据就是 args,将 args 设值给 inserted 即可
  34. case 'push':
  35. case 'unshift':
  36. inserted = args
  37. break
  38. // 如果当前的方法是 splice 的话,那么插入的数据就是 args.slice(2)
  39. case 'splice':
  40. inserted = args.slice(2)
  41. break
  42. }
  43. // 如果的确新增了数据的话,将 inserted 作为参数执行 observer.observeArray() 方法,把新增的每个元素都变成响应式的
  44. if (inserted) ob.observeArray(inserted)
  45. // 通知 ob.dep 中的依赖
  46. ob.dep.notify()
  47. // 在最后,返回 Array 方法执行的结果
  48. return result
  49. })
  50. })

(1)首先使用变量 arrayProto 保存数组原先的原型对象。

(2)然后创建对象 arrayMethods,原型链指向 arrayProto。

(3)最后在 arrayMethods 中重写那些能够改变数组本身的原型方法,在重写的原型函数中会执行 ob.dep.notify() 

这样做的好处是:只重写了那些能够改变数组内容的原型方法,其余的原型方法借助于原型链还使用其原本实现。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/2023面试高手/article/detail/259161
推荐阅读
相关标签
  

闽ICP备14008679号