手写Vue源码(五) - 依赖收集
source link: https://zhuanlan.zhihu.com/p/343502936
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
手写Vue源码(五) - 依赖收集
源码地址:传送门
Vue
为用户提供了一个特别方便的功能:数据更新时自动更新DOM
。本文将详细介绍Vue
源码中该特性实现的核心思路,深入理解Vue
数据和视图的更新关系。
这是Vue
官方数据变化引发视图更新的图解:
用文字描述的话,其流程如下:
- 组挂载,执行
render
方法生成虚拟DOM
。此时在模板中用到的数据,会从vm
实例上进行取值 - 取值会触发
data
选项中定义属性的get
方法 get
方法会将渲染页面的watcher
作为依赖收集到dep
中- 当修改模板中用到的
data
中定义的属性时,会通知dep
中收集的watcher
执行update
方法来更新视图 - 重新利用最新的数据来执行
render
方法生成虚拟DOM
。此时不会再收集重复的渲染watcher
渲染
watcher
就是用来更新视图的watcher
,具体的执行过程在组件初渲染中有详细介绍,它的主要作用如下:
1. 执行vm._render
方法生成虚拟节点
2. 执行vm._update
方法将虚拟节点处理为真实节点挂载到页面中
需要注意的是,数组并没有为每个索引添加set/get
方法,而是重写了数组的原型。所以当通过调用原型方法修改数组时,会通知watcher
来更新视图,保证页面更新。
收集watcher
并且在数据更新后通知watcher
更新DOM
的功能主要是通过Dep
来实现的,其代码如下:
let id = 0;
class Dep {
constructor () {
// dep的唯一标识
this.id = id++;
this.subs = [];
}
addSub (watcher) {
this.subs.push(watcher);
}
// 通过watcher来收集dep
depend () {
Dep.target.addDep(this);
}
// 执行所有收集watcher的update方法
notify () {
this.subs.forEach(sub => {
sub.update();
});
}
}
Dep
会将watcher
收集到内部数组subs
中,之后通过notify
方法进行统一执行。
代码中还会维护一个栈,来保存所有正在执行的watcher
,执行完毕后watcher
出栈。
const stack = [];
// 当前正在执行的watcher
Dep.target = null;
export function pushTarget (watcher) {
stack.push(watcher);
Dep.target = watcher;
}
export function popTarget () {
stack.pop();
Dep.target = stack[stack.length - 1];
}
目前代码并没有用到栈,在之后实现计算属性时,会利用栈中存储的渲染
watcher
来更新视图
通过上面的代码,就可以通过dep
来实现对watcher
的收集和通知。
Watcher
本文中讲到的
watcher
只是起到渲染视图的作用,所以将其称为渲染watcher
。在之后涉及到watch
和computed
之后,还会有它们各自相对应的watcher
。
Watcher
的主要功能:
- 收集
dep
,用于之后实现computed
的更新 - 通过
get
方法来更新视图
let id = 0;
class Watcher {
constructor (vm, exprOrFn, cb, options) {
// 唯一标识
this.id = id++;
this.vm = vm;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.options = options;
this.deps = [];
this.depsId = new Set(); // 利用Set来进行去重
if (typeof exprOrFn === 'function') {
this.getter = this.exprOrFn;
}
this.get();
}
// 在watcher中对dep进行去重,然后收集起来,并且再让收集的dep收集watcher本身(this)。这样便完成了dep和watcher的相互收集
addDep (dep) {
// 用空间换时间,使用Set来存储deps id进行去重
if (!this.depsId.has(dep.id)) {
this.deps.push(dep);
this.depsId.add(dep.id);
// 重复的dep无法进入,每个dep只能收集一次对应watcher
dep.addSub(this);
}
}
get () {
// 更新视图之前将watcher入栈
pushTarget(this);
this.getter();
// 视图更新后,watcher出栈
popTarget();
}
// 更新视图
update () {
this.get();
}
}
Watcher
接收的参数如下:
vm
:Vue
组件实例exprOrFn
: 表达式或者函数cb
: 回调函数options
: 执行watcher
的一些选项
首先,在组件初次挂载时,会实例化Watcher
,在Watcher
内部会执行传入的exprOrFn
渲染页面:
Vue.prototype.$mount = function (el) {
// some code ...
mountComponent(vm);
};
export function mountComponent (vm) {
callHook(vm, 'beforeMount');
function updateComponent () {
vm._update(vm._render());
}
// 在实例化时,会执行updateComponent来更新视图
new Watcher(vm, updateComponent, () => {}, { render: true });
callHook(vm, 'mounted');
}
当data
选项中的值发生更新后,会通过dep.notify
来调用watcher
的update
,而watcher
的update
方法会调用exprOrFn
即我们之前传入的updateComponent
方法,从而更新视图。
依赖收集时分别对对象和数组进行了不同的操作:
- 对象:在对象每一个属性的
get
方法中,利用属性对应的dep
来收集当前正在执行的watcher
- 数组:在
Observer
中,为所有data
中的对象和数组都添加了__ob__
属性,可以获取Observer
实例。并且为Observer
实例设置了dep
属性,可以直接通过array.__ob__.depend()
来收集依赖。
设置值时:
- 对象:通过被修改属性的
set
方法,调用dep.notify
来执行收集的watcher
的update
方法 - 数组:通过调用数组方法来修改数组,在对应的数组方法更新完数组后,还会执行数组对应的
array.__ob__.notify
来通知视图更新
依赖收集的具体代码如下:
为每一个Observer
添加dep
属性:
class Observer {
constructor (value) {
this.value = value;
this.dep = new Dep(); // data中对象和数组创建dep
// 为data中的每一个对象和数组都添加__ob__属性,方便直接可以通过data中的属性来直接调用Observer实例上的属性和方法
defineProperty(this.value, '__ob__', this);
if (Array.isArray(value)) {
Object.setPrototypeOf(value, arrayProtoCopy);
this.observeArray(value);
} else {
this.walk();
}
}
// some code ...
}
observe
中将Observer
实例返回,并且对已经执行过Observer
的数据不再处理:
function observe (data) {
// 如果是对象,会遍历对象中的每一个元素
if (typeof data === 'object' && data !== null) {
// 已经观测过的数据会有__ob__属性,将不再处理,返回undefined
if (data.__ob__) {
return;
}
// 返回Observer实例
return new Observer(data);
}
}
data
中每个对象的属性都会在get
方法中收集依赖,在set
方法中通知视图更新。也会为data
中的对象和数组在Observer
实例中创建的dep
收集watcher
:
function defineReactive (target, key) {
let value = target[key];
// 继续对value进行监听,如果value还是对象的话,会继续new Observer,执行defineProperty来为其设置get/set方法
// 否则会在observe方法中什么都不做
const childOb = observe(value);
const dep = new Dep();
Object.defineProperty(target, key, {
get () {
if (Dep.target) { // 每个属性都收集watcher
// 为对象的每一个属性收集依赖
dep.depend();
if (childOb) {
// 收集数组的依赖,在数组更新的时候,会调用notify方法,通知数组更新
// 这里是定义在Observer中的另一个新的dep
childOb.dep.depend();
// 对于数组中依旧有数组的情况,需要对其再进行依赖收集
dependArray(value);
}
}
return value;
},
set (newValue) {
if (newValue !== value) {
observe(newValue);
value = newValue;
dep.notify();
}
}
});
}
对于数组,要递归为数组中每一项继续收集watcher
。这样即使当数据为arr:[[1,2,3]]
时,也可以在内层数组调用数组方法更新时通知视图更新:
// src/observer/array.js
export function dependArray (data) {
if (Array.isArray(data)) {
for (let i = 0; i < data.length; i++) {
const item = data[i];
// item也可能是对象,会对对象再次进行依赖收集,此时和defineReactive中收集的dep不是同一个
item.__ob__?.dep.depend();
dependArray(item);
}
}
}
当调用修改原数组的方法时,通过vm.array.__ob__.dep.notify
来通知视图更新:
// some code ...
methods.forEach(method => {
arrayProtoCopy[method] = function (...args) {
const result = arrayProto[method].apply(this, args);
// data中的数组会调用这里定义的方法,this指向该数组
const ob = this.__ob__;
// some code ...
ob.dep.notify();
return result;
};
});
调用如concat
等数组方法时,并不会修改原数组,需要我们手动将原数组赋值为更改后的新数组,这样就会触发defineReactive
中原数组对应的set
方法,从而更新视图。
// 会触发array属性的set方法,调用dep.notify通知视图更新
vm.array = newArray
在Observer
中定义的dep
,与defineReactive
中的dep
不同,是一个新的dep
,会收集数组和对象依赖的watcher
。在之后便可以很方便的通过vm.data.__ob__
来获取到Observer
实例,进行而调用dep
中的depend
和notify
方法。
$set 和 $delete
现在数据更新,视图也会自动更新。但是删除和新增对象属性以及通过索引修改数组并不会更新视图,为了应对这些情况,我们为代码设计了$set
和$delete
方法。
其用法如下:
// Vue.set( target, propertyName/index, value )
// 为对象新增属性
this.$set(this.someObject, 'b', 2)
// 通过索引来修改数组
this.$set(this.someArray, 1, 2)
// Vue.delete( target, propertyName/index)
this.$delete(this.someObject, 'a')
下面是其代码实现:
由于新增属性时,
value
是自己传入的,需要重构defineReactive
函数。这里对于重构过程不再赘述,具体可以参考源代码。
function set (target, key, value) {
if (Array.isArray(target)) {// 数组直接调用splice方法
target.splice(key, 0, value);
return value;
}
if (typeof target === 'object' && target != null) { // 对象
const ob = target.__ob__;
// 通过Object.defineProperty为对象新加的属性,添加其对应的set/get方法,并进行依赖收集
defineReactive(target, key, value);
// 对象更新后通知视图更新
ob.dep.notify();
return value;
}
}
function del (target, key) {
if (Array.isArray(target)) {
// 代用splice删除元素
target.splice(key, 1);
return;
}
if (typeof target === 'object' && target != null) { // 对象
const ob = target.__ob__;
delete target.key;
// 删除对象属性后通知视图更新
ob.dep.notify();
}
}
对于数组,其实只是调用了splice
方法进行元素的添加和删除。
如果是对象,$set
方法会通过defineReactive
为对象新增属性,并保证属性具有响应性,而$delete
会帮用户将对象中的对应属性删除。最终,$set
和$delete
都会利用之前在Observer
中设置的dep
属性通知视图更新。
在实现对应的方法后,为了方便用户使用,将其设置到Vue
的原型上:
// src/state.js
export function stateMixin (Vue) {
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
}
import { stateMixin } from './state';
function Vue (options) {
this._init(options);
}
// some code ...
// 添加原型方法$set $delete
stateMixin(Vue);
export default Vue;
这样用户便可以从组件实例中方便的调用$set
和$delete
方法来保证数据的响应性
依赖收集的核心其实就是:
- 获取数据的值时将视图更新函数放到一个数组中
- 设置数据的值时依次执行数组中的函数来更新视图
这里可以回头再看一下Vue
官方文档中"数据更改追踪 "的流程图,相信你会有不一样的理解!
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK