data 中的資料是如何處理的?
每一次例項化一個元件,都會呼叫 initData 然後呼叫 observe 方法,observe 方法呼叫了 new Observer(value), 並且返回 __ob__
。
在 new Observer 中做了兩件事:
- 把當前例項掛載到資料的
__ob__
屬性上,這個例項在後面有用處。 - 根據資料型別(陣列還是物件)區別處理
如果是物件:
橫向遍歷物件屬性,呼叫 defineReactive;
遞迴呼叫 observe 方法, 當屬性值不是陣列或者物件停止遞迴
下面對 defineReactive 方法做了詳細的註釋:
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter ? : ? Function,
shallow ? : boolean
) {
const dep = new Dep(); // 閉包建立依賴物件; 每個物件的屬性都有自己的dep
// 下面是針對已經通過Object.defineProperty 或者Object.seal Object.freeze 處理過的資料
const property = Object.getOwnPropertyDescriptor(obj, key);
// 如果configurable為false ,再次Object.defineProperty(obj, key)會報錯,並且不會成功;所以直接返回
// 所以可以針對性的使用Object.freeze/seal優化效能。
if (property && property.configurable === false) {
return;
}
const getter = property && property.get;
const setter = property && property.set;
// 正常情況下 我們使用的資料getter、setter都是不存在的,並且在new Observer()中呼叫defineReactive的引數只有兩個
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]; // 也就是說 這行程式碼一般情況下會執行
}
// 一般情況下 shallow是false ;childOb就是返回的Observer例項,這個例項是儲存在資料的__ob__屬性上的
//
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val; // getter 不存在 ,直接用val
if (Dep.target) {
// Dep.target是一個全域性資料,儲存的是watcher棧(targetStack)棧頂的watcher,
dep.depend(); // 閉包dep把當前watcher收集起來; 收集依賴真正發生在render方法執行的時候(也就是虛擬dom生成的時候)
if (childOb) {
// val不是物件(非Array 或者object) observe方法才會返回一個Observer例項,否則返回undefined
// 此處為什麼要執行childOb.dep.depend()呢?
// 這麼做的效果是:在物件上掛載的__ob__的dep物件把點前watcher新增到了依賴裡,這個dep和閉包dep不是一個。
// 目的在於:
// 1.針對物件:要想this.$set/$del時候能夠觸發元件重新渲染,需要把渲染watcher儲存下來,然後在$set中呼叫 ob.dep.notify();這裡就用到了__ob__屬性
// 2.針對陣列:陣列的攔截中(呼叫splice push 等法法)要想觸發重新渲染,呼叫 ob.dep.notify() 這裡就用到了__ob__屬性
childOb.dep.depend();
if (Array.isArray(value)) {
// 如果value是一個陣列,在observe方法中走的是陣列那套程式,這些元素沒有被Object.defineProperty這一系列的處理(元素當做val處理),即便元素是object/array ,沒有childOb.dep.depend()這樣的一個過程,導致上面this.$set/$del、陣列無法觸發重新渲染;
// 所以呼叫dependArray 針對陣列做處理 這裡就用到了__ob__屬性
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
// 一般沒有getter
const value = getter ? getter.call(obj) : val;
// 值未變化, newVal !== newVal && value !== value 應該針對的是NaN
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
// getter 和setter 要成對才行
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
// 重新設定值之後,需要重新observe ,並且更新閉包變數 childOB
childOb = !shallow && observe(newVal);
// 更新
dep.notify();
},
});
}
如果是陣列:
修改陣列的
__proto__
屬性值,指向一個新的物件;
function protoAugment (target, src: Object) {
target.__proto__ = src
}
這個新物件中重新定義如下方法:
'push','pop','shift','unshift','splice','sort','reverse'
同時這個物件的 __proto__
指向 Array.prototype。
const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
最後專案的程式碼在控制檯列印出下面的截圖
data() {
return {
data1: [{
name: 1
}]
}
},
同時對陣列中的每個元素做 observe 遞迴處理。
watch 選項是如何處理的?
watch 的使用方法一般如下:
watch: {
a: function(newVal, oldVal) {
console.log(newVal, oldVal);
},
b: 'someMethod',
c: {
handler: function(val, oldVal) {
/* ... */
},
deep: true
},
d: {
handler: 'someMethod',
immediate: true
},
e: [
'handle1',
function handle2(val, oldVal) {},
{
handler: function handle3(val, oldVal) {},
}
],
'e.f': function(val, oldVal) {
/* ... */
}
}
watch 的處理按照如下流程, 把其中的關鍵程式碼羅列出來了:
-- > initData() // 初始化元件的時候呼叫 如果元件中有watch選項,呼叫initWatch
-- > initWatch()
if (Array.isArray(handler)) { // 這裡處理陣列的情況,也就是上面e的情況
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
-- > createWatcher()
if (isPlainObject(handler)) { // 相容c(物件)
options = handler
handler = handler.handler
}
// 如果是b 字串的情況,需要在vm上有對應的資料
if (typeof handler === 'string') {
handler = vm[handler]
}
// 預設是 a(函式)
vm.$watch(expOrFn, handler, options)
-- > vm.$watch()
options.user = true // 新增引數 options.user = true ,處理immediate:true的情況
const watcher = new Watcher(vm, expOrFn, cb, options)
-- > new Watcher() // 建立watcher
this.getter = parsePath(expOrFn) // 這個getter方法主要是get一下watch的變數,在get的過程中觸發依賴收集,把當前watcher新增到依賴
this.value = this.lazy // 選項lazy是false
?
undefined :
this.get() // 在constructor中直接呼叫get方法
-- > watcher.get()
pushTarget(this) // 把當前watcher推入棧頂
value = this.getter.call(vm, vm) // 這時候這個watch的變數的依賴裡就有了當前watcher
-- > watcher.getter() // 依賴收集的地方
當 watch 的變數變化的時候,會執行 watcher 的 run 方法:
run() {
if (this.active) {
const value = this.get()
// 渲染watcher情況下 value是undefined
// 在自定義watcher的情況下 value就是監聽的值
if (
value !== this.value || // 當watch的值有變化的時候
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
// 自定義watcher的user是true ,cb就是那個handler
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)
}
}
}
}
上面的的程式碼中 value !== this.value 和 deep 比較好理解,數值變化觸發 handler;
但是 isObject(value)對應的什麼情況呢?看一下下面的例子就知道了:
data() {
return {
data1: [{
name: 1
}],
}
},
computed: {
data2() {
let value = this.data1[0].name //
return this.data1 // 返回的是一個陣列,所以data2一致是不變的
}
},
watch: {
data2: function() {
// 雖然data2的值一直是data1,沒有變化;但是因為data2滿足isObject,所以仍然能觸發handler
// 由此可以想到,可以在computed中主動去獲取某個資料屬性來觸發watch,並且避免在watch中使用deep
// 但是這樣也不太合適,因為可以直接使用'e.f'這種例子來代替;
// 所以根據要實際情況確定
console.log('data2');
}
}
created() {
setInterval(() => {
this.data1[0].name++
}, 2000)
}
computed 資料是如何處理的?
computed 首先是建立 watcher,與渲染 watcher、自定義 watcher 不同之處:初始化的時候不會執行 get 方法,也就是不會做依賴收集。
另外使用 Object.defineProperty 定義 get 方法:
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
// lazy=true 然後 dirty 也是true
if (watcher.dirty) {
watcher.evaluate(); // 把computed watcher新增到涉及到的所有的變數的依賴中;
}
if (Dep.target) {
watcher.depend(); // 主動呼叫depend方法;假如這個computed是用在頁面渲染上,就會把渲染watcher新增到變數的依賴中
}
return watcher.value;
}
};
}
當 computed 資料在初次渲染中:
-- > render // 渲染
-- > computedGetter // computed Object.defineProperty 定義get方法:
-- > watcher.evaluate() // 計算得到watcher.value
-- > watcher.get()
-- > pushTarget(this) // 把當前computed watcher 推入watcher棧頂
-- > watcher.getter() // getter方法就是元件中computed定義的方法,執行的時候會做依賴收集
-- > dep.depend() // 把當前computed watcher加入變數的依賴中
-- > popTarget() // 把當前 computed watcher 移除棧,一般來說渲染watcher會被推出到棧頂
-- > cleanupDeps() // 清除多餘的watcher 和 dep
-- > watcher.depend() // 這是computed比較特殊的地方。假如computed中依賴變數data中的資料,這個步驟把當前watcher新增到變數的依賴中;為什麼要這麼做呢?個人猜測意圖是computed的目的是做一個處理資料的橋樑,真正的響應式還是需要落實到data中的資料。
當 computed 中的依賴資料變化的時候會走如下流程:
-- > watcher.update() // 這是個 computed watcher,其中lazy為true,所以不會往下走
if (this.lazy) {
this.dirty = true
}
-- > watcher.update() // 渲染watcher render 之後的過程就如同初次渲染一樣
渲染 watcher 的流程?
渲染 watcher 相對好理解一些
new Watcher(渲染 watcher) ->watcher.get-> pushTarget(this) ->watcher.getter()-> render -> Object.defineProperty(get) -dep.depend()-> popTarget()->watcher.cleanupDeps()
watcher.getter 是下面方法:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
Vue 中依賴清除?
原始碼在 vue/src/core/observer/watcher.js 中;
需要注意到 vue 中有一套清除 watcher 和 dep 的方案;vue 中的依賴收集並不是一次性的,重新 render 會觸發新一次的依賴收集,這時候會把無效的 watcher 和 dep 去除掉,這樣能夠避免無效的更新。
如下 computed ,只要有一次 temp<=0.5
, 改變 b 都不再會在列印 temp
;原因在於當 temp<0.5
之後, this.b
不會把當前 a
放進自己的 dep 中,也就不會再觸發這個 computed watcher 了
data() {
return {
b: 1
}
},
computed: {
a() {
var temp = Math.random()
console.log(temp); // 只要有一次a<=0.5 接下來就不會列印temp了
if (temp > 0.5) {
return this.b
} else {
return 1
}
}
},
created() {
setTimeout(() => {
this.b++
}, 5000)
},
這裡面主要是 watcher.js 中的 cleanupDeps 方法在處理;
cleanupDeps() {
let i = this.deps.length
// 遍歷上次儲存的deps
while (i--) { // i--
const dep = this.deps[i]
// newDepIds 是在本次依賴收集中加入的新depId集合
// 把不在newDepIds中的dep清除
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
// depIds是一個es6 set集合 ,是引用類資料
// newDepIds類似於一個臨時儲存的地方,最終需要把資料儲存到depIds。左手到右手的把戲
// newDeps 和 newDepIds 是一樣的
let 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
}
vuex 響應式原理?
依賴於 vue 自身的響應式原理,通過構建一個 Vue 例項,在 render 過程中完成依賴的收集。
store._vm = new Vue({
data: {
$$state: state, // 自定義的state資料
},
computed,
});
vue-router 響應式處理?
Vue.mixin({
beforeCreate() {
if (isDef(this.$options.router)) {
this._routerRoot = this;
this._router = this.$options.router;
this._router.init(this);
// 關鍵是這行程式碼,把_route屬性進行響應式處理
Vue.util.defineReactive(
this,
"_route",
this._router.history.current
);
} else {
this._routerRoot =
(this.$parent && this.$parent._routerRoot) || this;
}
registerInstance(this, this);
},
destroyed() {
registerInstance(this);
},
});
Object.defineProperty(Vue.prototype, "$route", {
get() {
return this._routerRoot._route;
},
});
渲染 router-view 的時候會觸發上面的
// 該元件渲染的時候render方法
render() {
...
// 當呼叫$route的時候會觸發依賴收集
var route = parent.$route;
}