vue3中v-model的原理示例詳解
模擬 Vue 3 的完整 v-model 功能,我們需要實(shí)現(xiàn)以下特性:**
- 表單元素的雙向綁定:支持 、、 等表單元素,包括修飾符(如 .lazy、.number、.trim)。
- 自定義組件的雙向綁定:支持 v-model 在組件上的使用,基于 modelValue 和 update:modelValue,并支持多重 v-model(如 v-model:name)。
- 自定義修飾符:支持組件上的自定義修飾符(如 v-model.capitalize)。
- 響應(yīng)式系統(tǒng):實(shí)現(xiàn)一個簡化的響應(yīng)式系統(tǒng),模擬 Vue 3 的 ref 或 Proxy 行為,確保數(shù)據(jù)變化觸發(fā)視圖更新。
原生 JavaScript 實(shí)現(xiàn),模擬 Vue 3 的 v-model 功能,包括表單元素和組件支持。我們將逐步構(gòu)建代碼,并提供使用示例。**
實(shí)現(xiàn)步驟
1. 響應(yīng)式系統(tǒng)(模擬 ref)
我們需要一個簡單的響應(yīng)式系統(tǒng),類似于 Vue 3 的 ref,用于創(chuàng)建響應(yīng)式數(shù)據(jù)并在數(shù)據(jù)變化時通知依賴。
javascript
// 定義 Dep 對象,用于依賴收集
let Dep = {
target: null // 當(dāng)前依賴的回調(diào)函數(shù)
};
// 模擬 Vue 3 的 ref
function ref(initialValue) {
const deps = new Set();
let _value = initialValue;
return {
get value() {
// 依賴收集
if (typeof Dep.target === 'function') {
deps.add(Dep.target);
}
return _value;
},
set value(newValue) {
if (_value !== newValue) {
_value = newValue;
// 通知依賴
deps.forEach((dep) => dep());
}
}
};
}
// 依賴收集的全局變量
Dep.target = null;
// 模擬 watchEffect,用于依賴收集
function watchEffect(callback) {
Dep.target = callback;
callback();
Dep.target = null;
}- ref 創(chuàng)建響應(yīng)式數(shù)據(jù),getter 收集依賴,setter 通知依賴更新。
- watchEffect 模擬 Vue 的依賴收集機(jī)制,執(zhí)行回調(diào)并收集依賴。
2. 表單元素的 v-model
表單元素的 v-model 是 :value 和 @input(或 @change)的語法糖。我們需要:
- 綁定響應(yīng)式數(shù)據(jù)到元素的 value(或 checked)。
- 監(jiān)聽輸入事件,更新響應(yīng)式數(shù)據(jù)。
- 支持修飾符(如 .lazy、.number**、.trim)。**
javascript
// 處理修飾符邏輯
function applyModifiers(value, modifiers) {
let result = value;
if (modifiers.number) {
result = isNaN(Number(result)) ? result : Number(result);
}
if (modifiers.trim && typeof result === 'string') {
result = result.trim();
}
return result;
}
// 表單元素的 v-model
function vModelForm(element, reactiveRef, modifiers = {}) {
const tag = element.tagName.toLowerCase();
const type = element.type;
// 確定事件類型
const eventName = modifiers.lazy
? 'change'
: tag === 'select' || type === 'checkbox' || type === 'radio'
? 'change'
: 'input';
// 更新視圖
const updateView = () => {
if (type === 'checkbox') {
element.checked = reactiveRef.value;
} else if (type === 'radio') {
element.checked = reactiveRef.value === element.value;
} else {
element.value = reactiveRef.value;
}
};
// 初始綁定和依賴收集
Dep.target = updateView;
updateView();
Dep.target = null;
// 監(jiān)聽輸入事件
element.addEventListener(eventName, (event) => {
let newValue;
if (type === 'checkbox') {
newValue = event.target.checked;
} else if (type === 'radio') {
if (event.target.checked) {
newValue = event.target.value;
} else {
return; // 未選中時不更新
}
} else {
newValue = event.target.value;
}
// 應(yīng)用修飾符
newValue = applyModifiers(newValue, modifiers);
reactiveRef.value = newValue;
});
// 數(shù)據(jù)變化時更新視圖
reactiveRef.deps = reactiveRef.deps || new Set();
reactiveRef.deps.add(updateView);
}- 修飾符處理:applyModifiers 處理 .number 和 .trim 修飾符。
- 事件選擇:根據(jù)元素類型(checkbox、radio、select)或修飾符(.lazy)選擇 input 或 change 事件。
- 視圖更新:根據(jù)元素類型更新 value 或 checked。
- 事件監(jiān)聽:根據(jù)元素類型獲取用戶輸入的值,并應(yīng)用修飾符后更新響應(yīng)式數(shù)據(jù)。
3. 自定義組件的 v-model
組件的 v-model 基于 modelValue 和 update:modelValue,支持多重 v-model 和自定義修飾符。我們模擬組件為一個簡單的 DOM 結(jié)構(gòu),包含子組件邏輯。
javascript
// 模擬組件的 v-model
function vModelComponent(element, reactiveRef, propName = 'modelValue', modifiers = {}) {
// 模擬組件的 props 和 emit
const props = { [propName]: reactiveRef.value };
const emit = (eventName, value) => {
if (eventName === `update:${propName}`) {
// 應(yīng)用修飾符
let newValue = value;
if (modifiers.capitalize && typeof newValue === 'string') {
newValue = newValue.charAt(0).toUpperCase() + newValue.slice(1);
}
reactiveRef.value = newValue;
}
};
// 更新視圖
const updateView = () => {
element.value = reactiveRef.value; // 模擬組件內(nèi)部 input 的 value
};
// 初始綁定
Dep.target = updateView;
updateView();
Dep.target = null;
// 模擬組件內(nèi)部的 input 事件
element.addEventListener('input', (event) => {
emit(`update:${propName}`, event.target.value);
});
// 數(shù)據(jù)變化時更新視圖
reactiveRef.deps = reactiveRef.deps || new Set();
reactiveRef.deps.add(updateView);
}- props 和 emit:模擬組件的 props(如 modelValue)和 $emit(如 update:modelValue)。
- 多重 v-model:通過 propName 支持自定義屬性(如 name、age)。
- 修飾符:支持自定義修飾符(如 .capitalize),在 emit 時處理。
- 視圖更新:模擬組件內(nèi)部的 ,綁定 value 并監(jiān)聽 input 事件。
4. 統(tǒng)一的 v-model 接口
創(chuàng)建一個統(tǒng)一的 vModel 函數(shù),根據(jù)上下文(表單元素或組件)調(diào)用不同的實(shí)現(xiàn)。
javascript
function vModel(element, reactiveRef, options = {}) {
const { modifiers = {}, propName } = options;
const isComponent = propName || element.tagName.toLowerCase() === 'custom-component';
if (isComponent) {
vModelComponent(element, reactiveRef, propName || 'modelValue', modifiers);
} else {
vModelForm(element, reactiveRef, modifiers);
}
}- 選項(xiàng):modifiers 指定修飾符,propName 指定組件的綁定屬性。
- 上下文判斷:根據(jù) propName 或元素標(biāo)簽判斷是表單元素還是組件。
5. 完整代碼與使用示例
javascript
// 定義 Dep 對象,用于依賴收集
const Dep = {
target: null, // 當(dāng)前依賴的回調(diào)函數(shù)
};
// 模擬 Vue 3 的 ref
function ref(initialValue) {
const deps = new Set();
let _value = initialValue;
return {
get value() {
// 依賴收集
if (typeof Dep.target === 'function') {
deps.add(Dep.target);
}
return _value;
},
set value(newValue) {
if (_value !== newValue) {
_value = newValue;
// 通知依賴
deps.forEach((dep) => dep());
}
},
};
}
// 模擬 watchEffect,用于依賴收集
function watchEffect(callback) {
Dep.target = callback;
callback();
Dep.target = null;
}
// 處理修飾符邏輯
function applyModifiers(value, modifiers) {
let result = value;
if (modifiers.number) {
result = isNaN(Number(result)) ? result : Number(result);
}
if (modifiers.trim && typeof result === 'string') {
result = result.trim();
}
return result;
}
// 表單元素的 v-model
function vModelForm(element, reactiveRef, modifiers = {}) {
const tag = element.tagName.toLowerCase();
const type = element.type;
const eventName = modifiers.lazy
? 'change'
: tag === 'select' || type === 'checkbox' || type === 'radio'
? 'change'
: 'input';
const updateView = () => {
if (type === 'checkbox') {
element.checked = reactiveRef.value;
} else if (type === 'radio') {
element.checked = reactiveRef.value === element.value;
} else {
element.value = reactiveRef.value;
}
};
Dep.target = updateView;
updateView();
Dep.target = null;
element.addEventListener(eventName, (event) => {
let newValue;
if (type === 'checkbox') {
newValue = event.target.checked;
} else if (type === 'radio') {
if (event.target.checked) {
newValue = event.target.value;
} else {
return;
}
} else {
newValue = event.target.value;
}
newValue = applyModifiers(newValue, modifiers);
reactiveRef.value = newValue;
});
reactiveRef.deps = reactiveRef.deps || new Set();
reactiveRef.deps.add(updateView);
}
// 組件的 v-model
function vModelComponent(
element,
reactiveRef,
propName = 'modelValue',
modifiers = {},
) {
const props = { [propName]: reactiveRef.value };
const emit = (eventName, value) => {
if (eventName === `update:${propName}`) {
let newValue = value;
if (modifiers.capitalize && typeof newValue === 'string') {
newValue = newValue.charAt(0).toUpperCase() + newValue.slice(1);
}
reactiveRef.value = newValue;
}
};
const updateView = () => {
element.value = reactiveRef.value;
};
Dep.target = updateView;
updateView();
Dep.target = null;
element.addEventListener('input', (event) => {
emit(`update:${propName}`, event.target.value);
});
reactiveRef.deps = reactiveRef.deps || new Set();
reactiveRef.deps.add(updateView);
}
// 統(tǒng)一 v-model 接口
function vModel(element, reactiveRef, options = {}) {
const { modifiers = {}, propName } = options;
const isComponent =
propName || element.tagName.toLowerCase() === 'custom-component';
if (isComponent) {
vModelComponent(element, reactiveRef, propName || 'modelValue', modifiers);
} else {
vModelForm(element, reactiveRef, modifiers);
}
}
// 使用示例
// 創(chuàng)建 DOM 元素
const inputText = document.createElement('input');
inputText.placeholder = '輸入文本';
const inputNumber = document.createElement('input');
inputNumber.type = 'number';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
const select = document.createElement('select');
select.innerHTML = `
<option value="option1">選項(xiàng) 1</option>
<option value="option2">選項(xiàng) 2</option>
`;
const customComponent = document.createElement('input'); // 模擬組件
customComponent.placeholder = '自定義組件輸入';
// 添加到頁面
document.body.appendChild(inputText);
document.body.appendChild(document.createElement('br'));
document.body.appendChild(inputNumber);
document.body.appendChild(document.createElement('br'));
document.body.appendChild(checkbox);
document.body.appendChild(document.createElement('br'));
document.body.appendChild(select);
document.body.appendChild(document.createElement('br'));
document.body.appendChild(customComponent);
// 創(chuàng)建響應(yīng)式數(shù)據(jù)
const message = ref('Hello');
const number = ref(42);
const checked = ref(false);
const selected = ref('option1');
const componentName = ref('alice');
const componentAge = ref(25);
// 綁定 v-model
vModel(inputText, message, { modifiers: { trim: true } });
vModel(inputNumber, number, { modifiers: { number: true } });
vModel(checkbox, checked);
vModel(select, selected);
vModel(customComponent, componentName, {
propName: 'name',
modifiers: { capitalize: true },
});
// 模擬多重 v-model
const componentAgeInput = document.createElement('input');
componentAgeInput.type = 'number';
document.body.appendChild(document.createElement('br'));
document.body.appendChild(componentAgeInput);
vModel(componentAgeInput, componentAge, {
propName: 'age',
modifiers: { number: true },
});
// 打印數(shù)據(jù)變化
watchEffect(() => {
console.log('message:', message.value);
console.log('number:', number.value);
console.log('checked:', checked.value);
console.log('selected:', selected.value);
console.log('componentName:', componentName.value);
console.log('componentAge:', componentAge.value);
});
// 程序化更新數(shù)據(jù)
setTimeout(() => {
message.value = 'Updated Text';
componentName.value = 'bob';
}, 2000);執(zhí)行流程圖
初始化:
定義 Dep, ref, vModel 等
創(chuàng)建 DOM 元素 -> 添加到頁面
創(chuàng)建 ref 數(shù)據(jù) (message, number, ...)
vModel 綁定:
-> vModelForm 或 vModelComponent
-> 確定事件 (input/change)
-> 定義 updateView
-> 初始更新 DOM (觸發(fā) getter, 收集 updateView)
-> 綁定事件監(jiān)聽
-> 添加 updateView 到 deps
watchEffect:
-> 打印初始值 (觸發(fā) getter, 收集 watchEffect)
設(shè)置定時器
用戶交互 (例如輸入 " Hello Vue "):
input 事件 -> 獲取值 " Hello Vue "
applyModifiers (.trim) -> "Hello Vue"
message.value = "Hello Vue" -> setter
-> 更新 _value
-> 通知 deps:
-> updateView: inputText.value = "Hello Vue"
-> watchEffect: 打印所有數(shù)據(jù)
程序化更新 (2秒后):
message.value = 'Updated Text' -> setter
-> 更新 _value
-> 通知 deps:
-> updateView: inputText.value = 'Updated Text'
-> watchEffect: 打印數(shù)據(jù)
componentName.value = 'bob' -> setter
-> 更新 _value
-> 通知 deps:
-> updateView: customComponent.value = 'bob'
-> watchEffect: 打印數(shù)據(jù)到此這篇關(guān)于vue3中v-model的原理示例的文章就介紹到這了,更多相關(guān)vue v-model原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用vue+elementUI設(shè)置省市縣三級聯(lián)動下拉列表框的詳細(xì)過程
在做電商項(xiàng)目的時候需要添加修改收貨地址,如何實(shí)現(xiàn)三級聯(lián)動選取省市區(qū)地址困擾了我不少時間,這篇文章主要給大家介紹了關(guān)于利用vue+elementUI設(shè)置省市縣三級聯(lián)動下拉列表框的相關(guān)資料,需要的朋友可以參考下2022-08-08
Vue3在router中使用pinia報(bào)錯的簡單解決辦法
這篇文章主要給大家介紹了關(guān)于Vue3在router中使用pinia報(bào)錯的簡單解決辦法,什么是pinia,可以理解為狀態(tài)管理工具,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08
使用Bootstrap4 + Vue2實(shí)現(xiàn)分頁查詢的示例代碼
本篇文章主要介紹了使用Bootstrap4 + Vue2實(shí)現(xiàn)分頁查詢的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-12-12
通過debug搞清楚.vue文件如何變成.js文件(案例詳解)
這篇文章主要介紹了通過debug搞清楚.vue文件如何變成.js文件,本文以@vitejs/plugin-vue舉例,通過debug的方式帶你一步一步的搞清楚vue文件是如何編譯為js文件的,需要的朋友可以參考下2024-07-07
在Vue中實(shí)現(xiàn)Excel導(dǎo)出功能(數(shù)據(jù)導(dǎo)出)
本文分享了如何在前端導(dǎo)出Excel文件,強(qiáng)調(diào)了前端導(dǎo)出的即時性、便捷性、靈活性和定制化優(yōu)勢,以及減輕后端服務(wù)器負(fù)擔(dān)的特點(diǎn),詳細(xì)介紹了ExcelJS和FileSaver.js兩個工具庫的使用方法和主要功能,最后通過Vue實(shí)現(xiàn)了Excel的導(dǎo)出功能2024-10-10
Vue自定義v-has指令,做按鈕權(quán)限判斷的步驟
這篇文章主要介紹了Vue自定義v-has指令,做按鈕權(quán)限判斷的步驟,幫助大家更好的理解和學(xué)習(xí)使用vue框架,感興趣的朋友可以了解下2021-04-04
一文搞懂vue中provide和inject實(shí)現(xiàn)原理對抗平庸
這篇文章主要為大家介紹了vue中provide和inject實(shí)現(xiàn)原理的深入理解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04

