引言-观察者模式
截止目前为止,基本的响应式和AST和render函数都能实现。不过目前只能通过手动调用_update(_render)
来实现data数据改变后的重新渲染。vue中data数据被操作改变后,会自动重新渲染页面,本期我们采用观察者模式来实现该功能。
先说说观察者模式,用借钱来打比方:一个债主有很多外债,每过一段时间就去找向他借钱的人讨钱,不过别人不一定有钱,所以老是白跑路。那么借钱的人就说,等我有钱了,我来找你。所有借钱的人都这样说,那么这些借钱的人就算是一个个“被观察者”,只要钱够了,就向“主目标(观察者)”触发还钱的操作(程序上一般会触发观察者的update方法)。
回到我们源码里渲染的情况,主目标就是一个负责渲染的函数,被观察者就是data里会改变的值。当data里的某个值改变之后,就给渲染函数发出通知,渲染函数收到通知后执行渲染,也就是_update(_render)
。
我们给渲染抽象为Watcher类,data里改动的值作为观察者新增Dep类。
Watcher
watcher的代码顺序在compileToFunction之后,使用mountComponent单独处理这一段逻辑。
watcher放入三个参数:vm实例、渲染更新函数、渲染watcher标识。在类中增加id标识;增加dep数组以及addDep方法,用于存储多个dep。
增加update方法,这个方法提供给Dep类进行通知,并且最终真正执行更新的方法。这里的更新使用队列维护,设置防抖使其最终只更新一次,并且要实现异步更新。这里的异步做了兼容性处理,可以参考以下代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
| import Dep, { popStack, pushStack } from "./dep";
let id = 0;
class Watcher{ constructor(vm,fn,options){ this.id = id++;
this.renderWatcher = options; this.getter = fn;
this.deps = []; this.depsId = new Set();
this.get(); }
get(){ pushStack(this); this.getter(); popStack(); }
addDep(dep){ let id = dep.id; if(!this.depsId.has(id)){ this.deps.push(dep); this.depsId.add(id); dep.addSub(this); } }
update(){ queueWatcher(this); }
run(){ this.get(); } }
let queue = []; let has = {}; let pending = false;
function flushSchedulerQueue(){ let flushQueue = queue.slice(0); queue = []; has = {}; pending = false; flushQueue.forEach(q=>q.run());
}
function queueWatcher(watcher){ const id = watcher.id; if(!has[id]){ queue.push(watcher); has[id] = true;
if(!pending){ nextTick(flushSchedulerQueue,0); pending = true; } } }
let callbacks = []; let waiting = false; function flushCallBacks(){ let cbs = callbacks.slice(0); waiting = false; callbacks = []; cbs.forEach(cb=>cb()); }
let timerFunc; if(Promise){ timerFunc = ()=>{ Promise.resolve().then(flushCallBacks); } }else if(MutationObserver){ let observer = new MutationObserver(flushCallBacks); let textNode = document.createTextNode(1); observer.observe(textNode,{ characterData:true, }); timerFunc = ()=>{ textNode.textContent = 2; } }else if(setImmediate){ setImmediate(flushCallBacks); }else{ timerFunc = ()=> { setTimeout(flushCallBacks); } }
export function nextTick(cb){ callbacks.push(cb); if(!waiting){ timerFunc(); waiting = true; } }
export default Watcher
|
Dep
Dep类因为要绑定到data里改动的值上,所以要在Object.defineProperty里的set中新建,当值发生变化重新触发set就会新增一个Dep实例。
在class外部增加Dep.target = null,这里直接把属性放在类名上,其实是静态属性,所有实例都共用且不会被继承,这里我也踩了坑,需要注意下。这个target便是拿来存储watcher,当调用更新后重新赋值为null。
addSub():watcher中遍历dep时调用,让dep中存储watcher实例。
depend():让watcher添加dep实例(让watcher记住这个dep)。
notify():通知watcher进行更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| let id = 0;
class Dep{ constructor(){ this.id = id++; this.subs = []; }
depend(){ Dep.target.addDep(this); }
addSub(Watcher){ this.subs.push(Watcher); }
notify(){ this.subs.forEach(watcher => { watcher.update() }) } }
Dep.target = null;
let stack = [];
export function pushStack(watcher){ stack.push(watcher); Dep.target = watcher; }
export function popStack(){ stack.pop(); Dep.target = stack[stack.length - 1]; }
export default Dep
|
下边是响应式核心处的代码,此前逻辑描述错误。
Dep是在get时就创建,这样的话data中的每个属性都会有一个Dep实例,当触发set时,data数据对应修改的dep就会调用notify通知watcher进行更新。而上边代码可以看到,watcher的更新是维护成队列的且添加防抖函数的,所以不会高频更新,而是一定时间内只会渲染一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| export function defineReactive(target, key, value) { let childob = observe(key);
let dep = new Dep(); Object.defineProperty(target, key, { get() { if(Dep.target){ dep.depend(); if(childob){ childob.dep.depend(); if(Array.isArray(value)){ dependArray(value); } } } return value; }, set(newValue) { if(newValue === value) return; observe(newValue); value = newValue; dep.notify(); }, }) }
|