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

95.Object的变化侦测 #95

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

95.Object的变化侦测 #95

webVueBlog opened this issue Feb 22, 2023 · 0 comments

Comments

@webVueBlog
Copy link
Member

Vue.js 是如何追踪 Object 的变化的?

Vue.js 会把对象作为参数,传递给 Observer 的构造函数,遍历所有属性,调用 defineReactive 把每个属性都转换成 getter/setter;

如果子属性也是对象,继续递归;

在 defineReactive 中,新建一个 dep 用来收集依赖,当某处访问到某个状态时,会触发 getter 收集依赖,之后状态修改时会触发 setter,对应的 dep 会通知收集的所有依赖 update;

另外,触发 setter 时,如果新的值是个对象,同样会使用 Observer 转换成响应式对象。

Object 的问题

我们观察下 Observer 的实现会发现,只有修改属性的操作会触发 setter 通知依赖更新,新增属性不会触发 setter 函数,因为新增的属性一开始不存在,也就没有 getter/setter。

Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。 ——Vue.js 官网

好在,官方给我们提供了 vm.$set 和 vm.$delete,可以实现追踪新增和删除属性。这两个 API

Vue.js 最独特的特性之一是看起来并不显眼的响应式系统。

从状态生成 DOM,再输出到用户界面显示的一整套流程叫做渲染,应用在进行时会不断地进行重新渲染。而响应式系统赋予框架重新渲染的能力,其重要组成部分就是 变化侦测。没有它,就没有重新渲染。框架在运行时,视图也无法随着状态的变化而变化。

简单来说,变化侦测的作用是侦测数据的变化。当数据变化时,会通知视图进行相应的更新。

Vue.js 中 Object 和 Array 的变化侦测采用不同的处理方式。

什么是变化侦测

通常,在运行时应用内部的状态会不断发生变化,此时需要不停地重新渲染。这是如何确定状态中发生了什么变化?

变化侦测就是用来解决这个问题的,它分为两种方式,一种是 “推”模型,一种是 “拉”模型。提到这两个词,想必一些同学就会感觉很熟悉。没错,就是观察者模式。

Angular 和 React 中的变化侦测都属于“拉”。 当状态发生变化时,框架并不知道具体是哪个状态变化了,所以会进行一个暴力对比来找出哪些 DOM 节点需要重新渲染。在 Angular 中是脏检查,在 React 中是虚拟 DOM。

Vue.js 的变化侦测属于“推”。 当状态发生变化时, Vue.js 能知道是哪些状态发生变化了,因此它可以进行更细粒度的更新。但是它也有一定的代价,因为粒度越细,每个状态所绑定的依赖就越多,依赖追踪在内存上的开销就会越大。因此,从 Vue.js 2.0 开始,引入了虚拟 DOM,将粒度调整为中等粒度,即一个状态所绑定的依赖不再是具体的 DOM 节点,而是一个组件。

如何追踪变化、收集依赖

有两种方法可以侦测到变化,Object.defineProperty 和 Proxy 。

function defineReative(data, key,val){
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function(){
            return val;
        },
        set: function (newVal) {
            if(val === newVal) return;
            val = newVal;
        }
    })
}

封装好之后,每当从 data 的 key 中读取数据时,get 函数被触发,每当往 data 的 key 中设置数据时,set 函数被触发。

所以,我们可以在 getter 中收集依赖,在 setter 中触发依赖。

依赖收集在哪里

我们已经明确了目标,要在 getter 中收集依赖,那么应该把依赖收集到哪里呢?

我们先假设依赖是个函数,保存在全局变量上,如 window.target 上。我们把之前的 defineReactive 改造以下:

function defineReative(data, key, val) {
  let dep = []; // 新增
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.push(window.target); // 新增
      return val;
    },
    set: function (newVal) {
      if (val === newVal) return;
      val = newVal;
      // 新增
      dep.forEach(function (w) {
        w(newVal, val);
      });
    },
  });
}

我们新增了数组 dep,用来存储收集的依赖。当 set 函数被触发时,会依次触发收集的依赖。接下来,我们把依赖收集的相关代码封装成一个 Dep 类,用来收集依赖、删除依赖和向依赖发送通知等。

export default class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    if (sub && sub.update) {
      this.subs.push(sub);
    }
  }

  removeSub(sub) {
    remove(this.subs, sub);
  }

  depend() {
    if (window.target) {
      this.addSub(window.target);
    }
  }

  notify() {
    const subs = this.subs.slice();
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}

function remove(arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1);
    }
  }
}

然后,我们再改造一下 defineReactive:

function defineReative(data, key, 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;
      val = newVal;
      // 修改
      dep.notify();
    },
  });
}

现在,依赖被收集到 Dep 实例中。

依赖是谁

当状态发生变化时,需要通知谁,谁就是依赖。在上面的代码中,window.target 就是依赖。

由于依赖会一直观察着状态的变化,所以我们就把依赖叫做 Watcher。

Watcher 是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。

我们先看一个 Watcher 的经典使用方式:

vm.$watch('a.b.c', function(newVal, oldVal){
    // ... 
}

这段代码表示当 data.a.b.c 发生变化时,就触发后面的函数。

那么,思考一下它是如何实现的?我们新建一个 Watcher 类来模拟一下:

export default class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm;
    // 执行 this.getter(),就可以读取 data.a.b.c 的数据
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.cb = cb;
    this.value = this.get();
  }

  get() {
    window.target = this;
    let value = this.getter.call(this.vm, this.vm);
    window.target = undefined;
    return value;
  }

  update() {
    const oldValue = this.value;
    this.value = this.get();
    this.cb.call(this.vm, this.value, oldValue);
  }
}

先把 window.target 设置成 this,也就是 watcher 实例,这时候读取 data.a.b.c 的值时,会触发 getter 进行依赖收集(回顾一下 defineReactive),把当前的 watcher 实例主动添加到 data.a.b.c 的 Dep 实例中。

每当 data.a.b.c 的值发生变化,触发 setter,对应的 Dep 调用 notify 方法,通知收集所有依赖,依次触发 update 方法。

我们再看一下 parsePath 是如何实现的:

export const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}

将 path 用 . 分割成数组,然后循环获取。

递归侦测所有的 key

上面我们已经实现侦测单个key,下面我们要开始侦测所有的 key 了。我们新建一个 Observer 类, 把一个对象的所有属性都转换成 getter/setter:

export default class Observer {
  constructor(value) {
    this.value = value;
    if(typeof value === 'object'){
        this.walk(value); // 把对象的所有属性都转成 getter/setter
    }
  }
  
  walk(obj) {
    Object.keys(obj).forEach((key) => {
      defineReactive(obj, key, obj[key]);
    });
  }
}

// 侦测单个 key 的变化
function defineReative(data, key, val) {
  let dep = new Dep(); 
  
  new Observer(val); // 新增, 如果子属性是对象,递归遍历子属性,同样转成 getter/setter
  
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend(); 
      return val;
    },
    set: function (newVal) {
      if (val === newVal) return;
      val = newVal;
      new Observer(newVal) // 新修改的值如果也是对象,同样转成 getter/setter
      dep.notify();
    },
  });
}

上述代码,我们定义了 Observer 类,用来将对象转换成可被侦测的 object。只有对象才会调用 walk 方法来遍历转换。

我们在 defineReactive 函数中新增了一行 new Observer(val),如果对象的子属性也是对象,递归遍历转换成 getter/setter。我们还在 setter 中 新增了一行 new Observer(newVal),当新设置的值是个对象时,也会把这个对象转成响应式的 object。

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