手摸手从0实现简版Vue --- (依赖收集)

接:javascript

手摸手从0实现简版Vue --- (对象劫持)html

手摸手从0实现简版Vue --- (数组劫持)vue

手摸手从0实现简版Vue --- (模板编译)java

前面咱们实现了:git

  • 数据的劫持
  • 模板解析

可是目前咱们去更新数据,视图不能正常去更新,如何知道视图是否须要更新,是否是任意一组data数据修改都须要从新渲染更新视图?其实并非,只有那些在页面被引用的数据变动后才会须要视图的更新,因此须要记录哪些数据是否被引用,被谁引用,从而决定是否更新,更新谁,这也就是依赖收集的目的。github

1. 发布-订阅

这里须要使用发布-订阅模式来收集咱们的依赖,咱们先简单实现一个简单的发布订阅,新建一个dep.js:数组

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

  addSub(watcher) {
    this.subs.push(watcher)
  }

  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}

const dep = new Dep()
dep.addSub({
  update() {
    console.log('1')
  }
})
dep.addSub({
  update() {
    console.log('2')
  }
})
dep.notify()
复制代码

此时咱们去调用notify的话,会依次输出1, 2,这里的dep就至关于发布者,watcher就属于订阅者,当执行notify时,全部的watcher都会收到通知,而且执行本身的update方法。app

2. 依赖收集

因此基于发布-订阅模式,咱们就要考虑咱们须要在哪去对咱们的数据进行发布订阅,能够想到咱们以前都对咱们的数据都添加了gettersetter,能够在getter的时候调用dep.addSub(),在setter的时候去调用dep.notify(),可是以什么样的方式去添加订阅。咱们以前在$mount的时候实现了一个渲染watcher,如今咱们去修改一下这个watcher。首先给Dep添加两个方法,用来操做subs:框架

let stack = [];
export function pushTarget(watcher) {
  Dep.target = watcher;
  stack.push(watcher);
}

export function popTarget() {
  stack.pop();
  Dep.target = stack[stack.length - 1]; 
}
复制代码

而后去修改一下watcher:函数

class Watcher { // 每次产生一个watch 都会有一个惟一的标识
  ...
  get() {
+    pushTarget(this); // 让 Dep.target = 这个渲染Watcher,若是数据变化,让watcher从新执行
    this.getter && this.getter(); // 让传入的函数执行
+    popTarget();
  }
+  update() {
+    console.log('数据更新');
+    this.get();
+  }
}
复制代码

而后去修改defineReactive方法,添加addSubdep.notify()

export function defineReactive(data, key, value) {
  observe(value);   // 若是value依旧是一个对象,须要深度递归劫持
+  const dep = new Dep()
  Object.defineProperty(data, key, {
    get() {
      // 取数据的时候进行依赖收集
+      if (Dep.target) {
+        dep.addSub(Dep.target)
+      }
      return value;
    },
    set(newValue) {
      if (newValue === value) return;
      observe(newValue); // 若是新设置的值是一个对象, 应该添加监测
      value = newValue;
      // 数据更新 去通知更新视图
+      dep.notify()
    }
  });
}
复制代码

此时咱们2s后去更新一下vm.msg = 'hello world',会发现视图已经更新了。

咱们梳理一下视图更新的执行流程:

  1. new Vue()初始化数据后,从新定义了数据的gettersetter
  2. 而后调用$mount,初始化了一个渲染watcher, new Watcher(vm, updateComponent)
  3. Watcher实例化时调用get方法,把当前的渲染watcher挂在Dep.target上,而后执行updateComponent方法渲染模版。
  4. complier解析页面的时候取值vm.msg,触发了该属性的getter,往vm.msg的dep中添加Dep.target,也就是渲染watcher。
  5. setTimeout2秒后,修改vm.msg,该属性的dep进行广播,触发渲染watcherupdate方法,页面也就从新渲染了。

代码点击=> 传送门

3. 依赖收集优化--过滤相同的watcher

若是在页面上,出现两个引用相同的变量,那么dep 便会存入两个相同的渲染watcher,这样就会致使在msg发生变化的时候触发两次更新。

<div id="app">
  {{msg}}
  {{msg}}
</div>
复制代码

下面进行一些优化,让depwatcher相互记忆,在dep收集watcher的同时,让watcher记录自身订阅了哪些dep

首先给Dep添加一个depend方法,让watcher也就是Dep.target将该dep记录。

class Dep {
	...
+  depend() {
+    if (Dep.target) { // Dep.target = 渲染 watcher
+      Dep.target.addDep(this);
+    }
+  }
}
复制代码

而后在watcher中添加addDep方法,用来记录Dep和调用dep.addSubwatcher存到Dep中,互相记录。

