接:javascript
前面咱们实现了:git
可是目前咱们去更新数据,视图不能正常去更新,如何知道视图是否须要更新,是否是任意一组data数据修改都须要从新渲染更新视图?其实并非,只有那些在页面被引用的数据变动后才会须要视图的更新,因此须要记录哪些数据是否被引用,被谁引用,从而决定是否更新,更新谁,这也就是依赖收集的目的。github
这里须要使用发布-订阅模式来收集咱们的依赖,咱们先简单实现一个简单的发布订阅,新建一个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
因此基于发布-订阅模式,咱们就要考虑咱们须要在哪去对咱们的数据进行发布订阅,能够想到咱们以前都对咱们的数据都添加了getter
和setter
,能够在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
方法,添加addSub
和dep.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'
,会发现视图已经更新了。
咱们梳理一下视图更新的执行流程:
new Vue()
初始化数据后,从新定义了数据的getter
,setter
。new Watcher(vm, updateComponent)
。complier
解析页面的时候取值vm.msg
,触发了该属性的getter
,往vm.msg
的dep中添加Dep.target,也就是渲染watcher。setTimeout
2秒后,修改vm.msg
,该属性的dep进行广播,触发渲染watcher
的update
方法,页面也就从新渲染了。代码点击=> 传送门
若是在页面上,出现两个引用相同的变量,那么dep
便会存入两个相同的渲染watcher
,这样就会致使在msg发生变化的时候触发两次更新。
<div id="app">
{{msg}}
{{msg}}
</div>
复制代码
下面进行一些优化,让dep
和watcher
相互记忆,在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.addSub
将watcher
存到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()
}
});
}
复制代码
此时去修改引用两次的变量,会发现只会更新一次了。
代码点击=> 传送门
上面处理了非数组的依赖收集,可是数组的依赖收集并不在defineReactive
的getter
和setter
中。
首先咱们给每一个观察过的对象和数组添加一个__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;
},
...
});
}
复制代码
此时给arr
去push
一个数据的话,会走到childOb.dep.depend();
而后这个Dep
收集的Watcher
将会去调用数组中notify
更新视图。
上面处理了数组的依赖收集,可是若是一个数组为[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
的基本框架和响应式核心原理也就实现了,后面的话咱们再去看下
computed
和watch
,核心原理也和前面相似。都是利用Watcher
去监听变化,后面咱们一块儿去实现一下!
代码点击=> 传送门
但愿各位老板点个star,小弟跪谢~