手寫Vue源碼之?dāng)?shù)據(jù)劫持示例詳解
源代碼: 傳送門
Vue會(huì)對(duì)我們?cè)赿ata中傳入的數(shù)據(jù)進(jìn)行攔截:
- 對(duì)象:遞歸的為對(duì)象的每個(gè)屬性都設(shè)置get/set方法
- 數(shù)組:修改數(shù)組的原型方法,對(duì)于會(huì)修改原數(shù)組的方法進(jìn)行了重寫
在用戶為data中的對(duì)象設(shè)置值、修改值以及調(diào)用修改原數(shù)組的方法時(shí),都可以添加一些邏輯來進(jìn)行處理,實(shí)現(xiàn)數(shù)據(jù)更新頁(yè)面也同時(shí)更新。
Vue中的響應(yīng)式(reactive): 對(duì)對(duì)象屬性或數(shù)組方法進(jìn)行了攔截,在屬性或數(shù)組更新時(shí)可以同時(shí)自動(dòng)地更新視圖。在代碼中被觀測(cè)過的數(shù)據(jù)具有響應(yīng)性
創(chuàng)建Vue實(shí)例
我們先讓代碼實(shí)現(xiàn)下面的功能:
<body> <script> const vm = new Vue({ el: '#app', data () { return { age: 18 }; } }); // 會(huì)觸發(fā)age屬性對(duì)應(yīng)的set方法 vm.age = 20; // 會(huì)觸發(fā)age屬性對(duì)應(yīng)的get方法 console.log(vm.age); </script> </body>
在src/index.js中,定義Vue的構(gòu)造函數(shù)。用戶用到的Vue就是在這里導(dǎo)出的Vue:
import initMixin from './init'; function Vue (options) { this._init(options); } // 進(jìn)行原型方法擴(kuò)展 initMixin(Vue); export default Vue;
在init中,會(huì)定義原型上的_init方法,并進(jìn)行狀態(tài)的初始化:
import initState from './state'; function initMixin (Vue) { Vue.prototype._init = function (options) { const vm = this; // 將用戶傳入的選項(xiàng)放到vm.$options上,之后可以很方便的通過實(shí)例vm來訪問所有實(shí)例化時(shí)傳入的選項(xiàng) vm.$options = options; initState(vm); }; } export default initMixin;
在_init方法中,所有的options被放到了vm.$options中,這不僅讓之后代碼中可以更方便的來獲取用戶傳入的配置項(xiàng),也可以讓用戶通過這個(gè)api來獲取實(shí)例化時(shí)傳入的一些自定義選選項(xiàng)。比如在Vuex 和Vue-Router中,實(shí)例化時(shí)傳入的router和store屬性便可以通過$options獲取到。
除了設(shè)置vm.$options,_init中還執(zhí)行了initState方法。該方法中會(huì)判斷選項(xiàng)中傳入的屬性,來分別進(jìn)行props、methods、data、watch、computed 等配置項(xiàng)的初始化操作,這里我們主要處理data選項(xiàng):
import { observe } from './observer'; import { proxy } from './shared/utils'; function initState (vm) { const options = vm.$options; if (options.props) { initProps(vm); } if (options.methods) { initMethods(vm); } if (options.data) { initData(vm); } if (options.computed) { initComputed(vm) } if (options.watch) { initWatch(vm) } } function initData (vm) { let data = vm.$options.data; vm._data = data = typeof data === 'function' ? data.call(vm) : data; // 對(duì)data中的數(shù)據(jù)進(jìn)行攔截 observe(data); // 將data中的屬性代理到vm上 for (const key in data) { if (data.hasOwnProperty(key)) { // 為vm代理所有data中的屬性,可以直接通過vm.xxx來進(jìn)行獲取 proxy(vm, key, data); } } } export default initState;
在initData中進(jìn)行了如下操作:
- data可能是對(duì)象或函數(shù),這里將data統(tǒng)一處理為對(duì)象
- 觀測(cè)data中的數(shù)據(jù),為所有對(duì)象屬性添加set/get方法,重寫數(shù)組的原型鏈方法
- 將data中的屬性代理到vm上,方便用戶直接通過實(shí)例vm來訪問對(duì)應(yīng)的值,而不是通過vm._data來訪問
新建src/observer/index.js,在這里書寫observe函數(shù)的邏輯:
function observe (data) { // 如果是對(duì)象,會(huì)遍歷對(duì)象中的每一個(gè)元素 if (typeof data === 'object' && data !== null) { // 已經(jīng)觀測(cè)過的值不再處理 if (data.__ob__) { return; } new Observer(data); } } export { observe };
observe函數(shù)中會(huì)過濾data中的數(shù)據(jù),只對(duì)對(duì)象和數(shù)組進(jìn)行處理,真正的處理邏輯在Observer中:
/** * 為data中的所有對(duì)象設(shè)置`set/get`方法 */ class Observer { constructor (value) { this.value = value; // 為data中的每一個(gè)對(duì)象和數(shù)組都添加__ob__屬性,方便直接可以通過data中的屬性來直接調(diào)用Observer實(shí)例上的屬性和方法 defineProperty(this.value, '__ob__', this); // 這里會(huì)對(duì)數(shù)組和對(duì)象進(jìn)行單獨(dú)處理,因?yàn)闉閿?shù)組中的每一個(gè)索引都設(shè)置get/set方法性能消耗比較大 if (Array.isArray(value)) { Object.setPrototypeOf(value, arrayProtoCopy); this.observeArray(value); } else { this.walk(); } } walk () { for (const key in this.value) { if (this.value.hasOwnProperty(key)) { defineReactive(this.value, key); } } } observeArray (value) { for (let i = 0; i < value.length; i++) { observe(value[i]); } } }
需要注意的是,__ob__屬性要設(shè)置為不可枚舉,否則之后在對(duì)象遍歷時(shí)可能會(huì)引發(fā)死循環(huán)
Observer類中會(huì)為對(duì)象和數(shù)組都添加__ob__屬性,之后便可以直接通過data中的對(duì)象和數(shù)組vm.value.__ob__來獲取到Observer實(shí)例。
當(dāng)傳入的value為數(shù)組時(shí),由于觀測(cè)數(shù)組的每一個(gè)索引會(huì)耗費(fèi)比較大的性能,并且在實(shí)際使用中,我們可能只會(huì)操作數(shù)組的第一項(xiàng)和最后一項(xiàng),即arr[0],arr[arr.length-1],很少會(huì)寫出arr[23] = xxx的代碼。
所以我們選擇對(duì)數(shù)組的方法進(jìn)行重寫,將數(shù)組的原型指向繼承Array.prototype新創(chuàng)建的對(duì)象arrayProtoCopy,對(duì)數(shù)組中的每一項(xiàng)繼續(xù)進(jìn)行觀測(cè)。
創(chuàng)建data中數(shù)組原型的邏輯在src/observer/array.js中:
// if (Array.isArray(value)) { // Object.setPrototypeOf(value, arrayProtoCopy); // this.observeArray(); // } const arrayProto = Array.prototype; export const arrayProtoCopy = Object.create(arrayProto); const methods = ['push', 'pop', 'unshift', 'shift', 'splice', 'reverse', 'sort']; methods.forEach(method => { arrayProtoCopy[method] = function (...args) { const result = arrayProto[method].apply(this, args); console.log('change array value'); // data中的數(shù)組會(huì)調(diào)用這里定義的方法,this指向該數(shù)組 const ob = this.__ob__; let inserted; switch (method) { case 'push': case 'unshift': inserted = args; break; case 'splice': // splice(index,deleteCount,item1,item2) inserted = args.slice(2); break; } if (inserted) {ob.observeArray(inserted);} return result; }; });
通過Object.create方法,可以創(chuàng)建一個(gè)原型為Array.prototype的新對(duì)象arrayProtoCopy。修改原數(shù)組的7個(gè)方法會(huì)設(shè)置為新對(duì)象的私有屬性,并且在執(zhí)行時(shí)會(huì)調(diào)用arrayProto 上對(duì)應(yīng)的方法。
在這樣處理之后,便可以在arrayProto中的方法執(zhí)行前后添加自己的邏輯,而除了這7個(gè)方法外的其它方法,會(huì)根據(jù)原型鏈,使用arrayProto上的對(duì)應(yīng)方法,并不會(huì)有任何額外的處理。
在修改原數(shù)組的方法中,添加了如下的額外邏輯:
const ob = this.__ob__; let inserted; switch (method) { case 'push': case 'unshift': inserted = args; break; case 'splice': // splice(index,deleteCount,item1,item2) inserted = args.slice(2); break; } if (inserted) {ob.observeArray(inserted);}
push、unshift、splice會(huì)為數(shù)組新增元素,對(duì)于新增的元素,也要對(duì)其進(jìn)行觀測(cè)。這里利用到了Observer中為數(shù)組添加的__ob__屬性,來直接調(diào)用ob.observeArray ,對(duì)數(shù)組中新增的元素繼續(xù)進(jìn)行觀測(cè)。
對(duì)于對(duì)象,要遍歷對(duì)象的每一個(gè)屬性,來為其添加set/get方法。如果對(duì)象的屬性依舊是對(duì)象,會(huì)對(duì)其進(jìn)行遞歸處理
function defineReactive (target, key) { let value = target[key]; // 繼續(xù)對(duì)value進(jìn)行監(jiān)聽,如果value還是對(duì)象的話,會(huì)繼續(xù)new Observer,執(zhí)行defineProperty來為其設(shè)置get/set方法 // 否則會(huì)在observe方法中什么都不做 observe(value); Object.defineProperty(target, key, { get () { console.log('get value'); return value; }, set (newValue) { if (newValue !== value) { // 新加的元素也可能是對(duì)象,繼續(xù)為新加對(duì)象的屬性設(shè)置get/set方法 observe(newValue); // 這樣寫會(huì)新將value指向一個(gè)新的值,而不會(huì)影響target[key] console.log('set value'); value = newValue; } } }); } class Observer { constructor (value) { // some code ... if (Array.isArray(value)) { // some code ... } else { this.walk(); } } walk () { for (const key in this.value) { if (this.value.hasOwnProperty(key)) { defineReactive(this.value, key); } } } // some code ... }
數(shù)據(jù)觀測(cè)存在的問題
我們先創(chuàng)建一個(gè)簡(jiǎn)單的例子:
const mv = new Vue({ data () { return { arr: [1, 2, 3], person: { name: 'zs', age: 20 } } } })
對(duì)于對(duì)象,我們只是攔截了它的取值和賦值操作,添加值和刪除值并不會(huì)進(jìn)行攔截:
vm.person.school = '北大' delete vm.person.age
而對(duì)于數(shù)組,用索引修改值以及修改數(shù)組長(zhǎng)度不會(huì)被觀測(cè)到:
vm.arr[0] = 0 vm.arr.length--
為了能處理上述的情況,Vue為用戶提供了$set和$delete方法:
- $set: 為響應(yīng)式對(duì)象添加一個(gè)屬性,確保新屬性也是響應(yīng)式的,因此會(huì)觸發(fā)視圖更新
- $delete: 刪除對(duì)象上的一個(gè)屬性。如果對(duì)象是響應(yīng)式的,確保刪除觸發(fā)視圖更新。
結(jié)語(yǔ)
通過實(shí)現(xiàn)Vue的數(shù)據(jù)劫持,將會(huì)對(duì)Vue的數(shù)據(jù)初始化和響應(yīng)式有更深的認(rèn)識(shí)。
在工作中,我們可能總是會(huì)疑惑,為什么我更新了值,但是頁(yè)面沒有發(fā)生變化?現(xiàn)在我們可以從源碼的角度進(jìn)行理解,從而更清楚的知道代碼中存在的問題以及如何解決和避免這些問題。
代碼的目錄結(jié)構(gòu)是參考了源碼的,所以看完文章的小伙伴,也可以從源碼中找出對(duì)應(yīng)的代碼進(jìn)行閱讀,相信你會(huì)有不一樣的理解!
到此這篇關(guān)于手寫Vue源碼之?dāng)?shù)據(jù)劫持的文章就介紹到這了,更多相關(guān)Vue源碼之?dāng)?shù)據(jù)劫持內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue中使用 pinia 全局狀態(tài)管理的實(shí)現(xiàn)
本文主要介紹了vue中使用 pinia 全局狀態(tài)管理的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07vue3中使用VueParticles實(shí)現(xiàn)粒子動(dòng)態(tài)背景效果
為了提高頁(yè)面展示效果,特別類似于登錄界面內(nèi)容比較單一的,粒子效果作為背景經(jīng)常使用到,vue工程中利用vue-particles可以很簡(jiǎn)單的實(shí)現(xiàn)頁(yè)面的粒子背景效果,本文給大家分享vue粒子動(dòng)態(tài)背景效果實(shí)現(xiàn)代碼,需要的朋友參考下吧2022-05-05vue如何實(shí)現(xiàn)二進(jìn)制流文件導(dǎo)出excel
這篇文章主要介紹了vue如何實(shí)現(xiàn)二進(jìn)制流文件導(dǎo)出excel,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-06-06解決cordova+vue 項(xiàng)目打包成APK應(yīng)用遇到的問題
這篇文章主要介紹了解決cordova+vue 項(xiàng)目打包成APK應(yīng)用遇到的問題,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-05-05vue2 el-checkbox-group復(fù)選框無法選中問題及解決
這篇文章主要介紹了vue2 el-checkbox-group復(fù)選框無法選中問題及解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05使用 webpack 插件自動(dòng)生成 vue 路由文件的方法
這篇文章主要介紹了使用 webpack 插件自動(dòng)生成 vue 路由文件的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-08-08