数据相关

通过 stateMixin 方法中挂载到 vue 原型上

1
2
3
4
5
export function stateMixin(Vue) {
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
Vue.prototype.$watch = function (expOrFn, cb, options) {};
}

vm.$set

全局 Vue.set 的别名,其用法相同。对象不能是 Vue 实例,或者 Vue 实例的根数据对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
export function set(target, key, val) {
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${target}`
);
}
// 1. 判断如果传入的target是数组并且传入的key是有效索引的话,那么就取当前数组长度与key这两者的最大值作为数组的新长度,然后使用数组的splice方法将传入的索引key对应的val值添加进数组
// splice方法已经被重写,当使用splice方法向数组内添加元素时,该元素会自动被变成响应式的
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val;
}
// 2. 判断传入的key是否已经存在于target中,如果存在,表明这次操作不是新增属性,而是对已有的属性进行简单的修改值,那么就只修改属性值即可
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
// 3. 获取到traget的__ob__属性,该属性是否为true标志着target是否为响应式对象,接着判断如果tragte是 Vue 实例,或者是 Vue 实例的根数据对象,则抛出警告并退出程序
const ob = target.__ob__;
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid adding reactive properties to a Vue instance or its root $data " +
"at runtime - declare it upfront in the data option."
);
return val;
}
// 4. 判断如果ob属性为false,那么表明target不是一个响应式对象,那么我们只需简单给它添加上新的属性,不用将新属性转化成响应式
if (!ob) {
target[key] = val;
return val;
}
// 5. 如果target是对象,并且是响应式,那么就调用defineReactive方法将新属性值添加到target上,defineReactive方会将新属性添加完之后并将其转化成响应式,最后通知依赖更新
defineReactive(ob.value, key, val);
ob.dep.notify();
return val;
}

vm.$delete

全局 Vue.delete 的别名,其用法相同。删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开 Vue 不能检测到属性被删除的限制

  • 目标对象不能是一个 Vue 实例或 Vue 实例的根数据对象
  • 原理基本与 set 相同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
export function del(target, key) {
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot delete reactive property on undefined, null, or primitive value: ${target}`
);
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return;
}
const ob = target.__ob__;
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid deleting properties on a Vue instance or its root $data " +
"- just set it to null."
);
return;
}
// 判断传入的key是否存在于target中,如果key本来就不存在于target中,那就不用删除,直接退出程序即可
if (!hasOwn(target, key)) {
return;
}
// 如果target是对象,并且传入的key也存在于target中,那么就从target中将该属性删除,
// 同时判断当前的target是否为响应式对象,如果是响应式对象,则通知依赖更新;如果不是,删除完后直接返回不通知更新
delete target[key];
if (!ob) {
return;
}
ob.dep.notify();
}

vm.$watch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
// 用法
// 键路径
vm.$watch(
"a.b.c",
function (newVal, oldVal) {
// 做点什么
if (unwatch) {
unwatch();
}
},
{
deep: true, // 监听数组的变动不需要
immediate: true, // 不能在第一次回调时取消侦听给定的 property
}
);

// 函数
vm.$watch(
function () {
// 表达式 `this.a + this.b` 每次得出一个不同的结果时
// 处理函数都会被调用。
// 这就像监听一个未被定义的计算属性
return this.a + this.b;
},
function (newVal, oldVal) {
// 做点什么
}
);

// 返回一个取消观察函数,用来停止触发回调
var unwatch = vm.$watch("a", cb);
unwatch();

// 原理
Vue.prototype.$watch = function (expOrFn, cb, options) {
const vm = this;
// 1. 判断传入的回调函数是否为一个对象
// - 如果是,表明是把第二个参数回调函数cb和第三个参数选项options合起来传入的,此时调用createWatcher函数
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options);
}
options = options || {};
// 2. 表示实例是用户手动调用$watch方法创建而来的,区分用户创建的watcher实例和Vue内部创建的watcher实例
options.user = true;
// 3. 创建一个watcher实例
const watcher = new Watcher(vm, expOrFn, cb, options);
// 4. 判断如果在选项参数options中指定的immediate为true,则立即用被观察数据当前的值触发回调
if (options.immediate) {
cb.call(vm, watcher.value);
}
// 5. 返回一个取消观察函数unwatchFn,用来停止触发回调
return function unwatchFn() {
watcher.teardown();
};

// 销毁分析
// 1. 上面watcher实例,它观察了数据a和数据b,那么它就依赖了数据a和数据b,那么这个watcher实例就存在于数据a和数据b的依赖管理器depA和depB中,
// 同时watcher实例的deps属性中也记录了这两个依赖管理器,即this.deps=[depA,depB]
// 2. 当取消观察时,就遍历this.deps,让每个依赖管理器调用其removeSub方法将这个watcher实例从自己的依赖列表中删除
vm.$watch(
function () {
return this.a + this.b;
},
function (newVal, oldVal) {
// 做点什么
}
);
};

