Vue?進階系列教程將在本號持續發布,一起查漏補缺學個痛快!若您有遇到其它相關問題,非常歡迎在評論中留言討論,達到幫助更多人的目的。若感本文對您有所幫助請點個贊吧!
2013年7月28日,尤雨溪第一次在 GItHub 上為 Vue.js 提交代碼;2015年10月26日,Vue.js 1.0.0版本發布;2016年10月1日,Vue.js 2.0發布。
最早的 Vue.js 只做視圖層,沒有路由,?沒有狀態管理,也沒有官方的構建工具,只有一個庫,放到網頁里就可以直接用了。
后來,Vue.js 慢慢開始加入了一些官方的輔助工具,比如路由(Router)、狀態管理方案(Vuex)和構建工具(Vue-cli)等。此時,Vue.js 的定位是:The Progressive Framework。翻譯成中文,就是漸進式框架。
Vue.js2.0 引入了很多特性,比如虛擬 DOM,支持 JSX 和 TypeScript,支持流式服務端渲染,提供了跨平臺的能力等。Vue.js 在國內的用戶有阿里巴巴、百度、騰訊、新浪、網易、滴滴出行、360、美團等等。
Vue 已是一名前端工程師必備的技能,現在就讓我們開始深入學習 Vue.js 內部的核心技術原理吧!
響應式原理
在前端開發中,"響應式"通常指的是用戶界面對數據的變化做出相應的能力。換句話說,當數據發生變化時,界面能夠自動更新以反映這些變化。這種機制可以讓開發者專注于數據和業務邏輯,而不必手動管理界面的更新。
在Vue.js中,響應式是框架的核心特性之一。在Vue 3中,響應式的原理主要依賴于ES6中的Proxy對象。
具體來說,Vue 3的響應式原理包括以下幾個步驟:
初始化階段:當你創建一個Vue實例或者定義一個響應式對象時,Vue會對數據進行初始化。在初始化階段,Vue會使用Proxy對象來監聽數據的變化。
Getter和Setter:對象被Proxy包裹后,每個屬性都會有對應的Getter和Setter函數。當你訪問響應式對象的屬性時,會觸發Getter函數,Vue會將這個屬性與當前的組件實例關聯起來,這樣Vue就知道哪些組件依賴于這個屬性。當屬性被修改時,會觸發Setter函數,Vue會通知所有依賴于該屬性的組件進行更新。
依賴追蹤:Vue使用依賴追蹤來跟蹤數據屬性與組件之間的關聯關系。每個組件都有一個依賴收集器,用于存儲與該組件相關的所有數據屬性。當屬性被訪問時,Vue會將當前組件與這個屬性建立關聯,并將屬性的變化依賴于這個組件。
觸發更新:當響應式對象的屬性被修改時,會觸發Setter函數。Setter函數會通知所有依賴于這個屬性的組件進行更新,從而使界面能夠反映數據的變化。
總的來說,Vue 3的響應式原理利用了ES6中的Proxy對象來實現數據的監聽和依賴追蹤,從而實現了高效的數據響應式更新。這種機制讓Vue能夠在數據發生變化時自動更新相關的界面組件,使開發者能夠更加專注于業務邏輯的實現。
實現reactive
開發思想,從單元測試出發,先定義自己想要的最終結果,然后逐步實現相關的API
第一步:這里呢,我們定義第一個單元測試
// reactive.spec.ts (這里用的單元測試為 jest)// 這里引入的是我們即將實現的自己的reactive
import { reactive } from "../reactive";
//?定義單元測試的標題為reactive,此處定義為hello world都可以
describe("reactive",()→{it("first case",()→{//?定義一個原生對象const original = {foo:1};//?此處用reactive包裹后返回一個對象const observed = reactive(original);// 期待observed的值不等于originalexpect(observed).not.toBe(original);//?期待observed.foo 為 1expect(observed.foo).toBe(1);});
});
根據上面測試的內容,我們可以實現這樣一個reactive
// reactive.tsexport function reactive(raw) {// reactive 實際上返回的就是一個proxy對象return new Proxy(raw, {// 攔截getget(target, key) {const?res?=?Reflect.get(target,?key);return res;}
}
此時我們已經實現了一個簡易的reactive,只不過還不支持依賴收集和觸發依賴的邏輯。通過上文我們知道,vue3中依賴收集和觸發依賴是在getter和setter中觸發的,所以我們的代碼可以寫成下面這樣:
// reactive.tsexport function reactive(raw) {return new Proxy(raw, {get(target, key) {const res = Reflect.get(target, key);// TODO 依賴收集track(target, key);return res;},set(target,key,value) {const res = Reflect.set(target, key, value);// TODO 觸發依賴trigger(target, key)return res;}
}
此時我們只需要實現 track和trigger即可。
下面我們看我們的第二個單元測試:
// effect.spec.tsimport?{?reactive?}?from?'../reactive'
// 這里的effect也是我們后面將要實現的
import { effect } from '../effect'//?effect?就是我們的依賴,也叫做副作用
describe("effect",()→{it("second case",()→{const user = reactive({age: 10,});let nextAge;effect(()→{nextAge=user.age + 1;});expect(nextAge).toBe(11);// updateuser.age++;expect(nextAge).toBe(12);});
});
可以看到上面的單元測試中定義了一個函數effect,effect 是一個函數,用于創建副作用。它是 Vue 3 中響應式 API 的一部分,用于處理響應式數據的變化。effect 函數接受一個回調函數作為參數,并在這個回調函數中定義副作用。當回調函數中依賴的響應式數據發生變化時,副作用將被重新執行。
這里先簡單說一下這個依賴收集和觸發依賴是個怎么回事,可以假設這么一個場景:
1. 在火車站都有寄存包裹的地方,每個旅游團就是一個對象,旅游團的每個人就是對象的鍵。
2. 當人員去存儲包裹的時候,寄存處會看當前人員屬于哪個旅游團,相同的旅游團集中放到一個包裹柜,后續方便查找。
3. 然后在這個包裹柜上面找一個箱子給人員,并且給他一把箱子鑰匙(依賴收集)
4. 當相同的人員第二次存儲包裹的時候,他會繼續在原有的箱子里放新的東西(依賴收集)
5. 以此類推
6.?當人員回來拿包裹時,會把鑰匙給寄存處,寄存處會將鑰匙對應的箱子里的所有東西拿出來(觸發依賴)
下面我們來實現effect:
// effect.tsclass ReactiveEffect {private _fn: any;constructor(fn) {this._fn=fn;}run(){activeEffect = thisthis._fn(); }
}// 所有依賴收集到的地方,可以理解成一個寄存處
const targetMap = new Map();
// 收集依賴
export?function?track(target,?key)?{let depsMap = targetMap.get(target);//?先看寄存處里面是否已經由當前對象對應的包裹柜if(!depsMap){depsMap = new Map();targetMap.set(target,depsMap);}let dep = depsMap.get(key)//?再看當前對象對應的鍵值,是否有對應的箱子if(!dep){dep = new Set();depsMap.set(key, dep)}//?最后將用戶傳入的fn作為依賴,添加進入箱子中trackEffects(dep)
}export function trackEffects(dep){dep.add(activeEffect);
}//?實現trigger
export function trigger(target, key) {//?先根據旅游團找到對應的包裹柜let depsMap = targetMap.get(target);//?根據人員找到對應的箱子let dep = depsMap.get(key);//?把箱子里所有的內容拿出來執行triggerEffects(dep)
}export function triggerEffects(dep){for(const effect of dep){effect.run();}
}let activeEffect;
export function effect(fn) {// fnconst _effect = new ReactiveEffect(fn)//?立即執行傳入的函數_effect.run();
}
此時我們的reactive就實現完成了,這里做個總結:
就是每個鍵在getter的時候,也就是effect函數傳入的時候(這里會觸發getter),將整個effect函數作為依賴,放入鍵值對應的箱子里
當數據更新的時候,也就是觸發setter時,將箱子里的內容(fn函數)拿出來執行一遍。此時,相關的響應式數據也就更新了
實現ref
有了上面reactive的基礎,ref會相當簡單的學會。我們還是通過一個單元測試開始:
// ref.spec.tsdescribe("ref",()→{it("first?case",()={const a = ref(1);expect(a.value).toBe(1);});it("second case",()=>{ const a = ref(1);let dummy;let calls = 0;effect(()=>{calls++;dummy = a.value;}};expect(calls).toBe(1);expect(dummy).toBe(1);a.value = 2;expect(calls).toBe(2);expect(dummy).toBe(2);})
})
ref都是通過.value來觸發,我們可以使用一個類,然后攔截他的get和set,這里給出最終代碼:
// ref.tsclass RefImpl {private _value: any;//?存放依賴的箱子public dep;constructor(value) {this._value = value;this.dep = new Set();}get value(){//?收集依賴trackEffects(this.dep)return this._value;}set value(newValue){this.value = newValue// 觸發依賴triggerEffects(this.dep)}
}
export function ref(value) {return new RefImpl(value);
}
Vue?進階系列教程將在本號持續發布,一起查漏補缺學個痛快!若您有遇到其它相關問題,非常歡迎在評論中留言討論,達到幫助更多人的目的。若感本文對您有所幫助請點個贊吧!
葉陽輝
HFun?前端攻城獅
往期精彩:
Vue 進階系列丨Object 的變化偵測
Vue 進階系列丨Array 的變化偵測
Vue 進階系列丨虛擬DOM和VNode
Vue 進階系列丨Patch 和模板編譯
Vue 進階系列丨事件相關的實例方法
Vue 進階系列丨生命周期相關的實例方法
Vue 進階系列丨生命周期
Vue 進階系列丨自定義指令
Vue 進階系列丨最佳實踐
Vue 進階系列丨Mixin 混入
Vue 進階系列丨npm發布vue組件
Vue 進階系列丨Vuex+TS 代碼提示
Vue 進階系列丨自定義指令實現按鈕權限功能
Vue 進階系列丨Pinia 的基本使用
Vue 進階系列丨vue2和vue3定義插件的區別
Vue 進階系列丨vuex持久化
Vue 進階系列丨webWorker 多線程
Vue 進階系列丨大文件切片上傳
Vue 進階系列丨實現簡易VueRouter