「试着读读 Vue 源代码」响应式系统是如何构建的 ❓
说明
-
首先这篇文章是读
vue.js
源代码的梳理性文章,文章分块梳理,记录着自己的一些理解及大致过程;更重要的一点是希望在vue.js 3.0
发布前深入的了解其原理。 -
如果你从未看过或者接触过
vue.js
源代码,建议你参考以下列出的vue.js
解析的相关文章,因为这些文章更细致的讲解了这个工程,本文只是以一些demo
演示某一功能点或API
实现,力求简要梳理过程。- 逐行级别的源码分析 - 强烈推荐
- Vue.js 源码分析
- Vue.js 源码解析
-
如果搞清楚了工程目录及入口,建议直接去看代码,这样比较高效 ( 遇到难以理解对应着回来看看别人的讲解,加以理解即可 )
-
文章所涉及到的代码,基本都是缩减版,具体还请参阅 vue.js - 2.5.17。
构建前对 data
选项的预处理
注:这里在对
data
选项初始化时,首先若存在data
选项,则调用initData
方法进行对data
预处理,最终调用observe(data, true /* asRootData */)
函数将data
数据对象转换成响应式的;若不存在,简单初始化为空对象处理即可。
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props);
if (opts.methods) initMethods(vm, opts.methods);
/****** 初始化 data 选项 ******/
if (opts.data) {
initData(vm);
} else {
observe((vm._data = {}), true /* asRootData */);
}
/****** 初始化 data 选项 ******/
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
initData
代码实现
function initData(vm: Component) {
/************************** data 提取并预处理 ***************************/
// 说明: 1. 根据上文,data 选项最终将被合并成一个函数,该函数返回 data 的值。
let data = vm.$options.data;
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {};
// 检验 data 选项是否是一个纯对象(注:在对data 选项合并处理之后走了一次 beforeCreate 钩子函数,防止 data 在那里被修改)
if (!isPlainObject(data)) {
data = {};
process.env.NODE_ENV !== 'production' &&
warn('数据函数应该返回一个对象', vm);
}
const keys = Object.keys(data);
const props = vm.$options.props;
const methods = vm.$options.methods;
let i = keys.length;
// 为了避免选项属性直接的覆盖,将迭代 data 选项
while (i--) {
const key = keys[i];
// 在非生产环境下 methods 存在:如果 methods 选项中的 key 在 data 中被定义将被警告
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(`方法“${key}”已经被定义为一个data属性。`, vm);
}
}
// 在非生产环境下 props 存在:如果 data 选项中的 key 在 props 中被定义了将被警告
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' &&
warn(`data的属性“${key}”已经被声明为一个props的属性。`, vm);
} else if (!isReserved(key)) {
// isReserved 作用: 检查字符串是否以$或_开头; 剔除这些特征字段,避免与 Vue 自身的属性和方法相冲突。
// 注: ① 如果你在data中定义了 `_message/$message` 你可以试一下 `this._message / $message` 能不能访问到?
// proxy 作用: data 数据代理, 使你能够:this.message 而不是 this.data.message;
// (`this.message <=> this.($data/_data/data).message`)。
proxy(vm, `_data`, key);
}
}
/************************** data 提取并预处理 ***************************/
observe(data, true /* asRootData */); // observe 函数将 data 数据对象转换成响应式
}
proxy
代码实现
/**
* 数据代理
* @param {Object} target 要在其上定义属性的对象
* @param {string} sourceKey 资源属性的名称
* @param {string} key 要定义或修改的属性的名称
*/
export function proxy(target: Object, sourceKey: string, key: string) {
// getter 函数
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key];
};
// setter 函数
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
- 上述代码思路:
- 提取
data
选项的值。 - 判断
data
选项内键名是否和 methods / props 内定义键名冲突。 - 对
data
选项内属性做一层代理,且剔除特征字符代理。 - 调用
observe
函数将data
数据对象转换成响应式。
- 提取
observe
观察函数
/**
* 在某些情况下,我们可能希望禁用组件更新计算中的观察。
*/
export let shouldObserve: boolean = true;
export function toggleObserving(value: boolean) {
shouldObserve = value;
}
/**
* 观察函数
* @param {Any} value 观测数据
* @param {Boolean} asRootData 被观测的数据是否是根级数据
*/
export function observe(value: any, asRootData: ?boolean): Observer | void {
// 值不是对象 或 值是虚拟DOM 直接退出
if (!isObject(value) || value instanceof VNode) {
return;
}
let ob: Observer | void;
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() && // 判断是否是服务端渲染
(Array.isArray(value) || isPlainObject(value)) && // 判断是否是数组 或 纯对象
Object.isExtensible(value) && // 判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)
!value._isVue // 避免 Vue 实例对象被观测
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++; // 根数据对象 target.__ob__.vmCount > 0
}
return ob;
}
- 上述代码思路:
- 对
data
选项的值进行类型判断; 若合法,调用Observer
类。 - 若是根数据对象,执行
ob.vmCount++
。 - 返回
Observer
实例。
- 对
Observer
观察者基类
/**
* 附加到每个被观察对象的观察者类。
* 一旦附加,观察者将目标对象的属性键转换为 getter/setter,用于收集依赖项和分派更新。
*/
export class Observer {
value: any; // 观察对象
dep: Dep; // Dep 是一个可观察的对象,可以有多个指令订阅它。
vmCount: number; // 将此属性作为根 $data 的 vm 数量
constructor(value: any) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
// 为 value 添加 __ob__ 不可枚举属性, 值为当前 `Observer` 实例
def(value, '__ob__', this);
// 后续的深度监测 data 数据下的二层级的数据可能是数组、对象等...
if (Array.isArray(value)) {
// 拦截数组变异方法。
// 判断是否可以使用 __proto__ 选择不同的执行方法。
const augment = hasProto ? protoAugment : copyAugment;
augment(value, arrayMethods, arrayKeys);
// 递归处理,解决嵌套数组。
this.observeArray(value);
} else {
this.walk(value);
}
}
/**
* 遍历每个属性并将它们转换为getter/setter。此方法只应在值类型为Object时调用。
*/
walk(obj: Object) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
/**
* 观察数组项的列表 - 使嵌套的数组或对象是响应式数据
*/
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]);
}
}
}
数组处理
拦截数组变异方法实现
// 创建一个新对象,使用 现有的对象(Array.prototype) 来提供新创建的对象的__proto__
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
// 需要拦截的数组变异方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
/**
* 拦截突变方法并发出事件
*/
methodsToPatch.forEach(function(method) {
// 缓存数组原始变异方法
const original = arrayProto[method];
// 在 arrayMethods 上添加这些变异方法并做一些事情。
def(arrayMethods, method, function mutator(...args) {
// 调用原始变异方法,并缓存其结果。
const result = original.apply(this, args);
const ob = this.__ob__;
let inserted;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
}
// 若存在将被插入的数组元素,将调用 observeArray 继续进行处理
if (inserted) ob.observeArray(inserted);
ob.dep.notify(); // 触发依赖
return result; // 将值结果返回
});
});
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
/**
* 通过使用 _proto__ 拦截原型链来增加目标对象或数组
*/
function protoAugment(target, src: Object, keys: any) {
target.__proto__ = src;
}
/**
* 通过定义隐藏属性来扩充目标对象或数组。
*/
function copyAugment(target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i];
def(target, key, src[key]);
}
}
下面是对数据变异拦截后的断点截图:

