閱讀完該文章, 你不必定會掌握響應式原理, 但必定會有助於你掌握響應式原理, 源碼這玩意兒若是光看看文章視頻, 不本身親手調試一下的話, 很難掌握.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