// deep 原理
// 思路:
// 1. 成为内层数据的依赖: 数据变化会通知所有的依赖
// 2. 创建 watcher 实例时把对象内部所有的值都递归读取一遍,watcher 实例加入到对象内部所有值的依赖列表中

export default class Watcher {
constructor(/* ... */) {
// ...
this.value = this.get();
}
get() {
// ...
// "touch" every property so they are all tracked as dependencies for deep watching
if (this.deep) {
traverse(value);
}
return value;
}
}

const seenObjects = new Set();

export function traverse(val) {
_traverse(val, seenObjects);
seenObjects.clear();
}

function _traverse(val, seen) {
let i, keys;
const isA = Array.isArray(val);
// 1. 判断传入的val类型,如果它不是Array或object,再或者已经被冻结,那么直接返回,退出程序
if (
(!isA && !isObject(val)) ||
Object.isFrozen(val) ||
val instanceof VNode
) {
return;
}
// 2. 拿到val的dep.id,存入创建好的集合Set中,因为集合相比数据而言它有天然的去重效果,以此来保证存入的dep.id没有重复,不会造成重复收集依赖
if (val.__ob__) {
const depId = val.__ob__.dep.id;
if (seen.has(depId)) {
return;
}
seen.add(depId);
}
// 3. 判断如果是数组,则循环数组,将数组中每一项递归调用_traverse;如果是对象,则取出对象所有的key,然后执行读取操作,再递归内部值
if (isA) {
i = val.length;
while (i--) _traverse(val[i], seen);
} else {
keys = Object.keys(val);
i = keys.length;
while (i--) _traverse(val[keys[i]], seen);
}
}

事件相关

通过 eventsMixin 方法中挂载到 vue 原型上

1
2
3
4
5
6
export function eventsMixin(Vue) {
Vue.prototype.$on = function (event, fn) {};
Vue.prototype.$once = function (event, fn) {};
Vue.prototype.$off = function (event, fn) {};
Vue.prototype.$emit = function (event) {};
}

vm.$on

监听当前实例上的自定义事件。事件可以由 vm.$emit 触发。回调函数会接收所有传入事件触发函数的额外参数
定义一个事件中心,通过 $on 订阅事件,将事件存储在事件中心里面,然后通过 $emit 触发事件中心里面存储的订阅事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.prototype.$on = function (event, fn) {
const vm = this;
// 1. 判断传入的事件名是否是一个数组,如果是数组,就表示需要一次性订阅多个事件,就遍历该数组,将数组中的每一个事件都递归调用$on方法将其作为单个事件订阅
// 2. 不是就当做单个事件名来处理,以该事件名作为key,先尝试在当前实例的_events属性中获取其对应的事件列表,如果获取不到就给其赋空数组为默认值,并将第二个参数回调函数添加进去
// --> _events属性用来作为当前实例的事件中心,所有绑定在这个实例上的事件都会存储在事件中心_events属性中
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$on(event[i], fn);
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn);
}
return vm;
};

vm.$emit

触发当前实例上的事件。附加参数都会传给监听器回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Vue.prototype.$emit = function (event) {
const vm = this;
// 1. 根据传入的事件名从当前实例的_events属性(即事件中心)中获取到该事件名所对应的回调函数cbs
let cbs = vm._events[event];
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs;
// 2. 获取传入的附加参数args
const args = toArray(arguments, 1);
// 3. 遍历执行回调函数并将附加参数args传给该回调
for (let i = 0, l = cbs.length; i < l; i++) {
try {
cbs[i].apply(vm, args);
} catch (e) {
handleError(e, vm, `event handler for "${event}"`);
}
}
}
return vm;
};

vm.$off

移除自定义事件监听器

  • 如果没有提供参数,则移除所有的事件监听器
  • 如果只提供了事件,则移除该事件所有的监听器
  • 如果同时提供了事件与回调,则只移除这个回调的监听器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Vue.prototype.$off = function (event, fn) {
const vm = this;
// 1. 如果没有提供参数,则移除所有的事件监听器。把_events属性重新置为空对象即可
if (!arguments.length) {
vm._events = Object.create(null);
return vm;
}
// 2. 如果传入的需要移除的事件名是一个数组,就表示需要一次性移除多个事件,那么我们只需同订阅多个事件一样,遍历该数组,然后将数组中的每一个事件都递归调用$off方法进行移除即可
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
this.$off(event[i], fn);
}
return vm;
}
// 3. 获取到需要移除的事件名在事件中心中对应的回调函数cbs
const cbs = vm._events[event];
// 如果cbs不存在,那表明在事件中心从来没有订阅过该事件,直接返回
if (!cbs) {
return vm;
}
// 4. 如果cbs存在,但是没有传入回调函数fn,这就是第二种情况: 如果只提供了事件,则移除该事件所有的监听器
if (!fn) {
vm._events[event] = null;
return vm;
}
// 5. 如果既传入了事件名,又传入了回调函数,cbs也存在,那这就是第三种情况: 如果同时提供了事件与回调,则只移除这个回调的监听器
// 遍历所有回调函数数组cbs,如果cbs中某一项与fn相同,或者某一项的fn属性与fn相同,那么就将其从数组中删除即可
if (fn) {
// specific handler
let cb;
let i = cbs.length;
while (i--) {
cb = cbs[i];
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1);
break;
}
}
}
return vm;
};

