Vue2源码,响应式原理,数组

这篇文章将介绍Vue2的响应式数据如何处理数组的情况以及介绍Vue2的响应式数组有哪些不足

参考《深入浅出Vue.js》,Vue2源码

1. 如何拦截数组

Object.defineProperty这个API无法拦截数组的原型方法,也无法拦截数组的索引赋值length属性的修改

js 复制代码
let arr = ['1']
Object.defineProperty(arr, 1, {
    value: '1',
    configurable: true,
    writable: true,
    enumerable: true,
    getter: function (val) {
        // 不会触发
        console.log(val, 'val');
        return val;
    },
    setter: function (val) {
        // 不会触发
        console.log(val, 'val');
    }
})
arr[1] = 3
arr.push(2)
arr.length = 0

我们能做的是增加一个拦截器,真正执行方法时使用的是拦截器中的方法,在拦截器里面使用数组的原型方法

js 复制代码
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
[
    'push',
    'pop',
    'unshift',
    'shift',
    'splice',
    'sort',
    'reverse'
].forEach(method =>{
    const original = arrayProto[method]
    Object.defineProperty(arrayMethods, method, {
        // 最终执行的是这个方法
        value: function mutator (...args) {
            // 执行original,this指向这里
            return original.apply(this, args)
        },
        enumerable: false,
        writable: true,
        configurable: true
    })
})

如上代码中,以Array.prototype为原型,创建了一个新的对象arrayMethods;使用Object.defineProperty对每个方法如push pop等进行拦截,当调用数组方法时执行的实际上是value对应的mutator方法,mutator里面original.apply是使用原生的Array的原型方法操作数组

2. 拦截器只修改响应式数据

如下,我们把arrayMethods仅仅赋值给响应式数据的__proto__属性,这样就不会污染全局数组的原型,只有响应式数据的原型受到影响

diff 复制代码
class Observer {
    constructor (value) {
        this.value = value
        // 如果是数组
        if (Array.isArray(value)) {
+            value.__proto__ = arrayMethods
        }
        // 如果是对象
        else {
            this.walk(value)
        }
    }

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

判断是否有__proto__属性

js 复制代码
// 是否有__proto__属性
const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

如果有__proto__属性,调用protoAugment直接赋值原型;如果没有,则调用copyAugment把拦截器中的方法挂载到value上

js 复制代码
class Observer {
    constructor (value) {
        debugger
        this.value = value
        // 如果是数组
        if (Array.isArray(value)) {
            // 覆盖数组的原型
            // value.__proto__ = arrayMethods
            // 修改为
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arrayKeys)

        }
        // 如果是对象
        else {
            this.walk(value)
        }
    }

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

        observeArray (items) {
            // 数组所有子数据转化为响应式
            debugger
            for (let i = 0, l = items.length; i < l; i++) {
                debugger
                observe(items[i])
            }
        }
    }

    // def工具函数
    function def (obj, key, val, enumerable) {
        // 添加__ob__属性用
        Object.defineProperty(obj, key, {
            value: val,
            enumerable: !!enumerable,
            configurable: true,
            writable: true
        })
    }

    function protoAugment (target, src, keys) {
        debugger
        target.__proto__ = src
    }
    // 如果不支持__proto__,直接将arrayMethods上面的方法挂载到到被侦测的数组上
    function copyAugment (target, src, keys) {
        for (let i = 0, l = keys.length; i < keys.length, i < l; i++) {
            const key = keys[i]
            def(target, key, src[key])
        }
    }
}

3. 收集依赖

Vue2把数组的依赖保存在Observer类上,所以要在Observer类上绑定一个dep数组,同时记录__ob__属性,他的值就是当前Observer实例

这样以后响应式数据可以直接通过this.__ob__拿到Observer实例,进而拿到dep依赖数组,往里面存数据

diff 复制代码
class Observer {
    constructor (value) {
        this.value = value
+        this.dep = new Dep()
+        def(value, '__ob__', this)
        // 如果是数组
        if (Array.isArray(value)) {
            // 覆盖数组的原型
            // value.__proto__ = arrayMethods
            // 这样也可以实现
            // Object.setPrototypeOf(value, arrayMethods)

            // 修改为
            debugger
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arrayKeys)

            this.observeArray(value)
        }
        // 如果是对象
        else {
            this.walk(value)
        }
    }

    ......
}

如下代码,在defineReactive函数内调用observe函数,该函数会返回这个val对应的Observer实例。在下面会进一步介绍该方法。

封装observe方法,判断如果数据身上挂载__ob__属性,表示已经创建过Observer实例了直接返回;否则针对该数据创建一个新的Observer类,返回ob也就是Observer实例。这是__ob__的另一个作用

在getter中判断如果有childOb,则执行childOb.dep.depend(),是针对数组进行的依赖收集

diff 复制代码
function defineReactive (data, key, val) {
+    let childOb = observe(val) // 数组的Observer实例
    let dep = new Dep()
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            // 为对象收集依赖
            dep.depend()
            // 为数组收集依赖
+            if (childOb) {
+                childOb.dep.depend()
+            }
+            return val
        },
        set: function (newVal) {
            if (val === newVal) {
                return
            }
            val = newVal
            // 通知依赖
            dep.notify()
            console.log(val, 'val');
        }
    })
}

function observe (value) {
    debugger
    // 不是对象
    if (!isObject(value)) return
    let ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        // 已经存在响应式属性ob => 数组已经是响应式数据了
+        ob = value.__ob__
    } else {
        // 数组此时还不是响应式数据
+        ob = new Observer(value)
    }
    return ob
}