class Watcher {
  constructor(vm, exprOrFn, cb = () => {}, opts = {}) {
    ...
+    this.deps = [];
+    this.depsId = new Set();

    this.get();
  }
  
+  addDep(dep) {
+    // 同一个watcher 不该该重复记录 dep
+    let id = dep.id;
+    if (!this.depsId.has(id)) {
+      this.depsId.add(id);
+      this.deps.push(dep); // 让watcher记录dep
+      dep.addSub(this);
+    }
  }
复制代码

因此此时的defineReactive不该该去直接调用dep.addSub,应该改成:

export function defineReactive(data, key, value) {
  observe(value);   // 若是value依旧是一个对象,须要深度递归劫持
  const dep = new Dep()
  Object.defineProperty(data, key, {
    get() {
      // 取数据的时候进行依赖收集
      if (Dep.target) {
        // 实现dep存watcher, watcher也能够存入dep
+        dep.depend();
-        dep.addSub(Dep.target)
      }
      return value;
    },
    set(newValue) {
      if (newValue === value) return;
      observe(newValue); // 若是新设置的值是一个对象, 应该添加监测
      value = newValue;
      // 数据更新 去通知更新视图
      dep.notify()
    }
  });
}
复制代码

此时去修改引用两次的变量,会发现只会更新一次了。

代码点击=> 传送门

4. 数组的依赖收集

上面处理了非数组的依赖收集,可是数组的依赖收集并不在defineReactivegettersetter中。

首先咱们给每一个观察过的对象和数组添加一个__ob__属性,返回observer实例自己,而且给每一个observer实例添加一个dep,用来数组的依赖收集.

class Observe {
  constructor(data) {
    // 这个dep属性专门为数组设置
+    this.dep = new Dep()
+    // 给每一个观察过的对象添加一个__ob__属性, 返回当前实例
+    Object.defineProperty(data, '__ob__', {
+      get: () => this
+    })
    // ...
  }
}
复制代码

添加事后,咱们就能够在array的方法中,获取到这个dep,并在更新时调用dep.notify

methods.forEach(method => {
  arrayMethods[method] = function(...args) { // 函数劫持
    ...
    if(inserted) observerArray(inserted);
+    this.__ob__.dep.notify();
    return result;
  }
}); 
复制代码

可是还有重要的一点,咱们如今能够通知到了,可是数组的依赖没有收集到,下面去处理下数组的依赖收集:

export function defineReactive(data, key, value) {
+  let childOb = observe(value);
-  observe(value);
  const dep = new Dep()
  Object.defineProperty(data, key, {
    get() {
      // 取数据的时候进行依赖收集
      if (Dep.target) {
        // 实现dep存watcher, watcher也能够存入dep
        dep.depend();
+        if (childOb) {
+          childOb.dep.depend(); // 收集数组的依赖收集
+        }
      }
      return value;
    },
    ...
  });
}
复制代码

此时给arrpush一个数据的话,会走到childOb.dep.depend();而后这个Dep收集的Watcher将会去调用数组中notify更新视图。

5.嵌套数组依赖收集

上面处理了数组的依赖收集,可是若是一个数组为[1, 2, [3, 4]],那么arr[2].push('xx')将不能正常更新,下面咱们去处理嵌套数组的依赖收集,

处理的方法就是,在外层arr收集依赖的同时也帮子数组收集,这里新增一个dependArray方法。

咱们给每一个观察过的对象都添加过一个__ob__,里面嵌套的数组一样有这个属性,这时候只须要取到里面的dep,depend收集一下就能够,若是里面还有数组嵌套则须要继续调用dependArray

export function defineReactive(data, key, value) {
  let childOb = observe(value);   // 若是value依旧是一个对象,须要深度递归劫持
  const dep = new Dep()
  Object.defineProperty(data, key, {
    get() {
      // 取数据的时候进行依赖收集
      if (Dep.target) {
        // 实现dep存watcher, watcher也能够存入dep
        dep.depend();
        if (childOb) {
          childOb.dep.depend(); // 收集数组的依赖收集
+          dependArray(value); // 收集数组嵌套的数组
        }
      }
      return value;
    },
    ...
  });
}
复制代码

咱们实现一下dependArray

export function dependArray(value) {
  for(let i = 0; i < value.length; i++) {
    let currentItem = value[i];
    currentItem.__ob__ && currentItem.__ob__.dep.depend();
    if (Array.isArray(currentItem)) {
      dependArray(currentItem);
    }
  }
}
复制代码

这样,数组为[1, 2, [3, 4]],那么arr[2].push('xx'),能够正常去更新了。

到这里,依赖收集就结束了,整个Vue的基本框架和响应式核心原理也就实现了,后面的话咱们再去看下

computedwatch,核心原理也和前面相似。都是利用Watcher去监听变化,后面咱们一块儿去实现一下!

代码点击=> 传送门

但愿各位老板点个star,小弟跪谢~

相关文章
相关标签/搜索