Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

96.Array的变化侦测 #96

Open
webVueBlog opened this issue Feb 22, 2023 · 0 comments
Open

96.Array的变化侦测 #96

webVueBlog opened this issue Feb 22, 2023 · 0 comments

Comments

@webVueBlog
Copy link
Member

Array 的变化侦测是如何实现的

与 Object 不同,我们将数组实例的原型指向我们定义的拦截器,在 getter 中收集依赖,在拦截器中通知依赖。

为了在 getter 中收集依赖,我们给 observer 添加了一个实例属性 dep

为了能通知依赖,我们定义 value.ob 属性,指向 observer 实例, 在拦截器中使用 value.ob.dep.notify() 来通知依赖。

考虑到数组中元素可能是对象,为了侦测到数组中对象元素的变化,尝试把数组中的元素也转成响应式,在 observer 上新增 observeArray 方法,既能在初始化时将数组中元素转成响应式,也能在数组的拦截器中,调用 value.ob.observeArray(inserted),将新增元素也尝试转成响应式的。

给 value 定义 ob 属性,有两个好处:1. 标识这个值是响应式的。2. 方便随时获取 observer 实例

Array 的问题

直接通过下标修改元素,以及 list.length = 0 来清空数组,这样的变化是无法侦测到的。

Object 的侦测变化,那为什么 Array 要单独来讲呢?我们用下面的例子来说明一下:

this.list.push(1)

Object 可以通过 getter/setter 来实现状态的侦测,而数组的 push 方法,无法触发 getter/setter。

如何追踪变化

Object 是通过 setter 来通知依赖 update 的。如果我们能在数组 push 的时候发出通知,就能实现相同的效果。

我们可以将数组的原型指向一个新的对象,这个对象会重写 Array.prototype 上的数组方法。

拦截器

上面所说的既有数组方法,又能实现通知功能的对象,我们给它取个名字,叫做 拦截器(这也是 代理模式 的一种体现)。

如何实现一个拦截器呢?

我们可以发现 Array.prototype 中 可以改变数组自身内容的方法共有7个: push、pop、shift、unshift、splice 和 reverse。

const arrayProto = array.prototype;

export const arrayMethods = Object.create(arrayProto);

[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
].forEach(function (method){
    // 缓存原型上的方法
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
        enumerable: false,
        writable: true,
        configurable: true,
        value: function mutator(...args){
            // ...
            // 调用原型上的方法
            return original.apply(this.args)
        }
    })
})

有了拦截器之后,我们该如何让它生效?暴力的方式是直接修改 Array.prototype,但是这种方式会污染全局的 Array,所以我们要换个方式———把数组实例的原型指向拦截器。

利用 proto

利用 ES6 的 Object.setPrototypeOf()

考虑兼容性的问题,我们使用第一种,如果连 proto 也不支持的话,就把拦截器的方法直接复制到数组实例上。

function protoAugment(target, src, keys) {
  target.__proto__ = src;
}

function copyAugment(target, src, keys) {
  for (let i = 0, len = keys.length; i < len; i++) {
    const key = keys[i];
    def(target, key, src[key]);
  }
}

function def(target, key, val, enumerable?: boolean) {
  Object.defineProperty(target, key, {
    enumerable: !!enumerable,
    writable: true,
    configurable: true,
    value: val,
  });
}

我们现在重新改造一下 Observer:

const hasProto = '__proto__' in {};
export default class Observer {
  constructor(value) {
    this.value = value;
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
    } else {
      this.walk(value); // 把对象的所有属性都转成 getter/setter
    }
  }

  walk(obj) {
    Object.keys(obj).forEach((key) => {
      defineReactive(obj, key, obj[key]);
    });
  }
}

如何收集依赖

上面我们实现了拦截器,但是这个拦截器还不具备通知依赖的功能。要实现通知依赖,首先得先实现收集依赖的功能。那么,数组是如何收集依赖的呢?

我们先回顾一下 Object 的依赖是如何收集的。Object 的依赖收集是在 getter 中使用 Dep 实例收集的,每个 key 都有一个 dep 来收集依赖。

其实,数组也是在 getter 中收集依赖的。

{
    list: [1,2,3,4]
}

在读取 list 的时候,会触发 list 属性的 getter。

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();
      // 收集数组的依赖
      return val;
    },
    set: function (newVal) {
      if (val === newVal) return;
      dep.notify();
      val = newVal;
    },
  });
}

Array 在 getter 中收集依赖,在拦截器中触发依赖。

依赖收集在哪里

Vue.js 把 Array 的依赖存放在 Observer 实例上:

export default class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep(); // 数组的依赖收集在这里
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
    } else {
      this.walk(value); // 把对象的所有属性都转成 getter/setter
    }
  }
}

收集依赖

在 Observer 实例上添加一个 dep 属性后,我们就可以收集依赖了。

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();
      }
      return val;
    },
    set: function (newVal) {
      if (val === newVal) return;
      dep.notify();
      val = newVal;
    },
  });
}

function observe(value, asRootData) {
  if (typeof value !== "object") {
    return;
  }
  let ob;
  if (value.hasOwnProperty("__ob__") && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else {
    ob = new Observer(value);
  }
  return ob;
}

通知依赖更新

上面我们已经完成了数组的依赖收集,接下来就差通知依赖了。要想通知依赖,我们需要能拿到 Observer 实例上的 dep,那么想一想如何在拦截器中访问到 dep?

在上面的 observe 函数中出现了 ob,没错核心就是它。

export default class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    def(value, '__ob__', this); // 新增
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
    } else {
      this.walk(value); // 把对象的所有属性都转成 getter/setter
    }
  }
}

我们在 value 上定义一个新属性 ob 指向 Observer 实例,然后我们就可以在拦截器中在 value 上访问到 Observer 实例,也就能访问到它的 dep 属性。

const arrayProto = array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
].forEach(function (method){
    constt original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
        enumerable: false,
        writable: true,
        configurable: true,
        value: function mutator(...args){
            let ob = value.__ob__; // 新增
            ob.dep.notify() // 新增
            return original.apply(this.args)
        }
    })
})

侦测数组中元素的变化

上面,我们实现了数组的依赖收集和依赖的通知。那么如果数组中存在对象怎么办呢?

如果数组中某个对象的属性发生变化,按照道理也需要发送通知。另外,如果往数组中添加了一个对象,也需要把这个对象转成响应式的对象。所以,我们需要遍历数组,尝试把数组中元素转成响应式的。

export default class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep();
    if (Array.isArray(value)) {
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
      this.observeArray(value); // 新增
    } else {
      this.walk(value);
    }
  }

  observerArray(list) {
    for (let i = 0, len = list.length; i < l; i++) {
      observe(list[i]); // 每一项都尝试转成响应式的
    }
  }
}

侦测数组新增元素

我们可以在拦截器中,把新增元素传给 ob 的 observeArray 方法,就能把新增元素也尝试转成响应式的。

const arrayProto = array.prototype;
export const arrayMethods = Object.create(arrayProto);
[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
].forEach(function (method){
    const original = arrayProto[method];
    Object.defineProperty(arrayMethods, method, {
        enumerable: false,
        writable: true,
        configurable: true,
        value: function mutator(...args){
            const result = original.apply(this.args)
             let ob = value.__ob__; 
            // 新增, 将新增的元素也尝试转成响应式的
            let inserted;
            switch(metthod){
                case 'push':
                case 'unshift':
                    inserted = args;
                    break;
                case 'splice':
                    inserted = args.slice(2);
                    break;
            }
            if (inserted) ob.serveArray(inserted);
            ob.dep.notify() 
            
            return result;
        }
    })
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant