前言
這兩天的把 1w 多行的 vue 過了一遍,看了一下把剩下在例項化 vue 建構函式前做的所有事情,做了一個總結,寫下了這個最終章,文中有哪些問題,還希望大家可以指出。
正文
為VNode原型新增 child 屬性監聽
var VNode = function VNode(
tag,
data,
children,
text,
elm,
context,
componentOptions,
asyncFactory
) {
this.tag = tag;
this.data = data;
this.children = children;
this.text = text;
this.elm = elm;
this.ns = undefined;
this.context = context;
this.fnContext = undefined;
this.fnOptions = undefined;
this.fnScopeId = undefined;
this.key = data && data.key;
this.componentOptions = componentOptions;
this.componentInstance = undefined;
this.parent = undefined;
this.raw = false;
this.isStatic = false;
this.isRootInsert = true;
this.isComment = false;
this.isCloned = false;
this.isOnce = false;
this.asyncFactory = asyncFactory;
this.asyncMeta = undefined;
this.isAsyncPlaceholder = false;
};
var prototypeAccessors = { child: { configurable: true } };
prototypeAccessors.child.get = function () {
return this.componentInstance
};
Object.defineProperties(VNode.prototype, prototypeAccessors);
複製程式碼
通過 Object.defineProperties
為 VNode
的原型繫結了物件 prototypeAccessors
,prototypeAccessors
設定 child
是可修改的狀態。
初始化Mixin
initMixin(Vue);
複製程式碼
在初始化的時候,給 initMixin
傳入了 Vue
建構函式:
function initMixin(Vue) {
Vue.prototype._init = function (options) {
var vm = this;
vm._uid = uid$3++;
var startTag, endTag;
if (config.performance && mark) {
startTag = "vue-perf-start:" + (vm._uid);
endTag = "vue-perf-end:" + (vm._uid);
mark(startTag);
}
vm._isVue = true;
// 合併選項
if (options && options._isComponent) {
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
{
initProxy(vm);
}
vm._self = vm;
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, `beforeCreate`);
initInjections(vm); // 在data/props之前解決注入
initState(vm);
initProvide(vm); // 解決後提供的data/props
callHook(vm, `created`);
if (config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(("vue " + (vm._name) + " init"), startTag, endTag);
}
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}
複製程式碼
Vue.prototype._init = function (options) {}
複製程式碼
在初始化的時候,為 Vue
在 原型上,新增了個 _init
方法,這個方法在例項化 vue
建構函式的時候會被呼叫:
function Vue(options) {
if (!(this instanceof Vue)
{
warn(`Vue is a constructor and should be called with the `new` keyword`);
}
this._init(options);
}
複製程式碼
這裡對 _init
不做太多的解釋,大家看一下 _init
都做了什麼,等到接下來講解例項化後 vue
的時候,會對這裡做詳細的解釋,包括生命週期的實現。
state 的 mixin
stateMixin(Vue);
複製程式碼
在處理完 initMixin
後,接著對 state
做了 mixin
的處理,給 stateMixin
傳入了 Vue
建構函式。
function stateMixin(Vue) {
var dataDef = {};
dataDef.get = function () { return this._data };
var propsDef = {};
propsDef.get = function () { return this._props };
{
dataDef.set = function () {
warn(
`Avoid replacing instance root $data. ` +
`Use nested data properties instead.`,
this
);
};
propsDef.set = function () {
warn("$props is readonly.", this);
};
}
Object.defineProperty(Vue.prototype, `$data`, dataDef);
Object.defineProperty(Vue.prototype, `$props`, propsDef);
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
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();
}
};
}
複製程式碼
官方給該方法的解釋是:
在使用object.defineproperty時,流在某種程度上存在直接宣告的定義物件的問題,因此我們必須在此處按程式構建該物件。
複製程式碼
var dataDef = {};
dataDef.get = function () { return this._data };
var propsDef = {};
propsDef.get = function () { return this._props };
{
dataDef.set = function () {
warn(
`Avoid replacing instance root $data. ` +
`Use nested data properties instead.`,
this
);
};
propsDef.set = function () {
warn("$props is readonly.", this);
};
}
Object.defineProperty(Vue.prototype, `$data`, dataDef);
Object.defineProperty(Vue.prototype, `$props`, propsDef);
複製程式碼
一開始,宣告瞭兩個物件 dataDef
和 propsDef
,並分別新增了 set
和 get
, 在我們操作 vm.$data
的時候,返回的就是 this._data
,$props
也是如此;
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
複製程式碼
這裡就是給 vue
例項繫結了 set
和 del
方法,這兩個方法在之前的章節將結果,連結在 Vue 原始碼解析(例項化前) – 初始化全域性API(二)
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();
}
};
複製程式碼
這裡,就是我們在做 vue
專案當中,經常使用的 api
之一,這裡就是它的具體實現。
具體的用法,和引數的意思,可以看官方的 api
文件,那裡對引數的解釋和用法寫的非常清楚,我這裡就講一下怎麼實現的,具體意思,大家看 api
文件就好。
官方api文件路徑:vm.$watch( expOrFn, callback, [options] )
在一開始,把當前的 this
指標儲存在一個 vm
當變數當中;
檢測如果當前的 cb
是物件的話,返回一個 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)
}
複製程式碼
這是 createWatcher
的實現。
其實就是對在呼叫 vm.$watch
時接受到引數,包括當前的 vue
例項,然後還是第一步要檢查 handler
是不是物件,這裡的 handler
就是之前的 cb
;
如果是物件的話,就用 handler
覆蓋 options
, handler.handler
去當作當前的 handler
去使用;
如果 handler
是字串的話,就去把當前 vue
例項的該屬性,當作 handler
去使用;
最後,返回一個新的 vm.$watch
。
options = options || {};
options.user = true;
複製程式碼
檢查接收的引數 options
是否存在,不存在就設定一個空物件;
設定的 options.user
為 true
;
var watcher = new Watcher(vm, expOrFn, cb, options);
複製程式碼
在設定完 options.user
後,就例項化了 Watcher
這個建構函式,這裡是非常核心的一塊內容,希望大家可以仔細看看,這一塊當作一個大分類來講,先把下面的兩行給講了:
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();
}
複製程式碼
如果設定了 options.immediate
為 true
,那麼把當前 cb
的 this
指向 vue
的例項化建構函式,並把 watcher.value
傳給 cb
;
最後返回一個函式,呼叫 watcher.teardown
。
Watcher 實現
Watcher 建構函式
var Watcher = function Watcher(
vm,
expOrFn,
cb,
options,
isRenderWatcher
) {
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
vm._watchers.push(this);
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$1;
this.active = true;
this.dirty = this.lazy;
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
if (typeof expOrFn === `function`) {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
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();
};
Watcher.prototype.get = function get() {
pushTarget(this);
var value;
var 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 {
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
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);
}
}
};
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;
};
Watcher.prototype.update = function update() {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
Watcher.prototype.run = function run() {
if (this.active) {
var value = this.get();
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
var 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);
}
}
}
};
Watcher.prototype.evaluate = function evaluate() {
this.value = this.get();
this.dirty = false;
};
Watcher.prototype.depend = function depend() {
var i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
};
Watcher.prototype.teardown = function teardown() {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
var i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
};
複製程式碼
這是有關 Watcher
建構函式所有實現的程式碼。
Watcher初始化
this.vm = vm;
if (isRenderWatcher) {
vm._watcher = this;
}
複製程式碼
當前的 watcher
物件的 vm
屬性指向的是 vue
例項化物件;
如果 isRenderWatcher
為 true
時, vue
的 _watcher
指向當前 this
;
vm._watchers.push(this);
複製程式碼
給 vue
例項化物件的 _watchers
陣列新增一個陣列項,就是當前的 watcher
例項化物件;
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$1;
this.active = true;
this.dirty = this.lazy;
this.deps = [];
this.newDeps = [];
this.depIds = new _Set();
this.newDepIds = new _Set();
this.expression = expOrFn.toString();
複製程式碼
這裡就是 watcher
初始化的一些屬性值;
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
}
}
if (typeof expOrFn === `function`) {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
warn(
"Failed watching path: "" + expOrFn + "" " +
`Watcher only accepts simple dot-delimited paths. ` +
`For full control, use a function instead.`,
vm
);
}
}
複製程式碼
如果接收到的 expOrFn
是個函式的話,當前 this
的 getter
就指向它;
否則,通過 parsePath
去格式化當前的路徑;
如果 expOrFn
並不是已單詞字元結尾的,就直接返回,設定一個空的 noop
函式給當前例項的 getter
屬性;
把 expOrFn
進行切割,遍歷切割後的 expOrFn
,並把切割後的每個陣列項當作要返回的函式的接收到的 obj
的屬性。
this.value = this.lazy ? undefined : this.get();
複製程式碼
如果 lazy
是 true
的話,this.value
的就是 undefinend
,否則就是呼叫 this.get
方法
Watcher 獲取值
function pushTarget(target) {
targetStack.push(target);
Dep.target = target;
}
function popTarget() {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
Watcher.prototype.get = function get() {
pushTarget(this);
var value;
var 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 {
if (this.deep) {
traverse(value);
}
popTarget();
this.cleanupDeps();
}
return value
};
複製程式碼
在 Vue 原始碼解析(例項化前) – 響應式資料的實現原理 講解 Dep
建構函式的時候,涉及到過,這裡開始仔細講解,具體呼叫的地方,可以去之前的章節檢視。
get
在一開始的時候,講呼叫了 pushTarget
方法,並把當前 watcher
例項化物件傳過去;
pushTarget
方法,就是給 targetStack
陣列新增一個陣列項,就是當前的 watcher
例項化物件;
把當前的 watcher
例項化物件 指向 Dep.target
;
this.getter
的 this
指向 vue
的例項化物件,並呼叫它,把當前的值去做獲取返回到 value
;
如果設定了 this.deep
為 true
,就代表使用者想要發現物件內部值的變化,這個時候呼叫 traverse
函式,目的是遞迴遍歷一個物件以喚起所有轉換的getter,以便將物件內的每個巢狀屬性收集為“深度”依賴項,把最後的結果更新到 value
;
popTarget
把在 targetStack
陣列中的最後一個刪除,並把 Dep.target
指向刪除後的陣列的最後一個陣列項。
在這裡,其實就是給在獲取當前資料時的
watcher
在一開始做了儲存 (targetStack
),在所有值的展示和處理做完以後,在清空了儲存 (targetStack
)
this.cleanupDeps();
複製程式碼
清除依賴項集合,接下來講。
return value
複製程式碼
最後返回 value
。
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);
}
}
};
複製程式碼
檢查佇列是否存在當前的 id
,該 id
其實就是 Dep
的例項化物件的 id
,把它新增到對應的佇列裡面去;
這裡其實比較簡單明瞭,就不做太複雜的解釋了,大家看一眼就明白了。
Watcher 清空佇列
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;
};
複製程式碼
把當前 Watcher
監聽佇列裡的 Watcher
物件從後往前清空,在把一些屬性初始化。
Watcher 更新
Watcher.prototype.update = function update() {
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
};
複製程式碼
如果是懶更新的話,設定 dirty
為 true
;
如果是同步更新的話,直接呼叫 run
方法;
否則,呼叫 queueWatcher
方法。
// 將觀察者推入觀察者佇列。
// 具有重複ID的工作將被跳過,除非在重新整理佇列時將其推送。
function queueWatcher(watcher) {
var id = watcher.id;
if (has[id] == null) {
has[id] = true;
if (!flushing) {
queue.push(watcher);
} else {
// 如果已經重新整理,則根據其ID拼接觀察程式
// 如果已經超過了它的ID,它將立即執行。
var i = queue.length - 1;
while (i > index && queue[i].id > watcher.id) {
i--;
}
queue.splice(i + 1, 0, watcher);
}
// 重新整理佇列
if (!waiting) {
waiting = true;
if (!config.async) {
flushSchedulerQueue();
return
}
nextTick(flushSchedulerQueue);
}
}
}
複製程式碼
如果設定的 config.async
是同步的,那麼就重新整理兩個佇列並執行 Watcher
結束當前方法;
否則的話,執行 nextTick
後執行 flushSchedulerQueue
。
var MAX_UPDATE_COUNT = 100;
var queue = [];
var activatedChildren = [];
var has = {};
var circular = {};
var waiting = false;
var flushing = false;
var index = 0;
// 重置計劃程式的狀態
function resetSchedulerState() {
index = queue.length = activatedChildren.length = 0;
has = {};
{
circular = {};
}
waiting = flushing = false;
}
function flushSchedulerQueue() {
flushing = true;
var watcher, id;
//在重新整理之前排隊佇列。
//這可以確保:
// 1.元件從父級更新為子級。 (因為父母總是在孩子面前建立)
// 2.元件的使用者觀察者在其渲染觀察者之前執行(因為在渲染觀察者之前建立使用者觀察者)
// 3.如果在父元件的觀察程式執行期間銷燬了元件,可以跳過其觀察者。
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();
//在開發構建中,檢查並停止迴圈更新。
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
}
}
}
// 重置狀態前保留髮布佇列的副本
var activatedQueue = activatedChildren.slice();
var updatedQueue = queue.slice();
resetSchedulerState();
// 呼叫元件更新和啟用的鉤子
callActivatedHooks(activatedQueue);
callUpdatedHooks(updatedQueue);
if (devtools && config.devtools) {
devtools.emit(`flush`);
}
}
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`);
}
}
}
function callActivatedHooks(queue) {
for (var i = 0; i < queue.length; i++) {
queue[i]._inactive = true;
activateChildComponent(queue[i], true /* true */);
}
}
複製程式碼
Watcher 執行
Watcher.prototype.run = function run() {
if (this.active) {
var value = this.get();
if (value !== this.value || isObject(value) || this.deep) {
var 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);
}
}
}
};
複製程式碼
只有 active
是 true
的時候,執行 run
才有效果,因為 active
在呼叫 teardown
方法的時候會變成 false
;
檢查新值和舊值是否相同,如果不相同的話,把新值和舊值傳遞給 cb
回撥,並把 this
指向 vue
例項。
這樣可能大家理解的會更容易點。
Watcher 評估
Watcher.prototype.evaluate = function evaluate() {
this.value = this.get();
this.dirty = false;
};
複製程式碼
評估觀察者的價值,這隻適用於懶惰的觀察者。
Watcher 依賴
Watcher.prototype.depend = function depend() {
var i = this.deps.length;
while (i--) {
this.deps[i].depend();
}
};
複製程式碼
這裡就是真正的實現通知依賴的部分。
Watcher 解除安裝
Watcher.prototype.teardown = function teardown() {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this);
}
var i = this.deps.length;
while (i--) {
this.deps[i].removeSub(this);
}
this.active = false;
}
};
複製程式碼
清空所有依賴,從後往前。
結束語
本來是準備這一章把 Vue
建構函式例項化前要做的所有事情都寫完,發現在 state
的 mixin
時候,涉及到了 watcher
,但是發現了,就先講解了,之後還是有同樣量的內容,所以還是準備單拿出來一講,篇幅太長了對大家學習和吸收並不友好。
接下來的一章,會講到:
events
的 minxin
:$on
、 $once
、 $off
、 $emit
;
lifecycle
的 minxin
:updated
、 $forceUpdate
、 $destroy
;
render
的 minxin
: $nextTick
、 render
下一章,就是 vue
原始碼解析(例項化前) – 初始化全域性 API(最終章)了,文中有寫的不對的,還希望大家可以積極指出。