1、 數據響應式
首先請大家認真的思考一個問題:什么是數據響應式?
答:數據變化是可偵測的,并且和數據相關的內容可以更新。
?這里一定要明確一個概念,數據響應式和視圖更新是沒有關系的!數據響應式是一種機制,一種數據變化的偵測機制。而實現數據響應式這種機制的方法不唯一。
那么,在vue
是如何實現數據響應式的?vue2和vue3的數據響應式有什么區別?
2、vue如何實現數據響應式?
要知道,vue3.x
實現數據響應的方案跟vue2.x
是不一樣的,所以在這里我將vue2.x
和vue3.x
分別說說。這也是理解vue2.x
和vue3.x
區別的時候,可以指出來的一個巨大的區別。
2.1 vue2.x的實現方案
我貼上一個vue2.x源碼-Object的變化偵測解讀的鏈接,方便大家理解和后續關于vue2.x的學習需要。
(特別是還沒閱讀過vue源碼的同學,可以獨自過一遍這個文檔,能對vue有一個更深的認識)
在下面vue2
的源碼中可以看到,Observer
類會通過遞歸的方式把一個對象的所有屬性都轉化成可觀測對象,所以我們可以知道vue2
需要遍歷對象的所有的key
。其實現數據響應式的核心思想就是通過defineProperty
,去定義get
、set
等方法。從而能夠攔截到對象屬性的訪問和變更。
/**
* Observer類會通過遞歸的方式把一個對象的所有屬性都轉化成可觀測對象
*/
export class Observer {
constructor (value) {
this.value = value
// 給value新增一個__ob__屬性,值為該value的Observer實例
// 相當于為value打上標記,表示它已經被轉化成響應式了,避免重復操作
def(value,'__ob__',this)
if (Array.isArray(value)) {
// 當value為數組時的邏輯
// ...
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
/**
* 使一個對象轉化成可觀測對象
* @param { Object } obj 對象
* @param { String } key 對象的key
* @param { Any } val 對象的某個key的值
*/
function defineReactive (obj,key,val) {
// 如果只傳了obj和key,那么val = obj[key]
if (arguments.length === 2) {
val = obj[key]
}
if(typeof val === 'object'){
new Observer(val)
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
console.log(`${key}屬性被讀取了`);
return val;
},
set(newVal){
if(val === newVal){
return
}
console.log(`${key}屬性被修改了`);
val = newVal;
}
})
}
在日常開發中,產品經理總是會跟我們說,我們做了xxxx就是為了解決客戶的xxxx痛點。
那么,在繼續往下閱讀的時候,可以先思考一下vue2
這樣的實現方案的痛點有什么?或者說缺點有什么?
因為作為客戶(使用vue
開發的前端同學)的我們需要知道,vue3
是否解決了我們的痛點?
vue2的缺點:(僅僅是關于數據響應造成的缺點哦!)
- 1、影響初始化速度、數據過大時的資源問題
(在源碼的Observer
方法上,對象的每一個屬性都要被攔截。所有的key都要有一次循環和遞歸) - 2、數組的特殊處理,導致其修改數據不能使用索引
(原因在于defineProperty
不支持數組,參考vue源碼-Array的變化偵測) - 3、動態添加或刪除對象屬性無法被偵測
(defineProperty
哭著對我說:臣妾的的setter
函數辦不到呀)
對于沒閱讀過vue源碼的前端開發來說,應該也遇到過修改了數組,或者修改對象后發現,啥變化也沒有,一頭霧水,拍桌子直呼:vue真垃圾,有bug。
其實這些霧水大都是上面的2、3兩點引發的,vue也都提供了解決方案:$set
和$delete
,我都整理好了,需要理解的直接移步深入響應式原理。
但是,這就體驗極差
??小故事一則:去年還沒閱讀源碼的時候,公司一個大版本的發布后,出現了一個不是很嚴重,卻影響使用范圍很廣的一個bug,我們從凌晨2點修到4點,最后還是一個大牛搞了幾輪實驗發現了問題,說vue有bug,某某地方賦值需要用$set
。沒錯,就是上面痛點里的第3點。原因還是我們太菜呀,沒有閱讀相關源碼。
2.2 vue3.x的實現方案
文章開頭我就強調了:數據響應式是一種機制,一種數據變化的偵測機制。而實現數據響應式這種機制的方法不唯一。于是乎,vue3.x
來了,他帶著vue2.x
痛點的解決方案來了!
解決方案其實一點也不神秘,在ES6
之后,出現了一個新的特性:Proxy
。Vue3.x
在使用了Proxy
之后,痛點們一下子就全都解決了。Proxy
是怎么解決的呢?請聽下回...請繼續往下看哈看完手寫reactive
之后,就全都明白啦。
順便給個Proxy
的MDN地址: Proxy MDN傳松門
3、手寫reactive
在vue3.x中,定義響應式對象的方法如下:
const obj = reactive({
name: 'chenjing',
age: 18
})
3.1 測試Proxy是否生效
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
console.log('target, key', target, key, target[key])
return target[key]
})
}
ok,生效。在簡易版的
reactive
,我們要添加基本的屬性get
、set
和deleteProperty
。同時,在上面代碼的get
里直接return target[key]
,一來不太優雅、二來可能報錯。我們先來看看vue3是怎么處理的:再來一個傳送門:Reflect - MDN
Reflect 是一個內置的對象,它提供攔截 JavaScript 操作的方法。這些方法與proxy handlers的方法相同。
Reflect
不是一個函數對象,因此它是不可構造的。
與大多數全局對象不同Reflect
并非一個構造函數,所以不能通過new運算符對其進行調用,或者將Reflect
對象作為一個函數來調用。Reflect
的所有屬性和方法都是靜態的(就像Math
對象)。
Reflect
對象提供了以下靜態方法,這些方法與proxy handler methods的命名相同.
其中的一些方法與Object
相同, 盡管二者之間存在 某些細微上的差別 .
3.2 reactive基本形態
讓我們來學習一下vue3
的寫法后,加上了Reflect
后,于是我們最基本的reactive
就是下面這樣的:
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
const res = Reflect.get(target, key) // 可以直接return target[key],避免報錯和代碼的優雅性,模仿源碼采用Reflect
console.log('get', key)
return (typeof res === 'object') ? reactive(res) : res // 子屬性若是對象 需要再次代理
},
set(target, key, val) {
const res = Reflect.set(target, key, val)
console.log('set', key)
return res
},
deleteProperty() {
const res = Reflect.deleteProperty(target, key)
console.log('deleteProperty', key)
return res
}
})
}
通過跑腳本后的控制臺,可以看到訪問屬性成功的觸發了
get
。同時新增屬性也觸發了set
。到這里為止,
vue2
中的數據響應式在vue3
里其實已經完全實現了。回過頭來想想,是不是沒那么難理解了吧。沒有vue2
的循環遍歷遞歸,只是上了Proxy
的車當然了在
Vue3
內真正的實現,肯定不是這么幾行代碼就搞定的。只是響應式的原理就是利用了Proxy
!
既然要手寫實現一個簡易的reactive
函數,讓我們繼續往下閱讀。
目前只是想簡單理解vue3
數據響應式原理,了解vue3
數據響應和vue2
數據響應的區別的同學可以直接點贊了哈哈,鼓勵一下互相學習進步??
3.3 依賴的收集、觸發
既然要手寫實現一個簡易的reactive
函數,我們就繼續。
要實現reactive
函數,我們就要在get
內進行依賴收集,在set
中進行觸發。即便是vue2
也是通過類似的發布訂閱模式體現。在這里,我們也是通過發布訂閱模式去完成。
首先是依賴收集:在get內,我們需要對依賴進行收集。在依賴收集的時候,將其按照依賴關系放入map中映射。
然后就是依賴觸發:在set中,需要觸發響應式函數。即完成了發布訂閱。
下面代碼 有需要的可以直接復制粘貼,直接跑。可以自行斷點看看,有疑問的歡迎交流。
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
const res = Reflect.get(target, key)
console.log('get', key)
// 依賴收集
track(target, key)
return (typeof res === 'object') ? reactive(res) : res
},
set(target, key, val) {
const res = Reflect.set(target, key, val)
console.log('set', key)
// 觸發
trigger(target, key)
return res
},
deleteProperty() {
const res = Reflect.deleteProperty(target, key)
console.log('deleteProperty', key)
return res
}
})
}
// 保存副作用函數
const effectStack = []
// 添加副作用函數
function effect (fn) {
const e = createReactiveEffect(fn)
// 立即執行
e()
return e
}
function createReactiveEffect(fn) {
// 封裝fn,處理其錯誤,執行之,存放到stack
const effect = () => {
try {
// 0入棧
effectStack.push(effect)
// 1 執行fn
return fn()
} finally {
// 2 出棧
effectStack.pop
}
}
return effect
}
// 保存映射關系的數據結構
const targetMap = new WeakMap()
// 當副作用函數觸發響應式數據之后,執行track,進項依賴收集工作
// 目標是將target, key和前面effectStack中的副作用函數之間建立映射關系
function track (target, key) {
// 1.先拿出響應函數
const effect = effectStack[effectStack.length - 1]
if (effect) {
// 獲取target對應的map
let depMap = targetMap.get(target)
if (!depMap) {
// 初始化的時候 depMap不存在 初始化一次
depMap = new Map()
targetMap.set(target, depMap)
}
// 從depMap中 獲取對應的set
let deps = depMap.get(key)
if (!deps) {
// 初始化需要創建一個Set
deps = new Set()
depMap.set(key, deps)
}
// 將副作用函數放到集合中
deps.add(effect)
}
}
// 觸發響應式函數
function trigger (target, key) {
// 從targetMap中獲取對應副作用函數集合
// 1. 獲取target對應的map
const depMap = targetMap.get(target)
if (!depMap) return
// 根據key獲取對應的deps
const deps = depMap.get(key)
if (deps) {
// 遍歷執行他們
deps.forEach(dep => dep())
}
}
const obj = reactive({
name: 'chenjing',
age: 18,
look: {
height: '180cm'
}
})
effect(() => {
console.log('effect1', obj.name)
})
effect(() => {
console.log('effect2', obj.name, obj.look.height)
})
setTimeout(() => {
console.log('---- 分割線 -----')
obj.name = 'jay'
obj.look.height = '178cm'
}, 1000)
4. 結尾
好了,到此手寫簡易版vue3的reactive函數完成,希望可以幫助到打擊愛理解vue3數據響應原理。
單純的理解數據響應原理可以理解到Proxy就差不多了
后面依賴收集觸發就是具體到響應后要做的事。