您好,登錄后才能下訂單哦!
vue3 源碼正式放出來了,想必大家也都開始爭先恐后的學習 vue3 的知識了。由于 vue3 已經不再支持 v-model 了,而使用 .sync 來代替,但是為了這篇文章可以幫助大家快速了解 vue 的雙向綁定實現原理,部分使用了 vue2.x v-model 的實現原理
proxy 的基礎知識,相信大家已經都很了解了,讓我們一起來回顧一下吧
proxy 是對一個對象的代理,并返回一個已代理的對象,已代理的對象如果發生任何 set 跟 get 的方法都可以被捕獲到,我們寫一個簡單的 :chestnut:
const target = { a: 1 } const handers = { get() { // 當對 observed.a 進行取值時會觸發 }, set() { // 當對 observed.a 進行賦值時會觸發 }, // 還有一些額外的參數如 has 等,這里用不到,就不多說了 .... } const observed = new Proxy(target, handers)
這樣我們就可以對 target 對象設置了一層代理,當我們對 target 進行取賦值操作的時候就可以接可以截獲到它的行為了,但是如果你以為就只有這么簡單你就錯了。
我們把 target 改寫成多層嵌套
const target = { a: { b: 1 } } ... const observed = new Proxy(target, handers)
我們再獲取 observed.a.b = 2 的時候,get 方法取到的是 a 的值 { b: 1 }, 而 set 并不會觸發。這也說明了 proxy 只能代理一層對象,不能深層代理!
那么我們需要監聽到嵌套的對象怎么辦?
其實這個也不難,就是在 get 的時候判斷一下得到的值是不是對象,如果是對象的話就 在對它代理一層,直到最后一層,全部代理完為止,這里就需要一個遞歸函數
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) }
這樣我們就可以對目標函數內部的所有屬性進行深層監聽了,但是這樣還是不夠,因為我們每次取值的時候都會設置代理這樣會導致代碼無限循環->死循環,所以我們需要做一層判斷,如果已經設置了代理的或這已經是代理的對象就不需要在此設置代理了。又因為我們要儲存對象的映射,所以需要使用map函數。下面是reactive完整的代碼。
const rawToReactive: WeakMap<any, any> = new WeakMap(); const reactiveToRaw: WeakMap<any, any> = new WeakMap(); function reactive(data: any) { // 已經有代理 let observed = rawToReactive.get(data); if (observed !== void 0) { return observed; } // 這個數據已經是代理 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; // 通知所有訂閱者觸發更新 trigger(target); // 嚴格模式下需要設置返回值,否則會報錯 return value; } }; // 返回代理監聽對象 observed = new Proxy(data, handler as any); rawToReactive.set(data, observed); reactiveToRaw.set(observed, data); return observed; }
定義watcher 用來作為 compile 跟 reactive 的橋梁, 跟 vue3 的實現可能不一樣
// 收集watcher依賴 const Dep: Dep = { deps: [], add(watcher: Watcher) { this.deps.push(watcher); } }; // observer跟compile的橋梁,在編譯時添加watcher,在數據更新時觸發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]; } }; }
接下來是編譯模板
這里只是模擬編譯,真正的編譯不是這樣的
獲取到模板上的 v-model 、 v-bind 屬性,獲取到綁定的屬性。當數據發生變化時,更新視圖(這里會在trigger進行觸發),當視圖改變數據時修改數據(為了簡單,通過eval函數實現),具體代碼如下
// 編譯模板 function _compile(nodes: any, $data: any) { [...nodes].forEach((e, index) => { const theNode = nodes[index]; // 獲取到 input標簽下的 v-model 屬性,并添加watcher if (theNode.tagName === 'INPUT' && theNode.hasAttribute('v-model')) { const key = theNode.getAttribute('v-model'); Dep.add(_watcher(theNode, 'value', $data, key)); // 監聽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 對依賴進行觸發
function trigger(target: any, key?: string | symbol) { Dep.deps.forEach((e: Watcher) => { e.update(); }); }
使用效果
廢話不多說。直接上代碼!
假設我們有一個模板是這樣的,接下來我們在這個模板的 id="my-app" 元素內實現雙向綁定
<div id="my-app"> <h2 v-bind="a"></h2> <input v-model="a" type="text"> </div>
vue3 中 new Vue 已經被 createApp 所代替,reactive 是反應原理,可以抽出來單獨使用,vue3 外漏了所有內部的 api,都可以在外部使用
const { createApp, reactive } = require('./vue.ts').default; const App = { setup() { const react = reactive({ a: { b: { c: { d: { e: 111 } } } } }); // 測試異步反應 setTimeout(() => { react.a.b.c.d.e = 222; }, 100); return react; } }; createApp().mount(App, '#my-app');
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。