对应源码

diff 复制代码
/src/core/observer/index.ts
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean,
  observeEvenIfShallow = false
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if (
    (!getter || setter) &&
    (val === NO_INITIAL_VALUE || arguments.length === 2)
  ) {
    val = obj[key]
  }
   // 调用observe方法,兼容数组的响应式情况
+  let childOb = shallow ? val && val.__ob__ : observe(val, false, mock)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        if (__DEV__) {
          dep.depend({
            target: obj,
            type: TrackOpTypes.GET,
            key
          })
        } else {
          dep.depend()
        }
+        if (childOb) {
+          childOb.dep.depend()
+          if (isArray(value)) {
+            dependArray(value)
+          }
+        }
      }
      return isRef(value) && !shallow ? value.value : value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (!hasChanged(value, newVal)) {
        return
      }
      if (__DEV__ && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else if (getter) {
        // #7981: for accessor properties without setter
        return
      } else if (!shallow && isRef(value) && !isRef(newVal)) {
        value.value = newVal
        return
      } else {
        val = newVal
      }
      childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)
      if (__DEV__) {
        dep.notify({
          type: TriggerOpTypes.SET,
          target: obj,
          key,
          newValue: newVal,
          oldValue: value
        })
      } else {
        dep.notify()
      }
    }
  })

  return dep
}

对应的observe的函数

diff 复制代码
/src/core/observer/index.ts
export function observe(
  value: any,
  shallow?: boolean,
  ssrMockReactivity?: boolean
): Observer | void {
// 若有无__ob__属性直接返回
+  if (value && hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
+    return value.__ob__
+  }
  if (
    shouldObserve &&
    (ssrMockReactivity || !isServerRendering()) &&
    (isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value.__v_skip /* ReactiveFlags.SKIP */ &&
    !isRef(value) &&
    !(value instanceof VNode)
  ) {
      // 否则创建并返回Observer实例
+    return new Observer(value, shallow, ssrMockReactivity)
  }
}

4. 发送通知

如下在拦截器中,通·this.__ob__拿到Observer实例,通过ob.dep.notify通知所有依赖进行更新

diff 复制代码
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto);
['push',
'pop',
'unshift',
'shift',
'splice',
'sort',
'reverse'].forEach(method =>{
    const original = arrayProto[method]
    def(arrayMethods, method, function mutator (...args) {
        const result = original.apply(this, args)
+        const ob = this.__ob__     
+        ob.dep.notify()
        return result
    })
})

5. 递归侦测数组中的所有元素

通过observeArray方法,将数组的每一项数据转化成响应式

diff 复制代码
class Observer {
    constructor (value) {
        this.value = value
        this.dep = new Dep()
        // 将__ob__属性存到Observer实例上
        def(value, '__ob__', this)
        // 如果是数组
        if (Array.isArray(value)) {
            // 覆盖数组的原型
            // value.__proto__ = arrayMethods
            // 这样也可以实现
            // Object.setPrototypeOf(value, arrayMethods)

            // 修改为
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arrayKeys)

+            this.observeArray(value)
        }
        // 如果是对象
        else {
            this.walk(value)
        }
    }

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

+    observeArray (items) {
+        // 数组所有子数据转化为响应式
+        for (let i = 0, l = items.length; i < l; i++) {
+            observe(items[i])
+        }
+    }
}

6. 侦测新增数据

如下,如果是新增元素会用到push unshift splice等方法,如果inserted有值,则调用observeArray对新增的数据进行遍历

diff 复制代码
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto);
['push',
'pop',
'unshift',
'shift',
'splice',
'sort',
'reverse'].forEach(method =>{
    const original = arrayProto[method]
    def(arrayMethods, method, function mutator (...args) {
        const result = original.apply(this, args)
        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)
+        }
        ob.dep.notify()
        return result
    })
})

splice方法的处理是,如果调用list.splice(0, 1, 33),则args[0, 1, 33]inserted值就是33,此时ob.observeArray就会对33进行判断是否要转化为响应式数据

对应源码

js 复制代码
// src/core/observer/array.ts
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator(...args) {
    const result = original.apply(this, args)
    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)
    // notify change
    if (__DEV__) {
      ob.dep.notify({
        type: TriggerOpTypes.ARRAY_MUTATION,
        target: this,
        key: method
      })
    } else {
      ob.dep.notify()
    }
    return result
  })
})

7. 不足

目前的实现,无法拦截直接通过length修改者是通过索引修改情况,只实现了对数组原型方法的拦截

js 复制代码
this.list.length = 0
this.list[0] = 1
相关推荐
Asort16 分钟前
JavaScript 从零开始(六):控制流语句详解——让代码拥有决策与重复能力
前端·javascript
无双_Joney35 分钟前
[更新迭代 - 1] Nestjs 在24年底更新了啥?(功能篇)
前端·后端·nestjs
在云端易逍遥37 分钟前
前端必学的 CSS Grid 布局体系
前端·css
ccnocare38 分钟前
选择文件夹路径
前端
艾小码38 分钟前
还在被超长列表卡到崩溃?3招搞定虚拟滚动,性能直接起飞!
前端·javascript·react.js
闰五月39 分钟前
JavaScript作用域与作用域链详解
前端·面试
泉城老铁43 分钟前
idea 优化卡顿
前端·后端·敏捷开发
前端康师傅43 分钟前
JavaScript 作用域常见问题及解决方案
前端·javascript
司宸44 分钟前
Prompt结构化输出:从入门到精通的系统指南
前端