天天看點

vue3 實作 v-model 原理

vue3 源碼正式放出來了,想必大家也都開始争先恐後的學習 vue3 的知識了。由于 vue3 已經不再支援 v-model 了,而使用 .sync 來代替,但是為了這篇文章可以幫助大家快速了解 vue 的雙向綁定實作原理,部分使用了 vue2.x v-model 的實作原理

proxy 的基礎知識,相信大家已經都很了解了,讓我們一起來回顧一下吧

proxy 是對一個對象的代理,并傳回一個已代理的對象,已代理的對象如果發生任何 set 跟 get 的方法都可以被捕獲到,我們寫一個簡單的 ?
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

定義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">  <h1 v-bind="a"></h1>  <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');           

複制