基于 vue2.6.11
- 代码目录
bash
├─dist # 项目构建后的文件
├─scripts # 与项目构建相关的脚本和配置文件
├─flow # flow的类型声明文件
├─src # 项目源代码
│ ├─complier # 与模板编译相关的代码
│ ├─core # 通用的、与运行平台无关的运行时代码
│ │ ├─observe # 实现变化侦测的代码
│ │ ├─vdom # 实现virtual dom的代码
│ │ ├─instance # Vue.js实例的构造函数和原型方法
│ │ ├─global-api # 全局api的代码
│ │ └─components # 内置组件的代码
│ ├─server # 与服务端渲染相关的代码
│ ├─platforms # 特定运行平台的代码,如weex
│ ├─sfc # 单文件组件的解析代码
│ └─shared # 项目公用的工具代码
└─test # 项目测试代码
- Vue 的整个项目包含了类型检测相关、单元测试相关、与平台无关的核心代码以及跨平台运行的相关代码。
- 这里只是学习 Vue.js 的设计思想以及代码实现的相关逻辑,所以暂不去关心类型检测、单元测试以及特定平台运行等相关逻辑实现,仅关注它的核心代码,即 src/core 和 src/complier 这两个目录下的代码,并且接下来后续的学习也都是只在这两个目录的范围之内
学习路线
- 响应式实现:学习 Vue 中如何实现数据的响应式系统,从而达到数据驱动视图。
- 虚拟 DOM:学习什么是虚拟 DOM,以及 Vue 中的 DOM-Diff 原理
- 模板编译:学习 Vue 内部是怎么把 template 模板编译成虚拟 DOM,从而渲染出真实 DOM
- 实例方法:学习 Vue 中所有实例方法(即所有以$开头的方法)的实现原理
- 全局 API:学习 Vue 中所有全局 API 的实现原理
- 生命周期:学习 Vue 中组件的生命周期实现原理
- 指令:学习 Vue 中所有指令的实现原理
- 过滤器:学习 Vue 中所有过滤器的实现原理
- 内置组件:学习 Vue 中内置组件的实现原理
Vue.js 的响应式实现
众所周知,Vue 最大的特点之一就是数据驱动视图,那么什么是数据驱动视图呢?在这里,我们可以把数据理解为状态,而视图就是用户可直观看到页面。页面不可能是一成不变的,它应该是动态变化的,而它的变化也不应该是无迹可寻的,它或者是由用户操作引起的,亦或者是由后端数据变化引起的,不管它是因为什么引起的,我们统称为它的状态变了,它由前一个状态变到了后一个状态,页面也就应该随之而变化,所以我们就可以得到如下一个公式:
UI = render(state) 上述公式中:状态 state 是输入,页面 UI 输出,状态输入一旦变化了,页面输出也随之而变化。我们把这种特性称之为数据驱动视图。
Object 的响应式实现
- Vue.js 2.x
Object.defineProperty
定义 getter 和 setter- 无法检测到对象属性的添加或删除
- Vue.js 3.x
- Proxy
- Reflect
js
let car = {};
let val = 3000;
Object.defineProperty(car, "price", {
configurable: true,
enumerable: true,
get() {
console.log("price 属性被访问了");
return val;
},
set(newValue) {
console.log("price 属性被修改了");
val = newValue;
},
});
使对象变得可观测
首先定义一个
observer
类,将一个普通的object
对象转换为可观测的object
。并且会给value
新增一个__ob__
属性,相当于给value
打了一个标记,表示这个value
已经转化为响应式数据,避免重复操作 判断数据的类型,是否是数组,只有object
的数据才调用walk
把每一个属性转换成getter/setter
的形式监听
js
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
// walk 方法,遍历对象的每一个属性,调用 defineReactive 方法
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
defineReactive
方法,将一个对象转换成响应式对象,核心就是使用Object.defineProperty
方法,给对象的属性添加getter/setter
,当访问属性时就会触发getter
,修改属性时就会触发setter
js
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 实例化一个依赖管理器,生成一个依赖管理数组 dep
const dep = new Dep();
// Object.getOwnPropertyDescriptor(obj, prop) // obj:叫查找其属性的对象;prop:要查找的属性名或symbol
// MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
const property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return;
}
// cater for pre-defined getter/setters
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;
if (Dep.target) {
// 在 getter 中收集依赖
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;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
// 在 setter 中通知依赖更新
dep.notify();
},
});
}
依赖收集
我们可以监听到一个数据的变化,然后去更新视图,但是不可能数据变化就把全部视图更新。 因此需要知道,那些视图依赖了哪些数据,需要给每个数据建立一个依赖数组(一个数据可以被多处使用),谁依赖了(用到了)这个数据,就把谁放到依赖数组中,当这个数据变化,我们就去对应的依赖数组中,通知每个依赖,去更新视图。 所谓谁依赖了这个数据,就是谁用到了这个数据,比如
{{name}}
,{{age}}
,当渲染模板的时候,就会把name
和age
放到依赖数组中,当name
或age
变化,就会通知依赖数组中的每个依赖,去更新视图。
在getter
中收集依赖,在setter
中通知依赖更新
我们应该给每一个数据都建立一个依赖管理器,把这个数据依赖的视图放到这个依赖管理器中,当数据变化,就通知这个依赖管理器中的每个依赖去更新视图。
js
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
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() {
// stabilize the subscriber list first
const subs = this.subs.slice();
if (process.env.NODE_ENV !== "production" && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id);
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null;
const targetStack = [];
export function pushTarget(target: ?Watcher) {
targetStack.push(target);
Dep.target = target;
}
export function popTarget() {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
依赖到底是谁
在 Vue 中实现了一个Watcher
类,Watcher
类的实例就是我们前边说的"谁"。谁用到了数据,谁就是依赖,我们就给谁创建一个Watcher
实例。这个类就是用来做依赖收集的,当渲染模板的时候,就会创建一个Watcher
实例,这个Watcher
实例就是依赖,当数据变化,就会通知这个依赖去更新视图。
js
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
// options
if (options) {
this.deep = !!options.deep;
this.user = !!options.user;
this.lazy = !!options.lazy;
this.sync = !!options.sync;
this.before = options.before;
} else {
this.deep = this.user = this.lazy = this.sync = false;
}
this.cb = cb;
this.id = ++uid; // uid for batching
this.active = true;
this.dirty = this.lazy; // for lazy watchers
this.deps = [];
this.newDeps = [];
this.depIds = new Set();
this.newDepIds = new Set();
this.expression =
process.env.NODE_ENV !== "production" ? expOrFn.toString() : "";
// parse expression for getter
if (typeof expOrFn === "function") {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
process.env.NODE_ENV !== "production" &&
warn(
`Failed watching path: "${expOrFn}" ` +
"Watcher only accepts simple dot-delimited paths. " +
"For full control, use a function instead.",
vm
);
}
}
this.value = this.lazy ? undefined : this.get();
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
pushTarget(this);
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 {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value;
}
/**
* Add a dependency to this directive.
*/
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);
}
}
}
/**
* Clean up for dependency collection.
*/
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;
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run() {
if (this.active) {
const value = this.get();
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value;
this.value = value;
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue);
} catch (e) {
handleError(
e,
this.vm,
`callback for watcher "${this.expression}"`
);
}
} else {
this.cb.call(this.vm, value, oldValue);
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate() {
this.value = this.get();
this.dirty = false;
}
/**
* Depend on all deps collected by this watcher.
*/
depend() {
let i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown() {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
let i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
}
}

通过全局 API:Vue.use
和Vue.delete
触发对象的新增和删除
总结流程
Object
通过Observer
转换成getter/setter
的形式去追踪数据变化- 当外界通过
Watcher
读取数据时,会触发getter
从而将Watcher
添加到依赖中- 当数据发生变化,会触发
setter
,从而向Dep
中的依赖发送通知- 当
Watcher
收到通知,会向外界发送通知,可能更新视图,也有可能触发某个回调函数
Array 的响应式实现
数组响应式实现
依然还是要在获取数据的时候,收集依赖。在数据变化时,通知依赖更新
在哪里收集依赖
js
data(){
return{
arr:[1,2,3,4],
user:{},
isLogin:true
}
}
Array
型数据还是在getter
中收集
js
let arr = [1, 2, 3, 4];
arr.push(5);
Array.prototype.newPush = function (val) {
// 这里不能使用箭头函数简写,否则会找不到`this`
console.info("arr push");
this.push(val);
};
arr.newPush(6);

创建拦截器
- 对数组的操作进行拦截封装。创建拦截器:
arrayMethods
js
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from "../util/index";
const arrayProto = Array.prototype;
/* 创建一个对象,作为拦截器 */
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method];
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;
}
if (inserted) ob.observeArray(inserted);
// notify change
ob.dep.notify();
return result;
});
});
- 使用拦截器:
arrayMethods
js
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
// value.__proto__ = arrayMethods
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
如何收集依赖
js
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
如何访问依赖
js
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method];
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;
}
if (inserted) ob.observeArray(inserted);
// notify change
ob.dep.notify();
return result;
});
});
深度监听
在Vue
中,不管是 Object 还是 Array 实现的响应式数据,都是深度监听。也就是不但要监听自身数据变化,还要监听数据中所有的子数据变化
js
let arr = [
{
name: "Fa-ce",
age: 18,
info: {
address: "xxx",
},
},
];
数组元素的新增:我们向数组中新增了一个元素,我们需要把新增的元素转换为响应式数据
不足
日常开发,可能这样写
js
let arr = [1, 2, 3];
arr[0] = 4; // 通过下标修改数组元素,不会触发视图更新
arr.length = 0; // 通过修改数组长度,不会触发视图更新
这两种方法都无法监听到,也不会触发响应式更新. Vue2 提供了两个方法来弥补不足之处:
Vue.set
:修改数组中的元素,会触发响应式更新。this.$set(this.list, index, newValue)
Vue.delete
: 删除数组中的元素,会触发响应式更新。this.$delete(this.list, index)
虚拟 DOM
虚拟 DOM 是什么
虚拟 DOM 是一个 JavaScript 对象,通过对象的方式来描述一个 DOM 节点
js
<div class="container" id="app">
<a />
</div>;
// 虚拟 DOM
const VNode = {
tag: "div" /* 元素标签 */,
attrs: {
class: "a",
id: "b",
} /* 属性 */,
children: [
/* 子元素 */
{
tag: "h1",
children: "hello world",
},
{
tag: "p",
children: "hello vue",
},
],
};
我们把组成一个 DOM 节点必要东西通过一个JS
对象表示出来,那么这个JS
对象就可以用来描述这个DOM
节点。 我们把这个JS
对象就称为真实DOM
节点的虚拟DOM
节点
Vue 中的虚拟 DOM
VNode 类
js
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
constructor(
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text; /* 节点的文本 */
this.elm = elm; /* 当前虚拟节点对应的真实DOM节点 */
this.ns = undefined; /* 命名空间 */
this.context = context; /* 当前虚拟节点对应的Vue实例 */
this.fnContext = undefined; /* 函数式组件对应的Vue实例 */
this.fnOptions = undefined;
this.fnScopeId = undefined;
this.key =
data &&
data.key; /* 节点的key属性,被当作节点的标识,diff算法性能优化会用到 */
this.componentOptions = componentOptions;
this.componentInstance = undefined;
this.parent = undefined;
this.raw = false; /* 是否是原生HTML字符串,或者普通文本,innerHTML的时候为true */
this.isStatic = false;
this.isRootInsert = true;
this.isComment = false; /* 是否注释节点 */
this.isCloned = false;
this.isOnce = false;
this.asyncFactory = asyncFactory;
this.asyncMeta = undefined;
this.isAsyncPlaceholder = false;
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child(): Component | void {
return this.componentInstance;
}
}
export const createEmptyVNode = (text: string = "") => {
const node = new VNode();
node.text = text;
node.isComment = true;
return node;
};
export function createTextVNode(val: string | number) {
return new VNode(undefined, undefined, undefined, String(val));
}
// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
export function cloneVNode(vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
// #7975
// clone children array to avoid mutating original in case of cloning
// a child.
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
);
cloned.ns = vnode.ns;
cloned.isStatic = vnode.isStatic;
cloned.key = vnode.key;
cloned.isComment = vnode.isComment;
cloned.fnContext = vnode.fnContext;
cloned.fnOptions = vnode.fnOptions;
cloned.fnScopeId = vnode.fnScopeId;
cloned.asyncMeta = vnode.asyncMeta;
cloned.isCloned = true;
return cloned;
}
- VNode 的类型
- 注释节点:
isComment: true
,createEmptyVNode(text: string = '')
- 文本节点:
isStatic: true
,createTextVNode(val: string | number)
- 元素节点:
js
<div class="container" id="app">
<a />
</div>; // 虚拟 DOM
const VNode = {
tag: "div" /* 元素标签 */,
attrs: {
class: "a",
id: "b",
} /* 属性 */,
children: [
{
tag: "h1",
children: "hello world",
},
{
tag: "p",
children: "hello vue",
},
],
};
- 组件节点
- 组件节点除了元素节点具有的属性,还有两个特定的属性
componentOptions
:组件的选项,比如组件的名称,组件的 props 等componentInstance
:组件的实例,组件渲染完成后,组件的实例会保存在这个属性中
- 函数式组件节点
fnContext
:函数式组件的上下文fnOptions
:函数式组件的选项
- 克隆节点:
cloneVNode(vnode: VNode)
,把一个已经存在的节点进行复制一份,主要做模板编译优化时使用
- VNode 的作用
- 我们在视图渲染之前,把写好的 template 模板先编译成
VNode
并缓存下来,等到数据发生变化需要重新渲染的时候,我们把数据发生变化后生成的VNode
和之前缓存好的VNode
进行对比,找出差异,然后有差异的 VNode 对应的真实 DOM 节点就是我们需要进行更新重新渲染的节点,最后根据有差异的 VNode 节点创建出真实的 DOM 节点插入到视图中,从而更新视图
- 我们在视图渲染之前,把写好的 template 模板先编译成
Vue 中的 DOM-diff 算法
-
Patch
- 指对旧的 VNode 进行修补,打补丁得到新的 VNode
- 以新的 VNode 为基准,改造旧的 VNode 使其与新的 VNode 保持一致。这就是 patch 的过程
- 创建节点:新的 VNode 中有而旧的 oldVNode 中没有,就在旧的 VNode 中创建
- 删除节点:新的 VNode 中没有而旧的 oldVNode 中有,就从旧的 VNode 中删除
- 更新节点:新的 VNode 中和旧的 oldVNode 中都有的节点,就以新的 VNode 为准,更新旧的 oldVNode
-
创建节点
- 创建节点只有三种类型:注释节点、文本节点、元素节点。Vue 创建节点的时候需要先判断在新的 VNode,会根据节点的类型,调用不同的创建方法插入到 DOM 中
jsfunction createElm(vnode, parentElm, refElm) { const tag = vnode.tag; if (isDef(tag)) { // 创建元素节点 vnode.elm = nodeOps.createElement(tag, vnode); // 创建子节点 createChildren(vnode, children, insertedVnodeQueue); // 插入 DOM insert(parentElm, vnode.elm, refElm); } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text); insert(parentElm, vnode.elm, refElm); } else { vnode.elm = nodeOps.createTextNode(vnode.text); insert(parentElm, vnode.elm, refElm); } }
-
删除节点
jsfunction removeNode(el) { const parent = nodeOps.parentNode(el); // element may have already been removed due to v-html / v-text if (isDef(parent)) { nodeOps.removeChild(parent, el); } }