依赖收集的过程
前言
使用真实节点替换原始节点,主要涉及以下步骤:
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; } }) }
修改
watcher
class 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,vm,视图,watcher,Dep,源码,vue2 From: https://www.cnblogs.com/dgqp/p/17330837.html
前言
上篇,主要介绍了依赖收集过程中
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
也有必要记住dep
let 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
- 重复视图初始化流程