源码地址:传送门
Vue
为用户提供了一个特别方便的功能:数据更新时自动更新DOM
。本文将详细介绍Vue
源码中该特性实现的核心思路,深入理解Vue
数据和视图的更新关系。
用文字描述的话,其流程如下:
- 组挂载,执行
render
方法生成虚拟DOM
。此时在模板中用到的数据,会从vm
实例上进行取值 - 取值会触发
data
选项中定义属性的get
方法 get
方法会将渲染页面的watcher
作为依赖收集到dep
中- 当修改模板中用到的
data
中定义的属性时,会通知dep
中收集的watcher
执行update
方法来更新视图 - 重新利用最新的数据来执行
render
方法生成虚拟DOM
。此时不会再收集重复的渲染watcher
渲染
watcher
就是用来更新视图的watcher
,具体的执行过程在组件初渲染中有详细介绍,它的主要作用如下:
- 执行
vm._render
方法生成虚拟节点- 执行
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
。在之后涉及到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
方法。
其用法如下:
// 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
官方文档中"数据更改追踪 "的流程图,相信你会有不一样的理解!