vm.$once

监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除
定义一个子函数,用这个子函数来替换原本订阅事件所对应的回调,当触发订阅事件时,其实执行的是这个子函数,然后再子函数内部先把该订阅移除,再执行原本的回调,以此来达到只触发一次的目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.prototype.$once = function (event, fn) {
const vm = this;
function on() {
// 2. 先通过$off方法移除订阅的事件,这样确保该事件不会被再次触发,接着执行原本的回调fn
vm.$off(event, on);
fn.apply(vm, arguments);
}
// 3. 防止在触发该事件后再调用 $off 方法移除事件失败,因为 _events 中该事件订阅的事件被替换了
// 为了解决这一问题,需要给on上绑定一个fn属性,属性值为用户传入的回调fn,这样在使用$off移除事件的时候,$off内部会判断如果回调函数列表中某一项的fn属性与fn相同时,就可以成功移除事件了
on.fn = fn;
// 1. 先通过$on方法订阅事件,同时所使用的回调函数并不是原本的fn而是子函数on
vm.$on(event, on);
return vm;
};

生命周期相关

1
2
3
4
5
6
7
8
export function lifecycleMixin(Vue) {
Vue.prototype.$forceUpdate = function () {};
Vue.prototype.$destroy = function (fn) {};
}

export function renderMixin(Vue) {
Vue.prototype.$nextTick = function (fn) {};
}

vm.$mount

在跨平台的代码中挂载到 Vue 原型上的

  • vm.$mount([elementOrSelector]),这个方法返回实例自身,因而可以链式调用其它实例方法
  • 如果 Vue 实例在实例化时没有收到 el 选项,则它处于 未挂载 状态,没有关联的 DOM 元素。可以使用 vm.$mount() 手动地挂载一个未挂载的实例
  • 如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素,并且必须使用原生 DOM API 把它插入文档中

vm.$forceUpdate

在 lifecycleMixin 函数中挂载到 Vue 原型上的

  • 迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件
  • 实例 watcher 执行了 update 方法
1
2
3
4
5
6
Vue.prototype.$forceUpdate = function () {
const vm = this;
if (vm._watcher) {
vm._watcher.update();
}
};

vm.$nextTick

在 renderMixin 函数中挂载到 Vue 原型上的

  • vm.$nextTick 是全局 Vue.nextTick 的别名,其用法相同
  • 将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上
  • Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个事件队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到事件队列中一次

内部原理:

  1. 能力检测
  • 内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替
  • 宏任务耗费的时间是大于微任务的,在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,使用宏任务;但是,各种宏任务之间也有效率的不同,需要根据浏览器的支持情况,使用不同的宏任务
  1. 根据能力检测以不同方式执行回调队列,两个注意点
  • 如何保证只在接收第一个回调函数时执行异步方法
    • nextTick 源码中使用了一个异步锁的概念,即接收第一个回调函数时,先关上锁,执行异步方法。此时,浏览器处于等待执行完同步代码就执行异步代码的情况
  • 执行 flushCallbacks 函数时为什么需要备份回调函数队列?执行的也是备份的回调函数队列?

    • 可能会出现这么一种情况: nextTick 的回调函数中还使用 nextTick。如果 flushCallbacks 不做特殊处理,直接循环执行回调函数,会导致里面 nextTick 中的回调函数会进入回调队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 能力检测
(function () {
let timerFunc;

if (typeof Promise !== "undefined" && isNative(Promise)) {
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
if (isIOS) setTimeout(noop);
};
isUsingMicroTask = true;
} else if (
!isIE &&
typeof MutationObserver !== "undefined" &&
(isNative(MutationObserver) ||
MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
});

// 执行回调队列
const callbacks = [];
let pending = false; // 异步锁

function flushCallbacks() {
pending = false;
// 防止出现nextTick中包含nextTick时出现问题,在执行回调函数队列前,提前复制备份并清空回调函数队列
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
export function nextTick(cb, ctx) {
let _resolve;
// 将回调函数推入回调队列
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, "nextTick");
}
} else if (_resolve) {
_resolve(ctx);
}
});
// 如果异步锁未锁上,锁上异步锁,调用异步函数,准备等同步函数执行完后,就开始执行回调函数队列
if (!pending) {
pending = true;
timerFunc();
}
// 如果没有提供回调,并且支持Promise,返回一个Promise
if (!cb && typeof Promise !== "undefined") {
return new Promise((resolve) => {
_resolve = resolve;
});
}
}

vm.$destory

在 lifecycleMixin 函数中挂载到 Vue 原型上的