defineReactive
/**
* 在对象上定义反应性属性。
*/
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep(); // 订阅池
/******************** 剔除不可配置属性 *********************/
const property = Object.getOwnPropertyDescriptor(obj, key); // 返回指定对象上一个自有属性对应的属性描述符
if (property && property.configurable === false) {
return;
}
/******************** 满足预定义的 getter / setter *********************/
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]; // 触发取值函数 - 收集依赖
}
let childOb = !shallow && observe(val); // 默认深度观测
/******************** 劫持属性并配置 *********************/
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
/******************** 返回正确的属性值并收集依赖 *********************/
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
// target 保存着要被收集的依赖(观察者)
if (Dep.target) {
dep.depend(); // 收集依赖,丢到订阅池
if (childOb) {
childOb.dep.depend(); // 如有深层对象 继续收集依赖,丢到订阅池
// 若是数组 - 进行数组处理
if (Array.isArray(value)) {
dependArray(value); // 逐个触发数组每个元素的依赖收集
}
}
}
return value;
},
/******************** 设置正确的属性值并触发依赖 *********************/
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val; // 缓存旧值
// NaN 或 值 相等不处理
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter(); // 用来打印辅助信息
}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal); // 深度观测
dep.notify(); // 触发依赖
}
});
}
Dep
import type Watcher from './watcher';
import { remove } from '../util/index';
let uid = 0;
/**
* Dep 是一个可观察的对象,可以有多个指令订阅它。
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor() {
this.id = uid++;
this.subs = [];
}
addSub(sub: Watcher) {
this.subs.push(sub);
}
removeSub(sub: Watcher) {
remove(this.subs, sub);
}
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
// 首先稳定订阅者列表
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
// 正在评估的当前目标监视程序。这是全局惟一的,因为在任何时候都只能评估一个监视程序。
Dep.target = null;
const targetStack = [];
export function pushTarget(_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target);
Dep.target = _target;
}
export function popTarget() {
Dep.target = targetStack.pop();
}
-
上述陈述了响应式系统构建的部分内容,知道如何为属性构建响应式属性,即构造
getter/setter
;知道了在getter
时收集依赖,在setter
触发依赖。 同时对Dep
这个基类做了相应的分析。 -
就以前文例子,断点图简单展示了构建之后的结果:

Watcher
import {
warn,
remove,
isObject,
parsePath,
_Set as Set,
handleError
} from '../util/index';
import { traverse } from './traverse';
import { queueWatcher } from './scheduler';
import Dep, { pushTarget, popTarget } from './dep';
import type { SimpleSet } from '../util/index';
let uid = 0;
/**
* 一个观察者解析一个表达式,收集依赖关系,当表达式值改变时触发回调。这用于$watch() api和指令。
* 通过对“被观测目标”的求值,触发数据属性的 get 拦截器函数从而收集依赖
*/
export default class Watcher {
vm: Component; // 组件实例
expression: string; // 被观察的目标表达式
cb: Function; // 当被观察的表达式的值变化时的回调函数
id: number; // 观察者实例对象的唯一标识
deep: boolean; // 当前观察者实例对象是否是深度观测
user: boolean; // 标识当前观察者实例对象是 开发者定义的 还是 内部定义的
computed: boolean; // 标识当前观察者实例对象是否是计算属性的观察者
sync: boolean; // 告诉观察者当数据变化时是否同步求值并执行回调 默认 将需要重新求值并执行回调的观察者放到一个异步队列中,当所有数据的变化结束之后统一求值并执行回调
dirty: boolean; // for computed watchers, true 代表着还没有求值
active: boolean; // 观察者是否处于激活状态,或者可用状态
dep: Dep;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet; // 用来在 多次求值(当数据变化时重新求值的过程) 中避免收集重复依赖
newDepIds: SimpleSet; // 用来避免在 一次求值 的过程中收集重复的依赖
before: ?Function; // Watcher 实例的钩子
getter: Function;
value: any;
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object, // 当前观察者对象的选项
isRenderWatcher?: boolean // isRenderWatcher 用来标识该观察者实例是否是渲染函数的观察者
) {
/****************** 初始化一些实例属性 ******************/
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.computed = !!options.computed;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.computed = this.sync = false;
}
this.cb = cb;
this.id = ++uid;
this.active = true;
this.dirty = this.computed;
/****************** 初始化一些实例属性 ******************/
/****************** 实现避免收集重复依赖 ******************/
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
/****************** 实现避免收集重复依赖 ******************/
/****************** 解析路径 ******************/
this.expression =
process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '';
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = function() {};
process.env.NODE_ENV !== 'production' &&
warn(
`监视路径失败:“${exports}”监视程序只接受简单的点分隔路径。要实现完全控制,可以使用函数。`,
vm
);
}
}
/****************** 解析路径 ******************/
/****************** 求值 - 计算属性的观察者 与 普通属性观察者 处理方式 ******************/
if (this.computed) {
this.value = undefined;
this.dep = new Dep();
} else {
this.value = this.get();
}
}
/**
* 求值:触发访问器属性的 get 拦截器函数,并重新收集依赖项。
*/
get() {
pushTarget(this); // 给 Dep.target 赋值
let value;
const vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`);
} else {
throw e;
}
} finally {
// “触发”每一个属性,因此它们都作为依赖项被跟踪,以便进行深度监视
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
}
/**
* 向该指令添加一个依赖项。
*/
addDep(dep: Dep) {
const id = dep.id;
if (!this.newDepIds.has(id)) {
// 避免收集重复依赖
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this); // 移除已经没有关联关系的观察者
}
}
}
/**
* 清理依赖项集合.
*/
cleanupDeps() {
let i = this.deps.length;
while (i--) {
const dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
// 引用类型变量交换值的过程
let tmp = this.depIds;
this.depIds = this.newDepIds;
this.newDepIds = tmp;
this.newDepIds.clear();
tmp = this.deps;
this.deps = this.newDeps;
this.newDeps = tmp;
this.newDeps.length = 0;
}
/**
* 用户界面。将在依赖项更改时调用。
*/
update() {
if (this.computed) {
// 计算属性监视程序有两种模式: 延迟模式 和 激活模式。
// 默认情况下,它初始化为lazy,只有当至少有一个订阅者(通常是另一个计算属性或组件的呈现函数)依赖于它时才会被激活。
if (this.dep.subs.length === 0) {
// 在延迟模式下,除非必要,否则我们不想执行计算,因此我们只需将监视程序标记为dirty。
// 实际计算是在访问计算属性时在this.evaluate()中即时执行的。
this.dirty = true;
} else {
// 在激活模式下,我们希望主动执行计算,但只在值确实发生更改时通知订阅者。
this.getAndInvoke(() => {
this.dep.notify();
});
}
} else if (this.sync) {
this.run();
} else {
// 处于性能考量,异步更新队列,但最终都会执行 watcher.run(),此处不再细说。
queueWatcher(this);
}
}
/**
* 调度器的工作界面。将由调度程序调用。
*/
run() {
if (this.active) {
this.getAndInvoke(this.cb);
}
}
getAndInvoke(cb: Function) {
const value = this.get();
if (
value !== this.value ||
// 即使值是相同的,深度观察者和对象/数组上的观察者也应该触发,因为值可能发生了突变。
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value;
this.value = value;
this.dirty = false;
if (this.user) {
try {
cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`);
}
} else {
cb.call(this.vm, value, oldValue);
}
}
}
/**
* 计算并返回监视程序的值。这只对计算过的属性观察者调用。
*/
evaluate() {
if (this.dirty) {
this.value = this.get();
this.dirty = false;
}
return this.value;
}
/**
* Depend on this watcher. Only for computed property watchers.
*/
depend() {
if (this.dep && Dep.target) {
this.dep.depend();
}
}
/**
* 从所有依赖项的订阅服务器列表中删除self。
*/
teardown() {
if (this.active) {
// 从vm的监视者列表中删除self这是一个有点昂贵的操作,所以如果正在销毁vm,我们就跳过它。
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
let i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
}
}
上文已分析了构建响应式全部的内容,下面就 $watch 函数
渲染函数
的观察者 简单演示整个响应流过程。
渲染函数
上文在谈 new Vue()
最终程序走的是挂载函数,接下来,就看看挂载函数做了哪些处理。(注意:这里的挂载函数在初始化时已经被重写,给运行时版的 $mount 函数增加编译模板的能力)
import { mountComponent } from 'core/instance/lifecycle';
/**
* 公用的挂载方法
*
* @param {String | Element} el 挂载元素
* @param {Boolean} hydrating 用于 Virtual DOM 的补丁算法
* @returns {Function} 真正的挂载组件的方法
*/
Vue.prototype.$mount = function(
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
};
mountComponent
/**
* 组件挂载函数
*/
export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
if (process.env.NODE_ENV !== 'production') {
if (
(vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el ||
el
) {
warn(
'您正在使用Vue的仅运行时构建,其中模板编译器不可用。要么将模板预编译为呈现函数,要么使用编译器包含的构建。',
vm
);
} else {
warn('加载组件失败:模板或呈现函数未定义。', vm);
}
}
}
callHook(vm, 'beforeMount');
/******************* 把虚拟DOM渲染成真正的DOM ********************/
let updateComponent;
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name;
const id = vm._uid;
const startTag = `vue-perf-start:${id}`;
const endTag = `vue-perf-end:${id}`;
mark(startTag);
const vnode = vm._render(); // 调用 vm.$options.render 函数并返回生成的虚拟节点(vnode)
mark(endTag);
measure(`vue ${name} render`, startTag, endTag);
mark(startTag);
vm._update(vnode, hydrating); // 渲染虚拟节点为真正的 DOM
mark(endTag);
measure(`vue ${name} patch`, startTag, endTag);
};
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating);
};
}
/******************* 把虚拟DOM渲染成真正的DOM ********************/
/******************* 实例化观察者 ********************/
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate');
}
}
},
true
);
hydrating = false;
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm;
}
$watch 函数
$watch
: 观察 Vue 实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更复杂的表达式,用一个函数取代。
在上文初始化过程,谈到 $watch
的初始化,下面是代码实现。
export function stateMixin(Vue: Class<Component>) {
...
Vue.prototype.$watch = function(
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this;
// 这个里就是为了规范化 watch 参数,这里不细说。
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options);
}
options = options || {};
options.user = true; // 用户自定义回调
const watcher = new Watcher(vm, expOrFn, cb, options); // 实例化观察者
// 如果立即触发,则立即执行回调。否则放入异步队列中
if (options.immediate) {
cb.call(vm, watcher.value); // 这里注意,第二个参数(newVal)未传,所以你在回调拿不到
}
// 返回一个取消观察函数,用来停止触发回调
return function unwatchFn() {
watcher.teardown();
};
};
...
}
演示 demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>vue.js DEMO</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app">
<p>数据属性:{{ message }}</p>
<button @click="update">更新</button>
</div>
<script>
new Vue({
el: '#app',
data: {
message: 'hello vue.js'
},
mounted() {
this.$watch('message', function(newVal, oldVal) {
console.log(`message: __新值__${newVal}___旧值___${oldVal}`);
});
},
methods: {
update() {
this.message = `${this.message} ---- ${Math.random()}`;
}
}
});
</script>
</body>
</html>
演示效果 及步骤梳理
- 点击更新按钮,设置
message
属性,触发message
更新(setter
) dep.notify()
触发依赖,依次执行update
(这里包含渲染函数的观察者: 渲染函数 => watch )- 经过异步队列处理,统一调用更新程序
run
- 渲染函数(经过其处理,最新更新的值已经被渲染到 DOM 上):
$watch
:get
求值,执行回调,更新新值- 返回新值,缓存旧值,调用回调,传入新旧值。
- 渲染函数(经过其处理,最新更新的值已经被渲染到 DOM 上):
转载自:https://juejin.cn/post/6844903865892995086