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
相关推荐
写代码的小王吧1 小时前
【安全】Web渗透测试(全流程)_渗透测试学习流程图
linux·前端·网络·学习·安全·网络安全·ssh
小小小小宇2 小时前
CSS 渐变色
前端
snow@li2 小时前
前端:开源软件镜像站 / 清华大学开源软件镜像站 / 阿里云 / 网易 / 搜狐
前端·开源软件镜像站
小小小小宇3 小时前
配置 Gemini Code Assist 插件
前端
one 大白(●—●)3 小时前
前端用用jsonp的方式解决跨域问题
前端·jsonp跨域
刺客-Andy3 小时前
前端加密方式 AES对称加密 RSA非对称加密 以及 MD5哈希算法详解
前端·javascript·算法·哈希算法
前端开发张小七4 小时前
13.Python Socket服务端开发指南
前端·python
前端开发张小七4 小时前
14.Python Socket客户端开发指南
前端·python
ElasticPDF-新国产PDF编辑器4 小时前
Vue 项目 PDF 批注插件库在线版 API 示例教程
前端·vue.js·pdf
拉不动的猪4 小时前
react基础2
前端·javascript·面试