前言 之前學習 vue 的時候,一直沒刨根問底過。在看到網上這類文章比較多,參差不齊的質量有時候看的一頭霧水。當然也有不錯的文章,但是終究是別人的理解。於是寫一篇關於自己的理解記錄下來,親身實踐才能收穫更多! 初階:響應式原理 在說明之前,我們先瞭解一個 Object.defineProperty( ...
前言
之前學習 vue 的時候,一直沒刨根問底過。在看到網上這類文章比較多,參差不齊的質量有時候看的一頭霧水。當然也有不錯的文章,但是終究是別人的理解。於是寫一篇關於自己的理解記錄下來,親身實踐才能收穫更多!
初階:響應式原理
在說明之前,我們先瞭解一個 Object.defineProperty()
。引用 MDN 上的權威介紹 developer.mozilla.org/zh-CN/docs/… :
Object.defineProperty()
方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。語法
Object.defineProperty(obj, prop, descriptor) 複製代碼
參數
obj
要定義屬性的對象。
prop
要定義或修改的屬性的名稱或
Symbol
。
descriptor
要定義或修改的屬性描述符。
返回值
被傳遞給函數的對象。
註意:除了本文項目。我還結合多年開發經驗整理出2020最新企業級實戰視頻教程, 包括 Vue3.0/Js/ES6/TS/React/node等,有興趣的進扣扣裙 519293536 免費獲取,小白勿進哦!接下來我們繼續
在瞭解了這個之後,我們就可以用它來實現一個響應式的初級樣子。
const TestObject = {
name:"",
age:10
}
let tempValue = '';
Object.defineProperty(TestObject,'name',{
get:function (){
console.log('我被獲取了,我可以在這裡搞點事情!')
return tempValue;
},
set:function (newValue){
console.log('我被寫入了,我可以在這裡搞點事情!')
tempValue = newValue
}
})
複製代碼
此時使用 TestObject.name
方法可以讓 get 和 set 裡面的對應生效。
我想看到這裡,聰明的同學可能會有一個疑問,為啥要搞一個 tempValue ?我直接 TestObject.name
在 get 和 set 裡面賦值不行麽?就像這樣:
const TestObject = {
name:"",
age:10
}
Object.defineProperty(TestObject,'name',{
get:function (){
console.log('我被獲取了,我可以在這裡搞點事情!')
return TestObject.name;
},
set:function (newValue){
console.log('我被寫入了,我可以在這裡搞點事情!')
TestObject.name = newValue
}
})
複製代碼
真正運行的時候,其實會發現,陷入了死迴圈,這裡大家切忌要避免坑!
我們重讀MDN上的文檔可以發現,get 和 set 本身就是在獲取和設置的時候觸發的函數,在裡面寫了 TestObject.name ,那麼就會繼續調用 set ,然後繼續 TestObject.name ,繼續 set ,繼續.....無限迴圈。所以使用一個臨時變數在外面,是比較安全的做法。
中階:接管對象
在前面基礎上,我們現在可以開始接管整個對象了,邏輯非常簡單,套個迴圈,上代碼:
function ProxyObj(obj){
Object.keys(obj).forEach(key=>{
DefineObj(obj,key,obj[key])
})
}
function DefineObj(obj,key,value){
Object.defineProperty(obj,key,{
get:function (){
console.log('我被獲取了,我可以在這裡搞點事情!')
return value;
},
set:function (newValue){
console.log('我被寫入了,我可以在這裡搞點事情!')
value = newValue
}
})
}
const TestObject = {
name:"",
age:10
}
ProxyObj(TestObject)
複製代碼
這時,聰明的同學又會有疑問了?為什麼要創建兩個 function ,ProxyObj 和 DefineObj 不能堆在一個裡面麽?就像這樣
function ProxyObj(obj){
Object.keys(obj).forEach(key=>{
Object.defineProperty(obj,key,{
get:function (){
console.log('我被獲取了,我可以在這裡搞點事情!')
return obj[key];
},
set:function (newValue){
console.log('我被寫入了,我可以在這裡搞點事情!')
obj[key] = newValue
}
})
})
}
const TestObject = {
name:"",
age:10
}
ProxyObj(TestObject)
複製代碼
不能,其實原因跟之前提到的問題一樣,存在死迴圈的問題。那為什麼我們拆分開來就不存在呢?因為這裡的 DefineObj 傳入的形參。用我們瞭解到的 js 基礎知識來解釋,一個函數內的形參,相當於是函數內預設的變數,一般情況下(也會有二般情況)這個變數的生命周期僅在函數內。所以利用了這個特性,案例巧妙的將形參 value 拿出來傳遞和賦值,就不存在無限死迴圈的問題了。
劃重點:使用 Object.defineProperty
切忌不要陷入到死迴圈當中!
高階:收集依賴
理解需求
終於來到了收集依賴環節,這塊也是我之前一直沒有想通的地方。直到第二天醒來朦朧中看著屏幕前的代碼,突然若有所思了,話不多說,直接來看看需求,對於需求都沒有非常清晰的概念,直接上代碼有點暈。
const TestObject = {
name:"",
age:10
}
watcher(TestObject,'type',()=>{
return TestObject.age>18?'成年人':'未成年人';
})
複製代碼
簡單實現
我們需要實現這個watcher函數,裡面可以填入對象,並且設置關註的對象 type
屬性,此時該屬性還沒有在對象身上,我們需要賦予一下,當 age
變化以後,type
也要對應著改變一下。這就是我們所說的依賴收集需求。
我們這樣簡單實現一下:
function watcher(obj,key,cb){
Object.defineProperty(obj,key,{
get:function (){
const val = cb();
return val;
},
set:function(newValue){
console.log('該屬性是被用於去自動計算的哦~不要人工賦值!')
}
})
}
const TestObject = {
name:"",
age:10
}
watcher(TestObject,'type',()=>{
return TestObject.age>18?'成年人':'未成年人';
})
console.log(TestObject.type)//未成年人
TestObject.age=19;
console.log(TestObject.type)//成年人
複製代碼
可以看到,我們通過這樣的方式,就簡單的實現了 TestObject
屬性上的依賴計算屬性 。
完善需求
但是問題又來了,我如果不去讀取 type 它是不會主動更新的。如何做到 age 變化以後,type 自動更新呢?
在前面的基礎上,我們先定義一個依賴更新時候的函數 updateTodo
此時代碼變這樣:
let target = '';
function watcher(obj,key,cb){
function updateTodo(){
const val = cb();
console.log('更新啦',val)
}
Object.defineProperty(obj,key,{
get:function (){
target = updateTodo;//重點代碼
const val = cb();//重點代碼
target = '';//重點代碼
return val;
},
set:function(newValue){
console.log('該屬性是被用於去自動計算的哦~不要人工賦值!')
}
})
}
const TestObject = {
name:"",
age:10
}
watcher(TestObject,'type',()=>{
return TestObject.age>18?'成年人':'未成年人';//重點代碼
})
console.log(TestObject.type)
TestObject.age = 19;
console.log(TestObject.type)
複製代碼
會發現我們順便在全局還定義一個target,儲存當前 callback,這裡是我覺得最重要的部分,一定要認真看裡面的這段,思考一下,我單獨拿出來:
target = updateTodo;
const val = cb();
target = '';
複製代碼
callback函數長這樣:
()=>{
return TestObject.age>18?'成年人':'未成年人';
}
複製代碼
可能大家會有疑問,target 賦一下有重置是什麼騷操作?有何意義?
註意, callback 裡面有一個 TestObject.age
!這個一旦被訪問,它的 get 函數就被調用了!那麼此刻,前面target裡面儲存的 updateTodo 函數,是不是就可以在 get 裡面取到了呢?
所以在 cb() 執行完之後,實際上裡面就有機會收集依賴了,target 就是這個作用,作為一個臨時的 callback 緩存著,那麼我們的需求也很好解決了,只需要在這裡進行一次依賴的收集和釋放即可!
這裡的疑問解決了以後,就開始直接上代碼吧!後面就比較好理解,跟我們前面的接管對象章節做一個合併,直接展示完整代碼,細細品味,非常有意思:
function ProxyObj(obj){
Object.keys(obj).forEach(key=>{
DefineObj(obj,key,obj[key])
})
}
function DefineObj(obj,key,value){
let deps = [];
Object.defineProperty(obj,key,{
get:function (){
console.log('我被獲取了,我可以在這裡搞點事情!')
//依賴收集
if(target && !deps.includes(target)){//判斷是否存在或重覆依賴
deps.push(target)
}
return value;
},
set:function (newValue){
console.log('我被寫入了,我可以在這裡搞點事情!')
value = newValue;
deps.forEach(fn=>{//依賴釋放
fn()
})
}
})
}
function watcher(obj,key,cb){
function updateTodo(){
const val = cb();
console.log('更新啦',val)
}
Object.defineProperty(obj,key,{
get:function (){
target = updateTodo;
const val = cb();
target='';
return val;
},
set:function(newValue){
console.log('該屬性是被用於去自動計算的哦~不要人工賦值!')
}
})
}
let target = '';
const TestObject = {
name:"",
age:10
}
ProxyObj(TestObject)
watcher(TestObject,'type',()=>{
return TestObject.age>18?'成年人':'未成年人';
})
console.log(TestObject.type)
TestObject.age=19;
console.log(TestObject.type)
複製代碼
當然,如果你對設計模式和一些代碼整潔有要求,看到這裡你可能會覺得非常不爽,沒有關係,嘗試著自己來封裝下吧,或者參看 Vue 內的源碼部分。
數組類型檢測原理
1.關於數組方面,可以參看 vue 源碼部分。
主要邏輯是通過新建一個 Array 對象來代理實現原生 Array 對象的7大操作。當我們在 vue 裡面使用數組方法的時候,實際上使用的是 vue 替我們實現的一些方法,而不是原生的數組操作。
2.這也很好解釋了,為什麼 vue 不推薦大家使用 [] 方式修改數組。因為受限於 js 語言本身,無法實現對 [] 修改的檢測。
3.註意:除了本文項目。我還結合多年開發經驗整理出2020最新企業級實戰視頻教程, 包括 Vue3.0/Js/ES6/TS/React/node等,有興趣的進扣扣裙 519293536 免費獲取,小白勿進哦!
本文的文字及圖片來源於網路加上自己的想法,僅供學習、交流使用,不具有任何商業用途,版權歸原作者所有,如有問題請及時聯繫我們以作處理