使用Vue逐步實現(xiàn)Watch屬性詳解
watch
對于watch
的用法,在Vue
文檔 中有詳細(xì)描述,它可以讓我們觀察data
中屬性的變化。并提供了一個回調(diào)函數(shù),可以讓用戶在屬性值變化后做一些事情。
watch
對象中的value
分別支持函數(shù)、數(shù)組、字符串、對象,較為常用的是函數(shù)的方式,當(dāng)想要觀察一個對象以及對象中的每一個屬性的變化時,便會用到對象的方式。
下面是官方的一個例子,相信在看完之后就能對watch
的幾種用法有大概的了解:
var vm = new Vue({ data: { a: 1, b: 2, c: 3, d: 4, e: { f: { g: 5 } } }, watch: { a: function (val, oldVal) { console.log('new: %s, old: %s', val, oldVal) }, // string method name b: 'someMethod', // the callback will be called whenever any of the watched object properties change regardless of their nested depth c: { handler: function (val, oldVal) { /* ... */ }, deep: true }, // the callback will be called immediately after the start of the observation d: { handler: 'someMethod', immediate: true }, // you can pass array of callbacks, they will be called one-by-one e: [ 'handle1', function handle2 (val, oldVal) { /* ... */ }, { handler: function handle3 (val, oldVal) { /* ... */ }, /* ... */ } ], // watch vm.e.f's value: {g: 5} 'e.f': function (val, oldVal) { /* ... */ } } }) vm.a = 2 // => new: 2, old: 1
初始化watch
在了解了watch
的用法之后,我們開始實現(xiàn)watch
。
在初始化狀態(tài)initState
時,會判斷用戶在實例化Vue
時是否傳入了watch
選項,如果用戶傳入了watch
,就會進行watch
的初始化操作:
// src/state.js function initState (vm) { const options = vm.$options; if (options.watch) { initWatch(vm); } }
initWatch
中本質(zhì)上是為每一個watch
中的屬性對應(yīng)的回調(diào)函數(shù)都創(chuàng)建了一個watcher
:
// src/state.js function initWatch (vm) { const { watch } = vm.$options; for (const key in watch) { if (watch.hasOwnProperty(key)) { const userDefine = watch[key]; if (Array.isArray(userDefine)) { // userDefine是數(shù)組,為數(shù)組中的每一項分別創(chuàng)建一個watcher userDefine.forEach(item => { createWatcher(vm, key, item); }); } else { createWatcher(vm, key, userDefine); } } } }
createWatcher
中得到的userDefine
可能是函數(shù)、對象或者字符串,需要分別進行處理:
function createWatcher (vm, key, userDefine) { let handler; if (typeof userDefine === 'string') { // 字符串,從實例上取到對應(yīng)的method handler = vm[userDefine]; userDefine = {}; } else if (typeof userDefine === 'function') { // 函數(shù) handler = userDefine; userDefine = {}; } else { // 對象,userDefine中可能會包含用戶傳入的deep,immediate屬性 handler = userDefine.handler; delete userDefine.handler; } // 用處理好的參數(shù)調(diào)用vm.$watch vm.$watch(key, handler, userDefine); }
createWatcher
中對參數(shù)進行統(tǒng)一處理,之后調(diào)用了vm.$watch
,在vm.$watch
中執(zhí)行了Watcher
的實例化操作:
export function stateMixin (Vue) { // some code ... Vue.prototype.$watch = function (exprOrFn, cb, options) { const vm = this; const watch = new Watcher(vm, exprOrFn, cb, { ...options, user: true }); }; }
此時new Watcher
時傳入的參數(shù)如下:
vm
: 組件實例exprOrFn
:watch
選項對應(yīng)的key
cb
:watch
選項中key
對應(yīng)的value
中提供給用戶處理邏輯的回調(diào)函數(shù),接收key
在data
中的對應(yīng)屬性的舊值和新值作為參數(shù)options
:{user: true, immediate: true, deep: true}
,immediate
和deep
屬性當(dāng)key
對應(yīng)的value
為對象時,用戶可能會傳入
在Watcher
中會判斷options
中有沒有user
屬性來區(qū)分是否是watch
屬性對應(yīng)的watcher
:
class Watcher { constructor (vm, exprOrFn, cb, options = {}) { this.user = options.user; if (typeof exprOrFn === 'function') { this.getter = this.exprOrFn; } if (typeof exprOrFn === 'string') { // 如果exprFn傳入的是字符串,會從實例vm上進行取值 this.getter = function () { const keys = exprOrFn.split('.'); // 后一次拿到前一次的返回值,然后繼續(xù)進行操作 // 在取值時,會收集當(dāng)前Dep.target對應(yīng)的`watcher`,這里對應(yīng)的是`watch`屬性對應(yīng)的`watcher` return keys.reduce((memo, cur) => memo[cur], vm); }; } this.value = this.get(); } get () { pushTarget(this); const value = this.getter(); popTarget(); return value; } // some code ... }
這里有倆個重要的邏輯:
- 由于傳入的
exprOrFn
是字符串,所以this.getter
的邏輯就是從vm
實例上找到exprOrFn
對應(yīng)的值并返回 - 在
watcher
實例化時,會執(zhí)行this.get
,此時會通過this.getter
方法進行取值。取值就會觸發(fā)對應(yīng)屬性的get
方法,收集當(dāng)前的watcher
作為依賴 - 將
this.get
的返回值賦值給this.value
,此時拿到的就是舊值
當(dāng)觀察的屬性值發(fā)生變化后,會執(zhí)行其對應(yīng)的set
方法,進而執(zhí)行收集的watch
對應(yīng)的watcher
的update
方法:
class Watcher { // some code ... update () { queueWatcher(this); } run () { const value = this.get(); if (this.user) { this.cb.call(this.vm, value, this.value); this.value = value; } } }
和渲染watcher
相同,update
方法中會將對應(yīng)的watch watcher
去重后放到異步隊列中執(zhí)行,所以當(dāng)用戶多次修改watch
屬性觀察的值時,并不會不停的觸發(fā)對應(yīng)watcher
的更新操作,而只是以它最后一次更新的值作為最終值來執(zhí)行this.get
進行取值操作。
當(dāng)我們拿到觀察屬性的最新值之后,執(zhí)行watcher
中傳入的回調(diào)函數(shù),傳入新值和舊值。
下面畫圖來梳理下這個過程:
deep、immdediate屬性
當(dāng)用戶傳入immediate
屬性后,會在watch
初始化時便立即執(zhí)行對應(yīng)的回調(diào)函數(shù)。其具體的執(zhí)行位置是在Watcher
實例化之后:
Vue.prototype.$watch = function (exprOrFn, cb, options) { const vm = this; const watcher = new Watcher(vm, exprOrFn, cb, { ...options, user: true }); if (options.immediate) { // 在初始化后立即執(zhí)行watch cb.call(vm, watcher.value); } };
此時watcher.value
是被觀察的屬性當(dāng)前的值,由于此時屬性還沒有更新,所以老值為undefined
。
如果watch
觀察的屬性為對象,那么默認(rèn)對象內(nèi)的屬性更新,并不會觸發(fā)對應(yīng)的回調(diào)函數(shù)。此時,用戶可以傳入deep
選項,來讓對象內(nèi)部屬性更新也調(diào)用對應(yīng)的回調(diào)函數(shù):
class Watcher { // some code ... get () { pushTarget(this); const value = this.getter(); if (this.deep) { // 繼續(xù)遍歷value中的每一項,觸發(fā)它的get方法,收集當(dāng)前的watcher traverse(value); } popTarget(); return value; } }
當(dāng)用戶傳入deep
屬性后,get
方法中會執(zhí)行traverse
方法來遍歷value
中的每一個值,這樣便可以繼續(xù)觸發(fā)value
中屬性對應(yīng)的get
方法,為其收集當(dāng)前的watcher
作為依賴。這樣在value
內(nèi)部屬性更新時,也會通知其收集的watch watcher
進行更新操作。
traverse
的邏輯只是遞歸遍歷傳入數(shù)據(jù)的每一個屬性,當(dāng)遇到簡單數(shù)據(jù)類型時便停止遞歸:
// traverse.js // 創(chuàng)建一個Set,遍歷之后就會將其放入,當(dāng)遇到環(huán)引用的時候不會行成死循環(huán) const seenObjects = new Set(); export function traverse (value) { _traverse(value, seenObjects); // 遍歷完成后,清空Set seenObjects.clear(); } function _traverse (value, seen) { const isArr = Array.isArray(value); const ob = value.__ob__; // 不是對象并且沒有被觀測過的話,終止調(diào)用 if (!isObject(value) || !ob) { return; } if (ob) { // 每個屬性只會有一個在Observer中定義的dep const id = ob.dep.id; if (seen.has(id)) { // 遍歷過的對象和數(shù)組不再遍歷,防止環(huán)結(jié)構(gòu)造成死循環(huán) return; } seen.add(id); } if (isArr) { value.forEach(item => { // 繼續(xù)遍歷數(shù)組中的每一項,如果為對象的話,會繼續(xù)遍歷數(shù)組的每一個屬性,即對對象屬性執(zhí)行取值操作,收集watch watcher _traverse(item, seen); }); } else { const keys = Object.keys(value); for (let i = 0; i < keys.length; i++) { // 繼續(xù)執(zhí)行_traverse,這里會對 對象 中的屬性進行取值 _traverse(value[keys[i]], seen); } } }
需要注意的是,這里利用Set
來存儲每個屬性對應(yīng)的dep
的id
。這樣當(dāng)出現(xiàn)環(huán)時,Set
中已經(jīng)存儲過了其對應(yīng)dep
的id
,便會終止遞歸。
結(jié)語
本文一步步實現(xiàn)了Vue
的watch
屬性,并對內(nèi)部的實現(xiàn)邏輯提供了筆者相應(yīng)的理解 。到此這篇關(guān)于使用Vue逐步實現(xiàn)Watch屬性詳解的文章就介紹到這了,更多相關(guān)Vue Watch屬性內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue?實現(xiàn)動態(tài)設(shè)置元素的高度
這篇文章主要介紹了在vue中實現(xiàn)動態(tài)設(shè)置元素的高度,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08Vue項目如何在js文件里獲取路由參數(shù)及路由跳轉(zhuǎn)
日常業(yè)務(wù)中路由跳轉(zhuǎn)的同時傳遞參數(shù)是比較常見的,下面這篇文章主要給大家介紹了關(guān)于Vue項目如何在js文件里獲取路由參數(shù)及路由跳轉(zhuǎn)的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01vue2+element-ui使用vue-i18n進行國際化的多語言/國際化詳細(xì)教程
這篇文章主要給大家介紹了關(guān)于vue2+element-ui使用vue-i18n進行國際化的多語言/國際化的相關(guān)資料,I18n是Vue.js的國際化插件,項目里面的中英文等多語言切換會使用到這個東西,需要的朋友可以參考下2023-12-12VUE?html5-qrcode實現(xiàn)H5掃一掃功能實例
這篇文章主要給大家介紹了關(guān)于VUE?html5-qrcode實現(xiàn)H5掃一掃功能的相關(guān)資料,html5-qrcode是輕量級和跨平臺的QR碼和條形碼掃碼的JS庫,集成二維碼、條形碼和其他一些類型的代碼掃描功能,需要的朋友可以參考下2023-08-08