阅读完该文章, 你不必定会掌握响应式原理, 但必定会有助于你掌握响应式原理, 源码这玩意儿若是光看看文章视频, 不本身亲手调试一下的话, 很难掌握.vue
为了方便调试, 我这里调试的不是源码, 而是打包好后的vue/dist/vue.esm.js
, 这样方便打日志, 也不用切不一样的文件. 因此准备工做就是用vue init webpack vuedemo
初始化一个项目, 而后在main.js
中初始化一些demo, 以下react
import Vue from 'vue'
/* eslint-disable no-new */
new Vue({
el: '#app',
data(){
return {
msg: '天气不错',
}
},
methods: {
click() {
//一些逻辑
}
},
template: `
<div>
<div>{{msg}}</div>
<button @click='click'>按钮</button>
</div>
`
})
复制代码
ps: 1. 切记不要经过挂载App
组件的方式调试, 直接用template
, 若是挂载组件, 会多不少重复的日志, 很是不利于调试 2. 这里只是给你们一些调试的建议(由于一开始我挂了个APP, 调得我好麻烦), 下文中不会出现 日志 相关的内容webpack
本文标题是 部分响应式原理, 响应式分三块: 侦听器(watch)
, computed(计算属性)
, render(模板渲染)
. 实现响应式的原理都是相同的, 只是在针对特性的业务逻辑上有些不一样. 本文就选render(模板渲染)
的响应式原理具体展开. 接下来会以调用栈
会主线, 进行原理的分析web
为了缩减篇幅, 在一些代码截取上, 我会忽略报错的警示代码以及与响应式原理无关的逻辑代码, 经过
...
取代, 不过仍是建议你们看下原函数,帮助理解数组
initmixin
入口函数function initmixin(Vue) {
Vue.prototype._init = function(options) {
var vm = this
...
//
initState(vm)
if (vm.$options.el) {
// 这行代码, 在第7步中'呼应1'会解释
vm.$mount(vm.$options.el);
}
}
}
复制代码
initState(vm)
, 入口函数, 逻辑很简单. 注意: 这里的opts.data
不是咱们写的那个data(){return {}}
方法, 而是一个name是mergedInstanceDataFn
的通过包装的方法var opts = vm.$options;
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
复制代码
initData(vm)
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {};
// 这里获取的data, 若是用我给的demo就是{msg: '天气不错'}
var keys = Object.keys(data);
var i = keys.length;
while (i--) {
var key = keys[i];
...
if (!isReserved(key)) {
// isReserved 函数是用来判断 data中的属性是否已 $ 或者 _ 开头, 由于这俩开头的属性可能会和vue内置的属性, API冲突, 因此vue选择不代理他们
// proxy 只是将data中的属性代理到vm实例上,这样就能够用this.xxx直接获取数据, 要实现响应式还得看下面的observe
proxy(vm, "_data", key);
}
}
...
//观察他们!
observe(data, true /* asRootData */);
复制代码
observer(value, asRootData)
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
) {
// 生成一个Observer实例
ob = new Observer(value);
}
复制代码
class Observer
声明一个观察者类if (Array.isArray(value)) {
// 若是是数组, 则调用observeArray, 其最终仍是会走walk
this.observeArray(value);
} else {
this.walk(value);
}
复制代码
Observer.prototype.walk
// 遍历每一个属性, 并执行defineReactive$$1
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
};
复制代码
defineReactive$$1
(直译就是定义响应式), 这是很关键的一个函数, 这个函数中定义了每一个响应式属性的getter
& setter
, 并在getter中执行依赖收集, 在setter中执行派发更新. 看这个函数前, 我强烈建议读者打开源码一块儿往下走, 由于这里特别绕, 若是光看文章, 很难理解// 这里进行了大幅的代码删减, 只为展现最直接的逻辑
function defineReactive$$1(obj, key, val, customSetter, shallow) {
// Dep是一个依赖类, 看下面代码
var dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
//标记①:
get: function reactiveGetter () {
// 注①
// Dep.target 是一个Watcher实例(可理解为一个订阅者), 若是Dep.target 不为undefined, 则去收集依赖
if (Dep.target) {//
dep.depend();
}
return value
},
//标记②:
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val;
dep.notify(); //派发更新
}
});
}
// Dep
var Dep = function Dep () {
this.id = uid++; // id
this.subs = []; // 订阅者数组
};
复制代码
注①:bash
Dep.target
何时被赋值, 它是个什么?1.全局搜索
Dep.target
, 会有两个函数中对其进行了赋值, 1.pushTarget
2.popTarget
能够看出这是逻辑相反的两个函数, 咱们就看pushTarget
2. 全局搜索pushTarget
, 会发现有5处地方调用了, 但只有Watcher.prototype.get
中给他传参了, 由于传的是this
, 因此很明显Dep.target
是一个Watcher的实例(即订阅者)app
/**
* Evaluate the getter, and re-collect dependencies.(翻译: 计算一个getter, 并从新收集依赖)
*/
Watcher.prototype.get = function get () {
pushTarget(this);
var value;
var vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
...
} finally {
// "touch" every roperty so they are all tracked as
// dependencies for deep watching
// watch中 有deep:true 属性的 会进入traverse, 进行递归绑定, 这里咱们忽略递归绑定逻辑
if (this.deep) {
traverse(value);
}
//这两步目前不用管
popTarget();
this.cleanupDeps();
}
return value
};
复制代码
3 找到了给
Dep.target
调用的地方, 也引入了一个Watcher
的概念, 那系统是何时建立的Watcher
实例的呢?全局搜索new Watcher
你会发现3个地方用到了, 分别是在Vue.prototype.$watch
(侦听器)中,initComputed
(计算属性)中, 以及mountComponent
(挂载组件)中(呼应1:mountComponent
是在Vue.prototype.$mount
中调用的),因此全部订阅者均来自于这三个地方.本要讲的也就是mountComponent
时建立的订阅者. 因此若是已我本文开头是给的demo
为例, 只会生成一个Watcher
(解释一下: demo中我只订阅了msg一个依赖, 若是我多订阅几个依赖, 依旧是一个Watcher.而若是是侦听器或者计算属性,则会生成对应多个watcher)
4 那么Watcher.prototype.get
是在何时调用的呢?在render Watcher
(就是本文要讲的Watcher, 即模板渲染Watcher)中, 有两个地方调用了, 一个是在function Watcher
的最下面,这个很明确, 意思就每新建一个Watcher
实例, 必然会执行一次Watcher.prototype.get
, 一个是在Watcher.prototype.run
中.这个根据调用栈 去倒推, 会发现是这样的调用栈: 1. 触发属性的set
(标记②) => 2.dep.notify
=> 3.Watcher.prototype.update
=> 4.queueWatcher
=> 5.flushSchedulerQueue
=> 6.watcher.run()
async
//function Watcher
...
this.value = this.lazy
? undefined
: this.get(); <= 这个就是调用```Watcher.prototype.get```
// Watcher.prototype.run
...
if (this.active) {
var value = this.get();<= 这个就是调用```Watcher.prototype.get```
复制代码
5 目前为止咱们知道了何时触发
Watcher.prototype.get
即(Dep.target何时是一个Watcher), 这个时候咱们还须要搞清楚何时触发属性的get
(标记①).在Watcher.prototype.get
中函数
Watcher.prototype.get = function get () {
...
try {
console.log(this.getter)
value = this.getter.call(vm, vm); <=这一行是触发了属性的get, 能够尝试注释它, 页面就会不渲染
} catch (e) {
...
}
复制代码
6 整理下触发订阅者进行依赖收集(呼应2:或者说依赖进行订阅者收集,后文会讲到)逻辑: 1. 执行模板渲染函数时, 若是有用到依赖属性, 则会触发依赖属性的
get
(好比我页面要渲染{{msg}}
, 则会触发msg的get
), 并执行依赖收集 2.当属性的值发生改变时, 会执行dep.notify通知视图层更新, 一样会触发依赖属性的get
. 咱们知道了触发依赖收集的条件, 而后咱们研究下属性是如何执行依赖收集的学习
//defineReactive$$1
function defineReactive$$1() {
...
Object.defineProperty(obj, key, {
...
get: function reactiveGetter () {
if (Dep.target) {
dep.depend(); <= 依赖收集入口
}
return value
},
}
// depend
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this); <= 注意这的Dep.target是一个Watcher
}
};
// addDep
Watcher.prototype.addDep = function addDep (dep) {
var id = dep.id;
if (!this.newDepIds.has(id)) {
// 订阅者执行依赖收集
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
dep.addSub(this);
}
}
};
// addSub
Dep.prototype.addSub = function addSub (sub) {
//呼应2: 依赖执行订阅者收集
this.subs.push(sub);
};
复制代码
7 收集完订阅者, 来看看如何通知订阅者完成派发更新的
function defineReactive$$1(obj, key, val, customSetter, shallow) {
...
Object.defineProperty(obj, key, {
...
set: function reactiveSetter (newVal) {
...
dep.notify(); //派发更新
}
});
}
// notify
Dep.prototype.notify = function notify () {
var subs = this.subs.slice();
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 对订阅者排序
subs.sort(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
// 呼应2: 挨个通知订阅者该更新了. 这也是为何为了便于理解,我偏向于叫订阅者收集,
// 由于他派发更新的主逻辑是,依赖收集订阅者,而后依赖挨个通知订阅者
subs[i].update();
}
};
复制代码
8
subs[i].update()
后还有一系列逻辑, 主要就是queueWatcher
=>flushSchedulerQueue
=>Watcher.prototype.run
=>Watcher.prototype.get
=>this.getter.call(vm, vm);
执行渲染 并触发属性中get
以上就是实现响应式的一系列最简洁的逻辑
get
, 并而后从新收集依赖(re-collect dep). 但本质都是触发渲染, 收集相关依赖Watcher.prototype.get
中有一步是cleanupDeps()
Watcher.prototype.get = function get () {
console.log('执行watcherget')
pushTarget(this);
var value;
var vm = this.vm;
try {
...
} catch (e) {
...
...
this.cleanupDeps(); // 这里
}
return value
};
// cleanupDeps
Watcher.prototype.cleanupDeps = function cleanupDeps () {
var i = this.deps.length;
while (i--) {
var dep = this.deps[i];
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this);
}
}
var 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;
};
复制代码
显然这一步是为了清洗依赖, 何时须要清洗依赖?
<template>
// 手动的将v-if置为false时, 本来须要订阅的msg,就无需再订阅了, 这也是cleanupDeps的做用
<div v-if=false>{{msg}}</div>
</template>
复制代码
以上是我总结的响应式原理
的最简化逻辑, 其实有不少须要拓展的分支, 但要经过文章媒介实在过于麻烦. 因此我以为想要学习源码的最好途径就是去debugger