vue2源码-八、依赖收集的过程
依赖收集的过程
前言
使用真实节点替换原始节点,主要涉及以下步骤:
1.新老节点的更新方案。
2.虚拟节点与真实节点映射。
3.实现新老节点的替换。
依赖收集
已经完成了
Vue的两大核心部分:响应式数据和数据渲染,即完成了整个Vue的初始化流程:当
new Vue()时,执行_init初始化,通过mountComponent做组件的挂载:1.
vm._render:调用render方法,创建新节点。2.
vm._render:更新逻辑,将虚拟节点渲染成真实DOM
patch:根据虚拟节点生成真实节点,新节点替换老节点
Vue特性:当响应式数据发生变化时,会触发对应视图的更新。举个例子:同一数据可能被放到多个视图中(页面或者组件)所共享,比如
Vuex中的数据:A组件,使用了数据
name;B组件,使用了数据
name这样,A,B两个组件就都依赖了数据
name,当数据发生变化时,两个组件都会触发对应视图更新操作。这就需要知道数据和视图间的对应关系,从而准确触发该数据对应的视图更新操作,从而设计模式上看就是观察者模式。
重点:
dep和watcher在
Vue中,依赖收集的实现使用了观察者模式:
watcher函数:每个组件或者页面所对应的渲染函数dep属性:每个数据都具有一个dep属性,用于记录使用该数据的组件或页面的视图渲染函数watcher。当数据发生变化时,
dep属性中存放的多个watcher将会被通知,watcher通过调用自身对应的更新方法update,完成页面的重新渲染:
- 为
name添加属性dep:用于收集组件A和组件B的渲染逻辑watcherA,watcherB- 为
watcherA,watcherB添加各自的更新方法update- 当数据发生变化时,通知
dep中存放的watcherA、watcherB触发各自的更新方法update。之前的内容;
- 由于
vm._update(vm._render)执行了数据渲染和更新操作- 所以
watcher中的update方法,便触发vm._update(vm._render())重新进行数据渲染和视图更新。- 所以,需要将
vm._update(vm._render())改造为可以通过watcher调用的方法。最后:
- 数据响应式过程中,为每个属性扩展
dep,用于收集watcher,在数据渲染时记录watcher;- 当同一数据在同一视图中被多次使用时,在
dep中需要对watcher进行查重,确保watcher进行查重,确保相同watcher仅记录一次。- 防止只要数据变化就会渲染视图的情况:当数据在视图中没有被使用时,数据的变化不应触发
watcher渲染,需要在视图渲染时进行依赖收集,知道哪些数据被“真正”使用了;
dep和watcher关联
watcher部分
根据上篇:
vm._render方法:调用render方法,生成虚拟节点。vm._update方法:将虚拟节点更新到页面上。所以就是通过执行
vm._update(vm._render())就能触发视图的更新。在
vue中,数据更新的原理如下:
- 每个数据都有一个
dep属性:记录使用改数据的组件或页面的视图渲染函数watcher。- 当数据发生变化时:
dep属性中存放的多个watcher将会被通知(观察者模式)。这里的
watcher就相当于vm._update(vm._render())因此,需要将视图渲染逻辑
vm._update(_render()),抽取为一个可单独调用的函数。抽取视图更新逻辑
watcher将视图渲染逻辑抽取成为可调用函数,包装为
function:export function mountComponent(vm, el) { // 1.调用render方法产生虚拟节点虚拟DOM vm.$el = el; const updateComponent = () => { vm._update(vm._render()); }; const watcher = new Watcher(vm, updateComponent, true); // vm._update(vm._render()); // vm.$options.render // 2.根据虚拟DOM产生真实DOM // 3.插入el元素中 }接下来,只要能够通过
watcher来调用执行updateComponent方法,就可以触发视图更新。创建
watcher类“数据改变,视图更新”,所以
Watcher类应从属响应式模块;class Watcher { constructor(vm, fn, options){ this.vm = vm; this.fn = fn; this.options = options; this.getter = fn; // fn 为页面渲染逻辑 this.get(); // Watcher初始化时调用页面渲染逻辑 } get(){ this.getter(); } } export default Watcher;收集依赖的必要性
做法:
- 有数据响应式原理可知,当响应式数据发生变化时,就会进入
Object.defineProperty中set方法。- 那么,此时在
set方法中调用视图更新逻辑vm._update(vm.render())就能触发图的更新操作。问题:
- 由于所有的响应式数据被修改时都会进入到
set方法,这就将会导致未被视图使用的数据发生变化时也会触发页面的更新。- 这种做法会触发不必要的视图更新,造成多余的性能开销。
针对上面,就需要进行依赖收集操作,为数据创建
dep用来收集渲染watcher
Dep部分创建Dep类
- 每一个数据都有一个
dep属性,用于存放对应的渲染watcher- 在每一个
watcher中,也可能存放多个dep所以:
- 在
dep类中,需要具有一个添加watcher的方法;- 在
watcher类中,也需要有一个添加dep的方法。dep
// src/observe/dep.js // dep 对象的唯一 id let id = 0; class Dep { constructor(){ this.id = id++; this.subs = []; } // 保存数据的渲染 watcher depend(){ this.subs.push(Dep.target) } } // 静态属性,用于记录当前 watcher Dep.target = null; export default Dep为data中的属性添加dep
function defineReactive(obj, key, value) { observe(value); let dep = new Dep(); // 为每个属性添加一个 dep Object.defineProperty(obj, key, { get() { return value; }, set(newValue) { if (newValue === value) return observe(newValue); value = newValue; } }) }修改
watcherclass Watcher { constructor(vm, fn, cb, options){ this.vm = vm; this.fn = fn; this.cb = cb; this.options = options; this.getter = fn; this.get(); } get(){ Dep.target = this; // 在触发视图渲染前,将 watcher 记录到 Dep.target 上 this.getter(); // 调用页面渲染逻辑 Dep.target = null; // 渲染完成后,清除 Watcher 记录 } } export default Watcher在数据渲染时,如果当前数据被视图所使用,当进入
Object.defineProperty的get方法时,Dep.target有值且为当前watcher对象,使用当前数据的dep对象记住此渲染watcher;function defineReactive(obj, key, value) { observe(value); let dep = new Dep(); Object.defineProperty(obj, key, { get() { // 如果 Dep.target 有值,将当前 watcher 保存到 dep if(Dep.target){ dep.depend(); } return value; }, set(newValue) { if (newValue === value) return observe(newValue); value = newValue; } }) }
视图更新部分
前言
上篇,主要介绍了依赖收集过程中
dep和watcher关联:利用js单线程特性,在watcher类中get方法,即将触发视图更新前,利用全局的类静态树丛Dep.target记录Watcher实例 并且,在试图渲染的取值过程中,在Object.defineProperty的get方法中,让数据dep记住渲染watcher,从而,实现了dep与watcher相关联,只有参与视图渲染的数据发生变化才会触发视图更新。实现视图更新逻辑
查重watcher
问题:同一数据在视图中多次使用会怎么样?
按照当前逻辑,同一数据在一个视图中被多次使用时,相同
watcher会在dep中被重复保存多次:<div id="app"> <li>{{name}}</li> <li>{{name}}</li> <li>{{name}}</li> </div>在
name属性的dep中,将会保存三个相同的渲染watcher,所以需要对watcher进行查重。因此需要设置一个
id作为标识符,每次new Watcher时id自增,因此作为标识对watcher实例进行查重。constructor(vm, fn, options) { this.id = id++; // 创建时递增 this.renderWatcher = options; this.getter = fn; // getter意味着调用这个函数可以发生取值操作 this.deps = []; // 后续实现计算属性和一些青理工作需要用 this.depsId = new Set(); this.get(); }让watcher也记住dep
前面,让数据
dep记住了渲染watcher,同样的,watcher也有必要记住deplet id = 0; class Dep { constructor() { this.id = id++; // 属性dep要收集watcher this.subs = []; // 这里存放着当前属性对应的watcher有哪些 } depend() { // 这里我们不希望放重复的watcher,而且刚才只是单向的关系 dep->watcher // watcher记录dep // this.subs.push(Dep.target); Dep.target.addDep(this); // 让watcher记住dep } // 让dep记住watcher-在watcher中被调用 addSub(watcher) { this.subs.push(watcher); } notify() { this.subs.forEach((watcher) => { watcher.update(); // 告诉watcher去更新 }); } } Dep.target = null; export default Dep;这里,如果互相记住,
watcher中要对dep查重,dep中也要对watcher查重;用这个方法,使
dep和watcher关联起来,只需要判断一次就可以了。import Dep from "./dep"; let id = 0; // 1)当我们创建渲染watcher的时候我们会把当前的渲染watcher放到Dep.target中 // 2)调用_render()会取值走到get上 // 不同的组件有不同的watcher,目前只有一个渲染根实例的 class Watcher { constructor(vm, fn, options) { this.id = id++; this.renderWatcher = options; this.getter = fn; // getter意味着调用这个函数可以发生取值操作 this.deps = []; // 后续实现计算属性和一些青理工作需要用 this.depsId = new Set(); this.get(); } addDep(dep) { let id = dep.id; // dep查重 if (!this.depsId.has(id)) { // 让watcher记住dep this.deps.push(dep); this.depsId.add(id); // 让dep也记住watcher dep.addSub(this); // watcher已经记住了dep并且去重了,此时让dep也记住了watcher } } get() { Dep.target = this; // 静态属性就只有一份 this.getter(); // 会去vm上取值 Dep.target = null; // 渲染完之后就清空 } update() { // this.get(); // 重新渲染 queueWatcher(this); // 先把当前的watcher暂存起来 } run() { this.get(); } }这样实现,会让
dep和watcher保持一种共存关系。如果
watcher中存在dep,那么dep中一定存在watcher,反之,亦然。所以,只需要判断一次,就能够完成
dep和watcher查重。数据改变触发视图更新
当视图改变的时候,会进入
Object.defineProperty的set方法。因此,需要在
set方法中,通知dep中所有收集的wathcer执行视图更新方法:function defineReactive(obj, key, value) { observe(value); let dep = new Dep(); // 为每个属性添加一个 dep Object.defineProperty(obj, key, { get() { if(Dep.target){ dep.depend(); } return value; }, set(newValue) { if (newValue === value) return observe(newValue); value = newValue; // 通知当前 dep 中收集的所有 watcher 依次执行视图更新 dep.notify(); } }) }在
Dep中添加notify方法:notify() { this.subs.forEach((watcher) => { watcher.update(); // 告诉watcher去更新 }); }
Watcher中添加update方法get(){ Dep.target = this; this.getter(); Dep.target = null; } // 执行视图渲染逻辑 update(){ this.get(); }结尾
Vue依赖收集的视图更新部分,主要涉及以下几点:视图初始化:
render方法中会进行取值操作,进入Object.defineproperty的get方法。get方法中为数据添加dep,并记录当前的渲染的watcher- 记录方式:
watcher查重并记住dep,dep再记住watcher‘数据更新时:
- 当数据发生改变,会进入
Object.defineProperty的set方法。- 在
set方法中,使dep中收集的全部watcher执行视图渲染操作watcher.get()- 在视图渲染前(
this.getter方法执行前),通过dep.target记录当前渲染的watcher- 重复视图初始化流程

浙公网安备 33010602011771号