博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Vue 源码剖析 —— 数组变化侦测
阅读量:7171 次
发布时间:2019-06-29

本文共 5117 字,大约阅读时间需要 17 分钟。

在当中,我们剖析了对象的变化侦测。由于其侦测方式是通过 getter/setter 实现的,而当通过 array.pusharray.pop 等方法操纵数组时,是不会触发 getter/setter 的。

问题一:如何追踪数组变化

和追踪对象类似,我们的需求是在调用 array.push 等函数时能够收到通知。Vue.js 中是通过创建一个拦截器覆盖 Array.prototype。之后,当调用数组方法时,执行的将会是拦截器中的提供方法,我们也就能因此追踪到数组的变化。下面是一个基础拦截器的实现:

const arrayProto = Array.prototypeconst arrayMethods = Object.create(arrayMethods);[  'push',  'pop',  'shift',  'unshift',  'splice',  'sort',  'reverse'].forEach((method) => {  const original = arrayProto[method]  Object.defineProperty(arrayMethods, method, {    value: function mutator (...args) {      return original.apply(this, args)    },    enumerable: false,    writable: true,    configurable: true  })})复制代码

在上面的代码中,我们创建了 arrayMethods 变量,它继承 Array.prototype。接着我们又在 arrayMethods 中使用了 Object.defineProperty。这样当我们调用 arrayMethods.push 时,其实是调用了其中的 mutator 函数,显然我们可以在 mutator 函数中添加对象变化侦测类似 setter 的逻辑。

我们不能直接让 arrayMethods 覆盖 Array.prototype,那会污染全局的 Array。我们的目的仅仅是拦截那些需要观测的数组,所以放在 Observer 类中处理会更为合理。

class Observer {  constructor(value) {    this.value = value    if (Array.isArray(value)) {      /**      * 新增,注意不是所有浏览器都支持__proto__属性,Vue.js 源码在这段      * 逻辑里面做了兼容处理,若不支持,则直接遍历 arrayMethods       * 将方法设置在被侦测的数组中      **/      value.__proto__ = arrayMethods    } else {      this.walk(value)    }  }  ...}复制代码

问题二:如何收集依赖并向依赖发送通知

Object 的依赖是在 getter 中的使用 Dep 收集的,每个 key 都有一个对应的 Dep 列表来存储依赖。Array 的依赖和 Object 一样,也是在 defineReactive 中收集。

function defineReactive(data, key, val) {  if (typeof val == 'object') {    new Observer(val)  }  let dep = new Dep()  Object.defineProperty(data, key, {    enumerable: true,    configurable: true,    get: function() {      dep.depend()      // 这里收集 Array 的依赖      return val    },    set: function(newVal) {      if (val === newVal) return      val = newVal      dep.notify()    }  })}复制代码

但此时我们需要改写一下 Dep 保存的地方,若按照之前的方式,对数组而言,我们只能在 getter 中访问到 dep,但在拦截器中是无法访问的,所以现在改写一下 Observer 类和 defineReactive

class Observer {  constructor(value) {    this.value = value    this.dep = new Dep()    if (Array.isArray(value)) {      value.__proto__ = arrayMethods    } else {      this.walk(value)    }  }  ...}function defineReactive (data, key, val) {  let childob = observe(val) // 修改  let dep = new Dep()  Object.defineProperty(data, key, {    enumerable: true,    configurable: true,    get: function() {      dep.depend()      // 新增      if (childOb) {        childOb.dep.depend()      }      // 这里收集 Array 的依赖      return val    },    set: function(newVal) {      if (val === newVal) return      val = newVal      dep.notify()    }  })}function observe (value, asRootData) {  if (!isObject(value)) {    return  }  let ob  if (hasOwn(value, '__ob__') && value.__ob__ instanceOf Observer) {    ob = value.__ob__  } else {    ob = new Observer(value)  }  return ob}复制代码

我们在 defineReactive 中调用了 observe,它把 val 作为参数输入,并返回一个 Observer 实例。这样我们就可以通过调用 childOb.dep.depend()getter 中添加依赖。接下来我们还需要修改一下 Observerob 属性到实例当中:

class Observer {  constructor(value) {    this.value = value    this.dep = new Dep()    // 新增    def(value, '__ob__', this)    if (Array.isArray(value)) {      value.__proto__ = arrayMethods    } else {      this.walk(value)    }  }  ...}function def (obj, key, val, enumerable) {  Object.defineProperty(obj, key, {    value: val,    enumerable: !!enumerable,    writable: true,    configurable: true  })}复制代码

我们已经可以通过数组数据的 ob 属性拿到 Observer 实例,然后就可以拿到 ob 上的 dep。现在我们就可以在拦截器中拿到依赖了:

;[  'push',  'pop',  'shift',  'unshift',  '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  })})复制代码

但是目前我们只是把数组变成了响应型的,数组项并没有被转换为响应式,需要在 Observer 内新增一些处理:

class Observer {  constructor(value) {    this.value = value    this.dep = new Dep()    // 新增    def(value, '__ob__', this)    if (Array.isArray(value)) {      this.observeArray(value)      value.__proto__ = arrayMethods    } else {      this.walk(value)    }  }  observeArray (items) {    for (let item in items) {      observe(item)    }  }  ...}复制代码

问题三:如何侦听新增元素

当我们使用 push 或是 unshift 等方法添加元素时,新元素不是响应式的,所以我们要在拦截器中添加处理:

;[  'push',  'pop',  'shift',  'unshift',  '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  })})复制代码

问题四:Array 的遗留问题

  1. 当直接使用索引设置一个数组项时,比如 this.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:this.items.length = newLength 从上面的实现原理中可以判断,以上两种情况我们是没有办法监控到的。

Array 的变化侦测过程梳理

Array 是通过创建拦截器去覆盖数组原型的方式来追踪变化的,收集依赖的方式和 Object 一样,都是在 getter 中收集。但是由于依赖的使用位置不同,要在拦截器向依赖发送消息就必须能访问到依赖。所以依赖不能像 Object 那样保存在 defineReactive 中,而是把依赖保存在 Observer 实例上。所以我们在 Observer 实例中绑定了 ob 属性,并将 this 保存在 ob 上。

转载于:https://juejin.im/post/5d0a395551882563f967d8e1

你可能感兴趣的文章
软工实践第一次作业
查看>>
php采集利器snoopy应用技巧
查看>>
java事件处理机制(自定义事件)j
查看>>
字符串反转
查看>>
我的友情链接
查看>>
Mysq-MMM读写分离实操(简单易懂版)
查看>>
博文索引
查看>>
修改mysql用户密码
查看>>
Postgresql PostGIS使用总结
查看>>
Django中间件
查看>>
活动目录服务的配置与管理(7) 利用组策略实现文件夹重定向
查看>>
删除除了匹配到的所有文件以及文件夹
查看>>
Linux中安装GRUB的两种方式
查看>>
iptables 详解
查看>>
Rancher中的服务升级实验
查看>>
数据结构-------顺序表的实现
查看>>
供应虚拟机没有usb端口和无法识别映射usb解决方案
查看>>
sed的用法
查看>>
为什么我叫应届生
查看>>
安装虚拟机shell脚本
查看>>