vue3 實現 v-model 原理

vue3 源碼正式放出來了,想必你們也都開始爭先恐後的學習 vue3 的知識了。因爲 vue3 已經再也不支持 v-model 了,而使用 .sync 來代替,可是爲了這篇文章能夠幫助你們快速瞭解 vue 的雙向綁定實現原理,部分使用了 vue2.x v-model 的實現原理vue

proxy 的基礎知識,相信你們已經都很瞭解了,讓咱們一塊兒來回顧一下吧node

proxy 是對一個對象的代理,並返回一個已代理的對象,已代理的對象若是發生任何 set 跟 get 的方法均可以被捕獲到,咱們寫一個簡單的 🌰
const target = {
  a: 1
}
const handers = {
  get() {
    // 當對 observed.a 進行取值時會觸發
  },
  set() {
    // 當對 observed.a 進行賦值時會觸發
  },
  // 還有一些額外的參數如 has 等,這裏用不到,就很少說了
  ....
}
const observed = new Proxy(target, handers)
複製代碼

這樣咱們就能夠對 target 對象設置了一層代理,當咱們對 target 進行取賦值操做的時候就能夠接能夠截獲到它的行爲了,可是若是你覺得就只有這麼簡單你就錯了。react

咱們把 target 改寫成多層嵌套api

const target = {
  a: {
    b: 1
  }
}

...

const observed = new Proxy(target, handers)
複製代碼
咱們再獲取 observed.a.b = 2 的時候,get 方法取到的是 a 的值 { b: 1 }, 而 set 並不會觸發。這也說明了 proxy 只能代理一層對象,不能深層代理!

那麼咱們須要監聽到嵌套的對象怎麼辦?app

其實這個也不難,就是在 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 對依賴進行觸發ui

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');
複製代碼

更過好看的文章,請查看個人公衆號

相關文章
相關標籤/搜索