vue3實(shí)現(xiàn)v-model原理詳解
vue3 源碼正式放出來(lái)了,想必大家也都開始爭(zhēng)先恐后的學(xué)習(xí) vue3 的知識(shí)了。由于 vue3 已經(jīng)不再支持 v-model 了,而使用 .sync 來(lái)代替,但是為了這篇文章可以幫助大家快速了解 vue 的雙向綁定實(shí)現(xiàn)原理,部分使用了 vue2.x v-model 的實(shí)現(xiàn)原理
proxy 的基礎(chǔ)知識(shí),相信大家已經(jīng)都很了解了,讓我們一起來(lái)回顧一下吧
proxy 是對(duì)一個(gè)對(duì)象的代理,并返回一個(gè)已代理的對(duì)象,已代理的對(duì)象如果發(fā)生任何 set 跟 get 的方法都可以被捕獲到,我們寫一個(gè)簡(jiǎn)單的 :chestnut:
const target = {
a: 1
}
const handers = {
get() {
// 當(dāng)對(duì) observed.a 進(jìn)行取值時(shí)會(huì)觸發(fā)
},
set() {
// 當(dāng)對(duì) observed.a 進(jìn)行賦值時(shí)會(huì)觸發(fā)
},
// 還有一些額外的參數(shù)如 has 等,這里用不到,就不多說(shuō)了
....
}
const observed = new Proxy(target, handers)
這樣我們就可以對(duì) target 對(duì)象設(shè)置了一層代理,當(dāng)我們對(duì) target 進(jìn)行取賦值操作的時(shí)候就可以接可以截獲到它的行為了,但是如果你以為就只有這么簡(jiǎn)單你就錯(cuò)了。
我們把 target 改寫成多層嵌套
const target = {
a: {
b: 1
}
}
...
const observed = new Proxy(target, handers)
我們?cè)佾@取 observed.a.b = 2 的時(shí)候,get 方法取到的是 a 的值 { b: 1 }, 而 set 并不會(huì)觸發(fā)。這也說(shuō)明了 proxy 只能代理一層對(duì)象,不能深層代理!
那么我們需要監(jiān)聽到嵌套的對(duì)象怎么辦?
其實(shí)這個(gè)也不難,就是在 get 的時(shí)候判斷一下得到的值是不是對(duì)象,如果是對(duì)象的話就 在對(duì)它代理一層,直到最后一層,全部代理完為止,這里就需要一個(gè)遞歸函數(shù)
const target = {
a: {
b: 1
}
}
function reactive(data: any) {
const handers = {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
if (isObject(res)) {
data[key] = reactive(res);
}
return target[key];
}
}
const observed = new Proxy(target, handers)
}
這樣我們就可以對(duì)目標(biāo)函數(shù)內(nèi)部的所有屬性進(jìn)行深層監(jiān)聽了,但是這樣還是不夠,因?yàn)槲覀兠看稳≈档臅r(shí)候都會(huì)設(shè)置代理這樣會(huì)導(dǎo)致代碼無(wú)限循環(huán)->死循環(huán),所以我們需要做一層判斷,如果已經(jīng)設(shè)置了代理的或這已經(jīng)是代理的對(duì)象就不需要在此設(shè)置代理了。又因?yàn)槲覀円獌?chǔ)存對(duì)象的映射,所以需要使用map函數(shù)。下面是reactive完整的代碼。
const rawToReactive: WeakMap<any, any> = new WeakMap();
const reactiveToRaw: WeakMap<any, any> = new WeakMap();
function reactive(data: any) {
// 已經(jīng)有代理
let observed = rawToReactive.get(data);
if (observed !== void 0) {
return observed;
}
// 這個(gè)數(shù)據(jù)已經(jīng)是代理
if (reactiveToRaw.has(data)) {
return data;
}
const handler = {
get: function(target: any, key: string, receiver: any) {
const res = Reflect.get(target, key, receiver);
if (isObject(res)) {
data[key] = data[key] = reactive(res);
}
return target[key];
},
set: function(target: any, key: string, value: any) {
// 將新值賦值
target[key] = value;
// 通知所有訂閱者觸發(fā)更新
trigger(target);
// 嚴(yán)格模式下需要設(shè)置返回值,否則會(huì)報(bào)錯(cuò)
return value;
}
};
// 返回代理監(jiān)聽對(duì)象
observed = new Proxy(data, handler as any);
rawToReactive.set(data, observed);
reactiveToRaw.set(observed, data);
return observed;
}
定義watcher 用來(lái)作為 compile 跟 reactive 的橋梁, 跟 vue3 的實(shí)現(xiàn)可能不一樣
// 收集watcher依賴
const Dep: Dep = {
deps: [],
add(watcher: Watcher) {
this.deps.push(watcher);
}
};
// observer跟compile的橋梁,在編譯時(shí)添加watcher,在數(shù)據(jù)更新時(shí)觸發(fā)update更新視圖
function _watcher(node: any, attr: string, data: any, key: string): Watcher {
return {
node,
attr,
data,
key,
update() {
// 逐層取值
const mutationKeys = this.key.split('.');
if (mutationKeys.length > 1) {
let d: any = null;
mutationKeys.forEach(key => (d = this.data[key] || (d && d[key])));
this.node[this.attr] = d;
return;
}
this.node[this.attr] = this.data[this.key];
}
};
}
接下來(lái)是編譯模板
這里只是模擬編譯,真正的編譯不是這樣的
獲取到模板上的 v-model 、 v-bind 屬性,獲取到綁定的屬性。當(dāng)數(shù)據(jù)發(fā)生變化時(shí),更新視圖(這里會(huì)在trigger進(jìn)行觸發(fā)),當(dāng)視圖改變數(shù)據(jù)時(shí)修改數(shù)據(jù)(為了簡(jiǎn)單,通過(guò)eval函數(shù)實(shí)現(xiàn)),具體代碼如下
// 編譯模板
function _compile(nodes: any, $data: any) {
[...nodes].forEach((e, index) => {
const theNode = nodes[index];
// 獲取到 input標(biāo)簽下的 v-model 屬性,并添加watcher
if (theNode.tagName === 'INPUT' && theNode.hasAttribute('v-model')) {
const key = theNode.getAttribute('v-model');
Dep.add(_watcher(theNode, 'value', $data, key));
// 監(jiān)聽input事件
theNode.addEventListener('input', () => {
const mutationKeys = key.split('.');
if (mutationKeys.length > 1) {
eval(`$data.${key}='${theNode.value}'`);
return;
}
$data[key] = theNode.value;
});
}
// 獲取 v-bind 屬性,并添加watcher
if (theNode.hasAttribute('v-bind')) {
const key = theNode.getAttribute('v-bind');
Dep.add(_watcher(theNode, 'innerHTML', $data, key));
}
});
trigger($data);
}
trigger 對(duì)依賴進(jìn)行觸發(fā)
function trigger(target: any, key?: string | symbol) {
Dep.deps.forEach((e: Watcher) => {
e.update();
});
}
使用效果
廢話不多說(shuō)。直接上代碼!
假設(shè)我們有一個(gè)模板是這樣的,接下來(lái)我們?cè)谶@個(gè)模板的 id="my-app" 元素內(nèi)實(shí)現(xiàn)雙向綁定
<div id="my-app"> <h1 v-bind="a"></h1> <input v-model="a" type="text"> </div>
vue3 中 new Vue 已經(jīng)被 createApp 所代替,reactive 是反應(yīng)原理,可以抽出來(lái)單獨(dú)使用,vue3 外漏了所有內(nèi)部的 api,都可以在外部使用
const { createApp, reactive } = require('./vue.ts').default;
const App = {
setup() {
const react = reactive({
a: {
b: {
c: {
d: {
e: 111
}
}
}
}
});
// 測(cè)試異步反應(yīng)
setTimeout(() => {
react.a.b.c.d.e = 222;
}, 100);
return react;
}
};
createApp().mount(App, '#my-app');
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
基于Vue3和element-plus實(shí)現(xiàn)登錄功能(最終完整版)
這篇文章主要介紹了基于Vue3和element-plus實(shí)現(xiàn)一個(gè)完整的登錄功能,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-03-03
vue使用swiper實(shí)現(xiàn)左右滑動(dòng)切換圖片
這篇文章主要為大家詳細(xì)介紹了vue使用swiper實(shí)現(xiàn)左右滑動(dòng)切換圖片,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-10-10
vue踩坑記-在項(xiàng)目中安裝依賴模塊npm install報(bào)錯(cuò)
這篇文章主要介紹了vue踩坑記-在項(xiàng)目中安裝依賴模塊npm install報(bào)錯(cuò),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04
Vue 頁(yè)面切換效果之 BubbleTransition(推薦)
使用 vue,vue-router,animejs 來(lái)講解如何實(shí)現(xiàn)vue頁(yè)面切換效果之 BubbleTransition,需要的朋友參考下吧2018-04-04
解決vue中props對(duì)象中設(shè)置多個(gè)默認(rèn)值的問(wèn)題
props中設(shè)置了默認(rèn)值,但是獲取時(shí)(獲取父頁(yè)面沒(méi)有傳的屬性) 打印出來(lái)是undefined,所以本文給大家介紹了解決vue中props對(duì)象中設(shè)置多個(gè)默認(rèn)值的問(wèn)題,需要的朋友可以參考下2024-04-04

