前言
在上一章節我們已經粗略的分析了整個的Vue 的原始碼(還在草稿箱,需要梳理清楚才放出來),但是還有很多東西沒有深入的去進行分析,我會通過如下幾個重要點,進行進一步深入分析。
- 深入瞭解 Vue 響應式原理(資料攔截)
- 深入瞭解 Vue.js 是如何進行「依賴收集」,準確地追蹤所有修改
- 深入瞭解 Virtual DOM
- 深入瞭解 Vue.js 的批量非同步更新策略
- 深入瞭解 Vue.js 內部執行機制,理解呼叫各個 API 背後的原理
這一章節我們針對2. 深入瞭解 Vue.js 是如何進行「依賴收集」,準確地追蹤所有修改 來進行分析。
初始化Vue
我們簡單例項化一個Vue的例項, 下面的我們針對這個簡單的例項進行深入的去思考:
// app Vue instance
var app = new Vue({
data: {
newTodo: '',
},
// watch todos change for localStorage persistence
watch: {
newTodo: {
handler: function (newTodo) {
console.log(newTodo);
},
sync: false,
before: function () {
}
}
}
})
// mount
app.$mount('.todoapp')
複製程式碼
initState
在上面我們有新增一個watch
的屬性配置:
從上面的程式碼我們可知,我們配置了一個key為newTodo
的配置項, 我們從上面的程式碼可以理解為:
當newTodo
的值發生變化了,我們需要執行hander
方法,所以我們來分析下具體是怎麼實現的。
我們還是先從initState
方法檢視入手:
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
複製程式碼
我們來具體分析下initWatch
方法:
function initWatch (vm, watch) {
for (var key in watch) {
var handler = watch[key];
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}
複製程式碼
從上面的程式碼分析,我們可以發現watch
可以有多個hander
,寫法如下:
watch: {
todos:
[
{
handler: function (todos) {
todoStorage.save(todos)
},
deep: true
},
{
handler: function (todos) {
console.log(todos)
},
deep: true
}
]
},
複製程式碼
我們接下來分析createWatcher
方法:
function createWatcher (
vm,
expOrFn,
handler,
options
) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}
複製程式碼
總結:
- 從這個方法可知,其實我們的
hanlder
還可以是一個string
- 並且這個
hander
是vm
物件上的一個方法,我們之前已經分析methods
裡面的方法都最終掛載在vm
例項物件上,可以直接通過vm["method"]
訪問,所以我們又發現watch
的另外一種寫法, 直接給watch
的key
直接賦值一個字串名稱, 這個名稱可以是methods
裡面定一個的一個方法:
watch: {
todos: 'newTodo'
},
複製程式碼
methods: {
handlerTodos: function (todos) {
todoStorage.save(todos)
}
}
複製程式碼
接下來呼叫$watch
方法
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
return function unwatchFn () {
watcher.teardown();
}
};
複製程式碼
在這個方法,我們看到有一個immediate
的屬性,中文意思就是立即
, 如果我們配置了這個屬性為true
, 就會立即執行watch
的hander
,也就是同步 執行, 如果沒有設定, 則會這個watcher
是非同步執行,下面會具體分析怎麼去非同步執行的。 所以這個屬性可能在某些業務場景應該用的著。
在這個方法中new
了一個Watcher
物件, 這個物件是一個重頭戲,我們下面需要好好的分析下這個物件。
其程式碼如下(刪除只保留了核心的程式碼):
var Watcher = function Watcher (
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
vm._watchers.push(this);
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
}
}
this.value = this.lazy
? undefined
: this.get();
};
複製程式碼
主要做了如下幾件事:
- 將
watcher
物件儲存在vm._watchers
中 - 獲取
getter
,this.getter = parsePath(expOrFn);
- 執行
this.get()
去獲取value
其中parsePath
方法程式碼如下,返回的是一個函式:
var bailRE = /[^\w.$]/;
function parsePath (path) {
if (bailRE.test(path)) {
return
}
var segments = path.split('.');
return function (obj) {
for (var i = 0; i < segments.length; i++) {
if (!obj) { return }
obj = obj[segments[i]];
}
return obj
}
}
複製程式碼
在呼叫this.get()
方法中去呼叫value = this.getter.call(vm, vm);
然後會呼叫上面通過obj = obj[segments[i]];
去取值,如vm.newTodo
, 我們從
深入瞭解 Vue 響應式原理(資料攔截),已經知道,Vue 會將data
裡面的所有的資料進行攔截,如下:
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var 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 (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);
dep.notify();
}
});
複製程式碼
所以我們在呼叫vm.newTodo
時,會觸發getter
,所以我們來深入的分析下getter
的方法
getter
getter 的程式碼如下:
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
}
複製程式碼
- 首先取到值
var value = getter ? getter.call(obj) : val;
- 呼叫
Dep
物件的depend
方法, 將dep
物件儲存在target
屬性中Dep.target.addDep(this);
而target
是一個Watcher
物件 其程式碼如下:
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);
}
}
};
複製程式碼
生成的Dep
物件如下圖:
現在我們已經完成了依賴收集, 下面我們來分析當資料改變是,怎麼去準確地追蹤所有修改。
準確地追蹤所有修改
我們可以嘗試去修改data
裡面的一個屬性值,如newTodo
, 首先會進入set
方法,其程式碼如下:
set: function reactiveSetter (newVal) {
var 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 (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);
dep.notify();
}
複製程式碼
下面我來分析這個方法。
- 首先判斷新的value 和舊的value ,如果相等,則就直接return
- 呼叫
dep.notify();
去通知所有的subs
,subs
是一個型別是Watcher
物件的陣列 而subs
裡面的資料,是我們上面分析的getter
邏輯維護的watcher
物件.
而notify
方法,就是去遍歷整個subs
陣列裡面的物件,然後去執行update()
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (!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(function (a, b) { return a.id - b.id; });
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
複製程式碼
上面有一個判斷config.async
,是否是非同步,如果是非同步,需要排序,先進先出, 然後去遍歷執行update()
方法,下面我們來看下update()
方法。
Watcher.prototype.update = function update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
複製程式碼
上面的方法,分成三種情況:
- 如果
watch
配置了lazy
(懶惰的),不會立即執行(後面會分析會什麼時候執行) - 如果配置了
sync
(同步)為true
則會立即執行hander
方法 - 第三種情況就是會將其新增到
watcher
佇列(queue
)中
我們會重點分析下第三種情況, 下面是queueWatcher
原始碼
function queueWatcher (watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// queue the flush
if (!waiting) {
waiting = true;
if (!config.async) {
flushSchedulerQueue();
return
}
nextTick(flushSchedulerQueue);
}
}
}
複製程式碼
- 首先
flushing
預設是false
, 所以將watcher
儲存在queue
的陣列中。 - 然後
waiting
預設是false
, 所以會走if(waiting)
分支 config
是Vue
的全域性配置, 其async
(非同步)值預設是true
, 所以會執行nextTick
函式。
下面我們來分析下nextTick
函式
nextTick
nextTick
程式碼如下:
function nextTick (cb, ctx) {
var _resolve;
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
if (useMacroTask) {
macroTimerFunc();
} else {
microTimerFunc();
}
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
複製程式碼
nextTick
主要做如下事情:
- 將傳遞的引數
cb
的執行放在一個匿名函式中,然後儲存在一個callbacks
的陣列中 pending
和useMacroTask
的預設值都是false
, 所以會執行microTimerFunc()
(微Task)microTimerFunc()
的定義如下:
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
microTimerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
} else {
// fallback to macro
microTimerFunc = macroTimerFunc
}
複製程式碼
其實就是用Promise
函式(只分析Promise
相容的情況), 而Promise
是一個i額微Task 必須等所有的巨集Task 執行完成後才會執行, 也就是主執行緒空閒的時候才會去執行微Task;
現在我們檢視下flushCallbacks
函式:
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
複製程式碼
這個方法很簡單,
- 第一個是變更
pending
的狀態為false
- 遍歷執行
callbacks
陣列裡面的函式,我們還記得在nextTick
函式中,將cb
儲存在callbacks
中。
我們下面來看下cb
的定義,我們呼叫nextTick(flushSchedulerQueue);
, 所以cb
指的就是flushSchedulerQueue
函式, 其程式碼如下:
function flushSchedulerQueue () {
flushing = true;
var watcher, id;
queue.sort(function (a, b) { return a.id - b.id; });
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
if (watcher.before) {
watcher.before();
}
id = watcher.id;
has[id] = null;
watcher.run();
// in dev build, check and stop circular updates.
if (has[id] != null) {
circular[id] = (circular[id] || 0) + 1;
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? ("in watcher with expression \"" + (watcher.expression) + "\"")
: "in a component render function."
),
watcher.vm
);
break
}
}
}
// keep copies of post queues before resetting state
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
resetSchedulerState();
// call component updated and activated hooks
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush');
}
}
複製程式碼
- 首先將
flushing
狀態開關變成true
- 將
queue
進行按照ID
升序排序,queue
是在queueWatcher
方法中,將對應的Watcher
儲存在其中的。 - 遍歷
queue
去執行對應的watcher
的run
方法。 - 執行
resetSchedulerState()
是去重置狀態值,如waiting = flushing = false
- 執行
callActivatedHooks(activatedQueue);
更新元件 ToDO: - 執行
callUpdatedHooks(updatedQueue);
呼叫生命週期函式updated
- 執行
devtools.emit('flush');
重新整理除錯工具。
我們在3. 遍歷queue去執行對應的watcher的run 方法。, 發現queue
中有兩個watcher
, 但是我們在我們的app.js
中初始化Vue
的 時候watch
的程式碼如下:
watch: {
newTodo: {
handler: function (newTodo) {
console.log(newTodo);
},
sync: false
}
}
複製程式碼
從上面的程式碼上,我們只Watch
了一個newTodo
屬性,按照上面的分析,我們應該只生成了一個watcher
, 但是我們卻生成了兩個watcher
了, 另外一個watcher
到底是怎麼來的呢?
總結:
- 在我們配置的
watch
屬性中,生成的Watcher
物件,只負責呼叫hanlder
方法。不會負責UI的渲染 - 另外一個
watch
其實算是Vue
內建的一個Watch
(個人理解),而是在我們呼叫Vue
的$mount
方法時生成的, 如我們在我們的app.js
中直接呼叫了這個方法:app.$mount('.todoapp')
. 另外一種方法不直接呼叫這個方法,而是在初始化Vue
的配置中,新增了一個el: '.todoapp'
屬性就可以。這個Watcher
負責了UI的最終渲染,很重要,我們後面會深入分析這個Watcher
$mount
方法是最後執行的一個方法,所以他生成的Watcher
物件的Id
是最大的,所以我們在遍歷queue
之前,我們會進行一個升序 排序, 限制性所有的Watch
配置中生成的Watcher
物件,最後才執行$mount
中生成的Watcher
物件,去進行UI渲染。
$mount
我們現在來分析$mount
方法中是怎麼生成Watcher
物件的,以及他的cb
是什麼。其程式碼如下:
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
複製程式碼
- 從上面的程式碼,我們可以看到最後一個引數
isRenderWatcher
設定的值是true
, 表示是一個Render Watcher, 在watch
中配置的,生成的Watcher
這個值都是false
, 我們在Watcher
的建構函式中可以看到:
if (isRenderWatcher) {
vm._watcher = this;
}
複製程式碼
如果isRenderWatcher
是true
直接將這個特殊的Watcher
掛載在Vue
例項的_watcher
屬性上, 所以我們在flushSchedulerQueue
方法中呼叫callUpdatedHooks
函式中,只有這個watcher
才會執行生命週期函式updated
function callUpdatedHooks (queue) {
var i = queue.length;
while (i--) {
var watcher = queue[i];
var vm = watcher.vm;
if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'updated');
}
}
}
複製程式碼
- 第二個引數
expOrFn
, 也就是Watcher
的getter
, 會在例項化Watcher
的時候呼叫get
方法,然後執行value = this.getter.call(vm, vm);
, 在這裡就是會執行updateComponent
方法,這個方法是UI 渲染的一個關鍵方法,我們在這裡暫時不深入分析。 - 第三個引數是
cb
, 傳入的是一個空的方法 - 第四個引數傳遞的是一個
options
物件,在這裡傳入一個before
的function, 也就是,在UI重新渲染前會執行的一個生命中期函式beforeUpdate
上面我們已經分析了watch
的一個工作過程,下面我們來分析下computed
的工作過程,看其與watch
有什麼不一樣的地方。
computed
首先在例項化Vue
物件時,也是在initState
方法中,對computed
進行了處理,執行了initComputed
方法, 其程式碼如下:
function initComputed (vm, computed) {
// $flow-disable-line
var watchers = vm._computedWatchers = Object.create(null);
// computed properties are just getters during SSR
var isSSR = isServerRendering();
for (var key in computed) {
var userDef = computed[key];
var getter = typeof userDef === 'function' ? userDef : userDef.get;
if (getter == null) {
warn(
("Getter is missing for computed property \"" + key + "\"."),
vm
);
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else {
if (key in vm.$data) {
warn(("The computed property \"" + key + "\" is already defined in data."), vm);
} else if (vm.$options.props && key in vm.$options.props) {
warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
}
}
}
}
複製程式碼
上面程式碼比較長,但是我們可以總結如下幾點:
var watchers = vm._computedWatchers = Object.create(null);
在vm
例項物件上面掛載了一個_computedWatchers
的屬性,儲存了由computed
生成的所有的watcher
- 然後遍歷所有的
key
, 每一個key 都生成一個watcher
var getter = typeof userDef === 'function' ? userDef : userDef.get;
從這個程式碼可以延伸computed
的兩種寫法,如下:
computed: {
// 寫法1:直接是一個function
// strLen: function () {
// console.log(this.newTodo.length)
// return this.newTodo.length
// },
// 寫法2: 可以是一個物件,但是必須要有get 方法,
// 不過寫成物件沒有什麼意義, 因為其他的屬性,都不會使用。
strLen: {
get: function () {
console.log(this.newTodo.length)
return this.newTodo.length
}
}
}
複製程式碼
- 如果不是服務端渲染,就生成一個
watcher
物件,並且儲存在vm._computedWatchers
屬性中,但是這個與watch
生成的watcher
有一個重要的區別就是, 傳遞了一個屬性computedWatcherOptions
物件,這個物件就配置了一個lazy: ture
我們在Watcher
的建構函式中,有如下邏輯:
this.value = this.lazy
? undefined
: this.get();
複製程式碼
因為this.lazy
是true
所以不會執行this.get();, 也就不會立即執行computed
裡面配置的對應的方法。
defineComputed(vm, key, userDef);
就是將computed
的屬性,直接掛載在vm
上,可以直接通過vm.strLen
去訪問,不過在這個方法中,有針對是不是伺服器渲染做了區別,伺服器渲染會立即執行computed
的函式,獲取值,但是在Web 則不會立即執行,而是給get
賦值一個函式:
function createComputedGetter (key) {
return function computedGetter () {
var watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
watcher.evaluate();
}
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
}
複製程式碼
如果我們在我們的template
中引用了computed
的屬性,如:<div>{{strLen}}</div>
, 會執行$mount
去渲染模版的時候,會去呼叫strLen
,然後就會執行上面的computedGetter
的方法去獲取值, 執行的就是:
Watcher.prototype.evaluate = function evaluate () {
this.value = this.get();
this.dirty = false;
};
複製程式碼
執行了this.get()
就是上面分析watch
中的this.get()
.
思考:
我們上面基本已經分析了computed
邏輯的基本過程,但是我們好像還是沒有關聯上, 當我們的data
裡面的值變了,怎麼去通知computed
更新的呢?我們的computed
如下:
computed: {
strLen: function () {
return this.newTodo.length
},
}
複製程式碼
當我們改變this.newTodo
的時候,會執行strLen
的方法呢?
答案:
- 在上面我們已經分析了我們在我們的
template
中有引用strLen
,如<div>{{strLen}}</div>
,在執行$mount
去渲染模版的時候,會去呼叫strLen
,然後就會執行的computedGetter
的方法去獲取值,然後呼叫get
方法,也就是我們computed
配置的函式:
computed: {
strLen: function () {
return this.newTodo.length
}
},
複製程式碼
- 在執行上面方法的時候,會引用
this.newTodo
, 就會進入reactiveGetter
方法(深入瞭解 Vue 響應式原理(資料攔截))
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
}
複製程式碼
會將當前的Watcher
物件新增到dep.subs
佇列中。
- 當
this.newTodo
值改變時,就會執行reactiveSetter
方法,當執行dep.notify();
時,也就會執行computed
裡面的方法,從而達到當data
裡面的值改變時,其有引用這個data
屬性的computed
也就會立即執行。 - 如果我們定義了
computed
但是沒有任何地方去引用這個computed
, 即使對應的data
屬性變更了,也不會執行computed
方法的, 即使手動執行computed
方法, 如:app.strLen
也不會生效,因為在Watcher
的addDep
方法,已經判斷當前的watcher
不是一個新加入的watcher
了
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);
}
}
};
複製程式碼