1、挂载元素
初次渲染页面的时候会实例化的一个watcher,在这个watcher里面通过vm._render()函数和vm._update()函数实现页面渲染。 _render()函数生成虚拟dom,update()函数再将虚拟dom生成真实dom。
export function mountComponent(vm, el){
// 实现页面的挂载流程
vm.$el = el;
const updateComponent = () => {
// 调用render函数,获取虚拟节点,生成真实dom
vm._update(vm._render())
};
new Watcher(vm, updateComponent, () => {}, true);
}
2、保存当前watcher
watcher会在构造函数里面调用get(),然后再调用外部传进来的updateComponent方法,在调用updateComponent之前先记录下当前watcher。
class Watcher{
constructor(vm, exprOrfn, cb, options){
this.vm = vm;
this.getter = exprOrfn;
this.cb = cb;
this.options = options;
this.id = id++;
this.deps = []; // 记录dep
this.depsId = new Set();
this.get();
}
get(){
// 在对属性取值之前先把watcher记录一下
pushTarget(this);
// 这个方法中会对属性进行取值操作
this.getter();
popTarget();
}
addDep(dep){
let id = dep.id;
if(!this.depsId.has(id)){
this.depsId.add(id);
this.deps.push(dep);
dep.addSub(this);
}
this.deps.push(dep);
}
update(){
this.get();
}
}
直接将当前watcher保存在了Dep.target中。
export function pushTarget(watcher){
Dep.target = watcher
}
3、给属性添加dep
watcher中调用this.getter()实际上就是调用了vm._update(vm._render()),在render()函数时会访问属性(例如_s(name)访问name属性),所以可以在数据劫持中做一下处理。在遍历属性添加数据劫持时,给每一个属性加了一个dep,当存在Dep.target时就会进行依赖收集。
walk(data){
// 循环遍历data的key值进行观测
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
})
}
function defineReactive(obj, key, value){
observe(value);
let dep = new Dep() // 每次都给属性创建一个dep
Object.defineProperty(obj, key, {
get(){
if(Dep.target){
dep.depend(); // 只有存在watcher才进行依赖收集,避免外部其他地方访问属性也进行了依赖收集。让这个属性的dep记住watcher,也要让watcher记住dep
}
return value;
},
set(newValue){
if(value === newValue) return;
observe(newValue);
value = newValue;
dep.notify(); // 当值发生变化时,让dep通知watcher去执行
}
})
}
4、watcher和dep互相记录
调用dep.denpend()让watcher记录dep
class Dep{
constructor(){
this.id = id++;
this.subs = []; // 让属性记住watcher
}
depend(){
// 让watcher记住dep
Dep.target.addDep(this);
}
addSub(watcher){
this.subs.push(watcher);
}
notify(){
// 通知watcher去更新
this.subs.forEach(sub => sub.update())
}
}
// watcher中的addDep()会有一个去重的过程,避免记录重复dep,记录完dep后也要让dep记录watcher,这样就可以保证watcher和dep互相都有记录
addDep(dep){
let id = dep.id;
if(!this.depsId.has(id)){
this.depsId.add(id);
this.deps.push(dep);
dep.addSub(this);
}
this.deps.push(dep);
}
5、更新页面
当数据发生变化时,就会调用dep.notify()通知watcher更新渲染
notify(){
// 通知watcher去更新
this.subs.forEach(sub => sub.update())
}
疑问:这样做的优势在哪?通知watcher更新的时候不也还是调用_update(_render())方法吗?不也还是重新生成虚拟dom再生成真实dom吗?