首先这篇文章是读 vue.js
源代码的梳理性文章,文章分块梳理,记录着本身的一些理解及大体过程;更重要的一点是但愿在 vue.js 3.0
发布前深刻的了解其原理。javascript
若是你从未看过或者接触过 vue.js
源代码,建议你参考如下列出的 vue.js
解析的相关文章,由于这些文章更细致的讲解了这个工程,本文只是以一些 demo
演示某一功能点或 API
实现,力求简要梳理过程。html
若是搞清楚了工程目录及入口,建议直接去看代码,这样比较高效 ( 遇到难以理解对应着回来看看别人的讲解,加以理解便可 )vue
文章所涉及到的代码,基本都是缩减版,具体还请参阅 vue.js - 2.5.17。java
data
选项的预处理在上文 「试着读读 Vue 源代码」new Vue()发生了什么 ❓, 着重梳理了 new Vue(()
其代码执行的全过程,了解了 Vue
内部到底作了哪些工做,但就响应式系统的构建
并无展开描述,Vue
在哪里开始对Data
选项进行响应式体系构建呢?没错,就是上文简单提过的 _init()
内部执行的 initState
函数。react
注:这里在对
data
选项初始化时,首先若存在data
选项,则调用initData
方法进行对data
预处理,最终调用observe(data, true /* asRootData */)
函数将data
数据对象转换成响应式的;若不存在,简单初始化为空对象处理便可。git
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]);
}
}
}
复制代码
拦截数组变异方法实现github
// 建立一个新对象,使用 现有的对象(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
这个基类作了相应的分析。express
就之前文例子,断点图简单展现了构建以后的结果:
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
$watch
:
get
求值,执行回调,更新新值