主頁 > 前端設計 > 用 Proxy 簡單實作 Vue 3 的 Reactive

用 Proxy 簡單實作 Vue 3 的 Reactive

2020-12-11 12:25:16 前端設計

這里要給同學們分享的是 Proxy 與雙向系結,我們對大部分的 JavaScript 的這種基礎庫其實已經在其他文章中做過一些講解了,或者是在我們編程的時候有所接觸了,唯有這個 Proxy 我們之前是非常的回避的,因為在業務中也不太推薦大量的使用 Proxy,

Proxy 的設計其實是一種,強大且危險的一種設計,因為應用了 Proxy 的一些代碼,它的 “預期性” 會變差,所以 proxy 這個特性是專門為底層庫而設計的,

Proxy 基本用法

這里我們就一起學習一下 proxy 的基本用法,在后面我們會一起實作一下 Vue 3.0reactive 的模型,當然這里實作的 reactive 并不是一個生產可用的代碼,只是寫一個概念版或者是玩具版的一個 reactive,主要還是用它去認識和學習一下 proxy 有哪些強大的用途,

這里我們邊寫代碼邊了解 Proxy 的一個整體特性,首先我們先創建一個 object,然后我們給這個 object 一些屬性,

let object = {
  a: 1,
  b: 2
}

現在如果我們去訪問這個 objecta 屬性和 b 屬性,這個中間其實是有一個獲取程序,但是在 JavaScript 的底層是一個寫死的方法,也就是說我們無法去干預或者監聽這個獲取物件屬性的程序的代碼,

那么這個 object 它就是一個不可 observe (觀察) 的物件,所以就是一個單純的資料存盤,這也是 JavaScript 最底層的機制,我們是沒有辦法去改變的,

那么如果我們想有一個物件,我們既想它擁有普通物件一樣的特性,又想讓它能夠被監聽,那么我們可以怎么做呢?這個時候我們就可以通過一個 proxy 來給 object 做一層包裹,

那么接下來我們就用 proxy 來實作一個這樣的物件,

  • 首先我們需要創建一個 Proxy()
  • 并且第一個引數需要把我們的 object 傳進去
  • 然后第二個引數是一個 config 的配置物件
  • 這個 config 物件里面就包含了所有的我們針對 proxy 物件的鉤子
  • 這里我們就做一個最簡單的鉤子 set —— 當我們去設定物件的一個屬性的時候就會觸發我們的 set 函式
  • 這個 set 函式會接收我們當前物件屬性名屬性值等三個引數
let object = {
  a: 1,
  b: 2
}

let po = new Proxy(object, {
  set(obj, prop, val) {
     console.log(obj, prop, val)
  }
}) 

這個時候我們把這個代碼在瀏覽器運行一下,這里我們運行一個 po.a = 6

這里同學們可以看到,如果 po 是一個普通物件的話這里應該什么代碼都不會去執行的,除非 a 它本身就是一個 setter,但是在我們撰寫的這個 proxy 物件上,不管我們去設定哪一個屬性,都會運行我們的 set 函式,并且獲得不一樣的值,

我們來嘗試設定一個 po 物件中沒有的屬性看看,

首先 proxy 跟 getter 和 setter 最主要的一個區別就是,proxy 物件上即使我們設定一個沒有的屬性,它也會默認觸發這個 set 的方法,

我們的 proxy 里面不只提供了 getset 這些屬性的鉤子,其實里面還可以攔截并且改變原生的操作或者是對物件進行操作的內置函式的行為,

如果我們上 MDN 的網站上是可以看到所有 proxy 所支持的鉤子,這里列出的有 applyconstructdefinePropertydeleteProperty 等等這一系列的內置或者原生的操作進行攔截并且改變它們的行為,所以說 proxy 物件是一個非常強大的物件

回到我們的例子中,我們 proxy 實際上就是代理了 object 這個物件,如果我們去呼叫原始的 object上的值,并不會觸發 proxy 上的 hook (鉤子) 里面的函式,

只有使用我們的 po(也就是我們定義的一個 object 物件的 proxy 代理物件) 才會最后去執行到 proxy 物件的攔截行為,而 object 還是原來的 object,

所以我們可以把 po 理解成一個特殊的物件,而 po 上面所有的行為都是可以被重新去指定的,這個也就是為什么我們一開始的時候會說,object 中使用了 proxy 之后物件行為的可預測性就會降低,因為我們看到的一個代碼,比如 po.a = 6 在執行的時候也許背后就做了一系列很復雜的操作,這些我們是不會知道的,所以 proxy 的這個特性是一個非常危險的特性,

接下來我們來看看 Proxy 的一些應用,

模仿 Reactive 實作原理

這里我們嘗試給物件做一個簡單的包裝, Vue 3.0 其中一個改動就是把 Vue 原來的能力拆了一個包,產生了一個叫reactive 的這一個單獨的包,

Reactive 是一個 Vue 3.0 中非常好的東西,這里我們就嘗試去模仿一下它在 Vue 中的實作原理,如果有看過 Vue 3.0 原始碼的同學應該都會知道,Vue 3.0 中的 reactive 是使用 proxy 來實作的,

那么我們就來一起實作一個玩具版的 reactive 的小練習,從而我們更能了解 proxy 的實際應用場景,

首先我們要知道,一般對 proxy 的使用,都是會對物件做某種監聽或者是改變他行為的事情,所以說對 proxy 的封裝是不會像我們這樣,直接用 new Proxy 這樣的方式,我們都會把它包進一個函式里面,跟我們的 Promise 比較類似,

封裝 reative 函式

所以這里我們先來實作一個包裹起來的 reactive 函式:

  • reactive 函式會接收一個 object 作為引數
  • 然后我們的 proxy 物件就是這個函式的回傳值
  • 之前的 Proxyconfig 中我們寫了 set, 這里我們加上一個 get 方法
  • 然后我們就可以把 po 改為使用 reactive(object) 來監聽它所有的屬性相關的操作了
let object = {
  a: 1,
  b: 2,
};

let po = reactive(object);

function reactive(object) {
  return new Proxy(object, {
    set(obj, prop, val) {
      console.log(obj, prop, val);
    },
    get(obj, prop) {
      console.log(obj, prop);
    },
  });
}

就是這樣我們就把 new Proxy 給包裝起來了,我們可以看到如果我們想去包裝多個 object 的話就可以繼續去復用這個 reactive 的代碼,

然后我們來看看在瀏覽器中運行的效果:

這里我們執行以下 po.a = 666

這里我們就可以得到 set 函式中的 console.log 列印出來的內容了,但是這里面其實還有一個問題,如果我們在 console 中列印 object ,就會發現我們的 object 原來它并沒有變化,就是我們執行的 po.a = 666 并沒有在 object 中生效,

所以這里我們需要在 set 函式中把這個執行改變的代碼加上,讓它實際的去操作這個 object 改變的行為,然后我們同時可以把 get 的功能也實作了,

let object = {
  a: 1,
  b: 2,
};

let po = reactive(object);

function reactive(object) {
  return new Proxy(object, {
    set(obj, prop, val) {
      obj[prop] = val;
      console.log(obj, prop, val);
      return obj[prop];
    },
    get(obj, prop) {
      console.log(obj, prop);
      return obj[prop];
    },
  });
}

這時候我們執行 po.x = 666,我們就會發現原始被代理的物件 object 上面已經添加了新的屬性 x,同樣我們也是可以去改原來的變數的,如果我們執行 po.b = 777 那么 object 中的屬性也會跟著發生變化,

這里我們就實作了一個 poobject 的一個完全的代理,當然如果我們想真正做一個完整的代理我們是需要把 proxy 中所有的 hook 都要考慮清楚,因為有的時候我們去訪問一個物件或者改變一個物件的時候,其實并不是說通過這種表面的 getset 的屬性的方式去訪問的,

我們還是可以通過一些內置的方法,比如說 defineProperty ,需要對我們的物件發生作用,這個時候我們就需要把所有的 hook 都補全了,

但是我們可以忽略一些 hook 不去處理,比如說 applyconstruct,因為它們管的是用 new 去呼叫這個物件和物件后面加圓括號產生的結果,

學習到這里我們已經獲得了一個基本的,能夠代理 object 行為并且可以去監聽 object ,并且包含了所有設定屬性或者改變屬性的行為的一個 proxy 物件,

接下來我們一起來嘗試給他再加入真正的 reactive 特性,讓事件可以變得可監聽,

實作事件監聽

我們有了 reactive 這樣一個函式之后,我們可以考慮一下如何去監聽,當然我們可以給 po 上面去加 addEventListener 類似的操作,但是在 Vue 當中他們用了一個特別有意思的 API,

就是我們可以直接通過 effect 傳一個函式進入來監聽 po 上面的一個屬性,以此來代替這個事件監聽的機制,那么下面我們來嘗試實作一個 “粗糙版”,

  • 因為這個 effect 是接收一個回呼函式的,所以我們這里需要再寫一個 effect 函式
  • 然后我們的 effect 函式需要接收一個 callback 引數
  • 我們需要一個全域的 callbacks 陣列變數來儲存我們所有的 callback 函式
  • effect 函式中我們把傳入進來的 callback 函式給 push 到我們的 callback 陣列中儲存起來
  • 這樣的話,我們在 set 的時候就直接遍歷 callbacks并執行里面所有的回呼函式即可
// 回呼函式儲存組數
let callbacks = [];

let object = {
  a: 1,
  b: 2,
};

let po = reactive(object);

/**
   * effect 函式
   * @param {Function} callback 回呼函式
   * @return void
   */
function effect(callback) {
  callbacks.push(callback);
}

// 加入一個監聽事件
effect(() => {
  console.log('effected a : ', po.a);
});

/**
   * reactive 相應函式
   * @param {Object} object
   * @return Object
   */
function reactive(object) {
  return new Proxy(object, {
    // 物件賦值
    set(obj, prop, val) {
      obj[prop] = val;

      // 呼叫所有監聽回呼函式
      for (let callback of callbacks) {
        callback();
      }

      return obj[prop];
    },
    // 物件取值
    get(obj, prop) {
      console.log(obj, prop);
      return obj[prop];
    },
  });
}

這個就是一個非常粗糙的實作了 reactive 中屬性的監聽事件,接下來我們來看看實際效果如何:

這里可以看到我們加入的 effect() 回呼函式確實被執行了,如果我們只考慮實作的正確性,而不考慮性能的話我們就已經完成了 reactive 的操作,但是這個里面顯然它有一個嚴重的性能問題的,比如說我們有 100 個物件,并且給 100 個物件設定了 100 個 effect,那么每次執行一遍就要調一萬遍,因為每次它都把我們全域變數 callbacks 中記錄的回呼函式都執行一遍,

顯然我們實作的這個 reactive 只是一個中間步驟,它并不是一個最終結果,那么我們接下來就去嘗試解決這個問題,看看能不能做到僅傳一個函式就能讓它只有在對應的變數變化的時候,觸發這個函式的呼叫,

建立 reactive 與 effect 連接

上一部分我們建立了物件屬性的監聽,這里我們給 reactive 物件屬性和 effect 函式之間建立獨立的連接,之前我們的 effect 函式與我們 reactive物件屬性是沒有一對一的關系的,這樣 100 個物件就會系結 100 effect,所以這里就會有一個性能隱患,

也就是說如果我們監聽了 po.a 的話,當我們執行 po.a = 2 的時候,我們的 effect 回呼函式就會被執行,但是如果我們執行的是 po.b = 3時,就不應該執行我們的 effect 函式,因為 po.b 并沒有被監聽,

如果我們想實作這樣的效果,我們就需要一個物件屬性effect 之間的依賴關系,它們之間有一個一對一的關聯關系,互相回應,

讓我們先來嘗試一下建立一個 userReactivities 來儲存我們的監聽物件屬性,

  • 首先我們需要準備一個 usedReactivities 的全域變數,來儲存我們需要監聽的物件和物件的屬性
  • 接著我們嘗試在 effect 里面去呼叫一次這個代理物件的屬性,比如 po.a,這樣就觸發了這個屬性的監聽,因為我們呼叫了 po.a,也就是一個獲取變數值的動作,所以這里就會呼叫到我們 reactive 中的 get,這里我們把物件和物件屬性都注冊進入 usedReactivies 這個變數里面
  • 然后我們改造一下我們的 effect 函式,在這里我們首先需要清除一次我們的 usedReactivities,保證每次注冊的時候都是全新的,這樣才會清除掉之前監聽的物件屬性,
// 回呼函式儲存組數
let callbacks = [];
// 使用過的函式屬性
let usedReactivities = [];

let object = {
  a: 1,
  b: 2,
};

let po = reactive(object);

/**
   * effect 函式
   * @param {Function} callback 回呼函式
   * @return void
   */
function effect(callback) {
  // callbacks.push(callback);
  usedReactivities = [];
  callback();
  console.log(usedReactivities);
}

// 加入一個監聽事件
effect(() => {
  console.log('effected a : ', po.a);
});

/**
   * reactive 相應函式
   * @param {Object} object
   * @return Object
   */
function reactive(object) {
  return new Proxy(object, {
    // 物件賦值
    set(obj, prop, val) {
      obj[prop] = val;

      // 呼叫所有監聽回呼函式
      for (let callback of callbacks) {
        callback();
      }

      return obj[prop];
    },
    // 物件取值
    get(obj, prop) {
      usedReactivities.push([obj, prop]);
      return obj[prop];
    },
  });
}

這里我們可以看到,在 effect 被呼叫的時候,我們的物件和物件的屬性都被正確的注入到 usedReactivities 之中,這里我們只是做了一個簡單的物件和物件屬性的存盤,并不能讓我們建立物件屬性與 effect 函式的依賴關系,我們需要另外把所有 callbacks 儲存起來,從而讓他們與我們的物件屬性建立依賴關系,

  • 接下來我們可以使用 callbacks 這個全域變數來存盤我們的依賴關系,所以這里我們就需要把它改造成一個 new Map() 來存,因為我們需要把 object 物件作為一個 key,這樣我們才可以用它來找到對應的 reactivities (物件屬性的對應 callback 函式),
  • 然后我們就可以去改造我們的 effect 函式,在我們呼叫了 callback() 之后,我們的 usedReactivites 中就會擁有我們需要監聽的物件和物件屬性了,接著我們就需要注入我們的物件屬性與 effect 依賴關系到 callbacks 里面,
  • 我們的 物件屬性effect 的依賴資料是以物件和物件屬性為 key,key [0] 是我們的物件key [1] 是我們的物件屬性,我們的 value 就是我們的 callback 回呼函式
  • 有了這個依賴關系,我們就需要在 reactive 觸發 set 的時候根據當前物件和物件屬性找到對應的 callback 函式來執行,如果找不到就是這個物件屬性沒有被監聽,不需要執行回呼函式,
// 回呼函式儲存組數
let callbacks = new Map();
// 使用過的函式屬性
let usedReactivities = [];

let object = {
  a: 1,
  b: 2,
};

let po = reactive(object);

/**
   * effect 函式
   * @param {Function} callback 回呼函式
   * @return void
   */
function effect(callback) {
  // callbacks.push(callback);
  usedReactivities = [];
  callback();

  for (let reactivity of usedReactivities) {
    if (!callbacks.has(reactivity[0])) {
      callbacks.set(reactivity[0], new Map());
    }

    if (!callbacks.has(reactivity[1])) {
      callbacks.get(reactivity[0]).set(reactivity[1], []);
    }

    callbacks.get(reactivity[0]).get(reactivity[1]).push(callback);
  }
}

// 加入一個監聽事件
effect(() => {
  console.log('effected a : ', po.a);
});

/**
   * reactive 相應函式
   * @param {Object} object
   * @return Object
   */
function reactive(object) {
  return new Proxy(object, {
    // 物件賦值
    set(obj, prop, val) {
      obj[prop] = val;

      if (callbacks.get(obj))
        if (callbacks.get(obj).get(prop))
          // 呼叫所有監聽回呼函式
          for (let callback of callbacks.get(obj).get(prop)) {
            callback();
          }

      return obj[prop];
    },
    // 物件取值
    get(obj, prop) {
      usedReactivities.push([obj, prop]);
      return obj[prop];
    },
  });
}

最后我們在瀏覽器中運行,我們就會發現執行 po.a=3 觸發了我們 effect 回呼函式,但是 po.b=6 并沒有觸發,這個就是我們想要的效果了,但是我們的代碼還是寫的比較粗糙的,也沒有考慮到解除的效果,不過我們這段代碼已經演示了 reactivity 的實作原理,

優化 reactive

到了這里我們的 effectreactive 已經可以跑起來了,但是其實里面還是有一些小問題的,

比如說現在我們的 object 中的 a 也是一個物件:

let object = {
  a: {b: 3},
  b: 2
}

然后我們在 effect 里面,呼叫了 po.a.b 這樣的連級物件呼叫,那么這個物件它是一個監聽不到 a 里面的 b 屬性的,

所以說我們有必要對它再進行一些處理,讓它能夠支持 po.a.b 這種形式的呼叫,要滿足這樣的功能,我們就要對 reactivegetset 有一定的要求,

當我們 get 中的 obj[prop] 是一個物件的時候,我們就需要給它套一個 reactivity,也就是說當我們檢測到 prop 是一個 object 的話,我們就給它回傳一個 reactive(obj[prop])

那么我們來改造一下 reactive 中的 get 方法:

/**
   * reactive 相應函式
   * @param {Object} object
   * @return Object
   */
function reactive(object) {
  return new Proxy(object, {
    // 物件賦值
    set(obj, prop, val) {
      obj[prop] = val;

      if (callbacks.get(obj))
        if (callbacks.get(obj).get(prop))
          // 呼叫所有監聽回呼函式
          for (let callback of callbacks.get(obj).get(prop)) {
            callback();
          }

      return obj[prop];
    },
    // 物件取值
    get(obj, prop) {
      usedReactivities.push([obj, prop]);

      if (typeof obj[prop] === 'object') return reactive(obj[prop]);

      return obj[prop];
    },
  });
}

這樣的改造雖然是可以讓我們物件中的物件也被代理了,但是我們會一個問題,就是我們的 reactive 是會回傳一個新的 proxy 的,那就意味著,po.a.b 拿到的 proxypo.a 不是同一個 proxy

所以這里我們就需要把 proxy 物件放入一個全域的暫存變數里面,方便我們呼叫的時候在快取資料里面重新拿出來,我們就宣告一個 reativities ,默認值為 new Map()

當每個物件去呼叫 reactivity 的時候,我們會加一個快取,因為 proxy 本身它是不存盤任何狀態的,而所有的狀態都會代理到 object 上,某種意義上講 reactive 其實是一個無狀態的函式,所以我們可以對它進行快取,

我們就在 reactive 的函式開始的位置,加入一個判斷,如果我們快取變數 reactivies 中有這個 object,我們就直接回傳,如果沒有我們就執行我們的新 proxy 生成并且把它存入 reactivies

好,我們來看看代碼是怎么實作的,

// 這個callbacks是一個依賴收集而已
// 它表示的是,某個object的某個prop,被一些函式使用了,
// 我們把這些函式存在一個array里,通過 callbacks.get(object).get(props),
// 我們就能拿到這些函式
let callbacks = new Map();

// reactivities 只是保存了object和它對應的proxy的k-v關系,
let reactivities = new Map();

// useReactivities是當我們初次呼叫effect(callback)的時候,
// 會先初始化運行一次callback,然后把依賴關系暫存在useReactivities
// 這個陣列里面,暫存的格式是像下面這樣:
/**
  [
    [物件A,物件A被依賴的某個屬性],
    [物件B,物件B被依賴的某個屬性],
    [物件C,物件C被依賴的某個屬性],
    ...
  ]
  **/
let usedReactivities = [];

let object = {
  a: { b: 3 },
  b: 2,
};

let po = reactive(object);

/**
   * effect 函式
   * @param {Function} callback 回呼函式
   * @return void
   */
function effect(callback) {
  // callbacks.push(callback);
  usedReactivities = [];
  callback();

  for (let reactivity of usedReactivities) {
    if (!callbacks.has(reactivity[0])) {
      callbacks.set(reactivity[0], new Map());
    }

    if (!callbacks.has(reactivity[1])) {
      callbacks.get(reactivity[0]).set(reactivity[1], []);
    }
    console.log('123', callbacks);

    callbacks.get(reactivity[0]).get(reactivity[1]).push(callback);
  }
}

// 加入一個監聽事件
effect(() => {
  console.log('effected a.b : ', po.a.b);
});

/**
   * reactive 相應函式
   * @param {Object} object
   * @return Object
   */
function reactive(object) {
  if (reactivities.has(object)) return reactivities.get(object);

  let proxy = new Proxy(object, {
    // 物件賦值
    set(obj, prop, val) {
      obj[prop] = val;

      if (callbacks.get(obj))
        if (callbacks.get(obj).get(prop))
          // 呼叫所有監聽回呼函式
          for (let callback of callbacks.get(obj).get(prop)) {
            callback();
          }

      return obj[prop];
    },
    // 物件取值
    get(obj, prop) {
      usedReactivities.push([obj, prop]);

      if (typeof obj[prop] === 'object') return reactive(obj[prop]);

      return obj[prop];
    },
  });

  reactivities.set(object, proxy);

  return proxy;
}

這樣我們就完成了 reactive 的邏輯,我們來看看實際效果是否正確,

這里我們可以看到,無論是我們直接去改變級聯物件中的 b ,還是給 a 重新賦值一個 {b: 66} 都是可以觸發我們的 effect 回呼函式的,

這就意味著我們最后的 function 已經能夠成功地呼叫和執行了,到這里為止我們的 proxyreactive 的實作和基本的模型就已經有了,當然還有很多細節,是需要我們用大量的 test case 去保證一些邊緣的情況的,如果大家想看看一個真正完成的 reactivity 這個庫是怎么寫的,那么我們可以參考 Vue 的源代碼,大家就可以看到這個代碼量是我們這個的好幾倍不止,

所以講原理和實際操作還是有比較大的區別的,希望大家在學習的時候都理解這一點,要不然我們直接把這些代碼拿過去生產使用,那就要出問題了,

Reactive 回應式物件

接下來我們來考慮一下 Reactive 到底有什么應用場景,這個也是很多同學在 Vue 3.0 出來了以后,在瘋狂的問的一個問題,

其實 Reactive 它是一個半成品的雙向系結,它可以負責從資料到 DOM 元素這一條線的監聽,從 DOM 元素到資料的這一條線的監聽其實很簡單,因為 DOM 元素本來就有事件,然后任何的原生輸入都可以代理到這個 reactive 的代理里面,

我們接下來就考慮一個實際的例子,這里我們來做一個輸入的單向系結來看看,

  • 我們給我們之前實作的 reactive 中加入一個 input 元素
  • 我們給這個 input 綁上一個 id="r"
  • 改造一下我們的 object 資料結構
  • 在我們的 effect 當中加入單向系結(從資料到 input)
<input type="text" id="r" />

<script>
  let callbacks = new Map();
  let reactivities = new Map();
  let usedReactivities = [];

  let object = {
    r: 1,
  };

  let po = reactive(object);
  
  // 加入一個監聽事件
  effect(() => {
    // 加入了單向資料系結
    document.getElementById('r').value = po.r;
  });

  /**
     * effect 函式
     * @param {Function} callback 回呼函式
     * @return void
     */
  function effect(callback) {
    usedReactivities = [];
    callback();

    for (let reactivity of usedReactivities) {
      if (!callbacks.has(reactivity[0])) {
        callbacks.set(reactivity[0], new Map());
      }

      if (!callbacks.has(reactivity[1])) {
        callbacks.get(reactivity[0]).set(reactivity[1], []);
      }

      callbacks.get(reactivity[0]).get(reactivity[1]).push(callback);
    }
  }

  /**
     * reactive 相應函式
     * @param {Object} object
     * @return Object
     */
  function reactive(object) {
    if (reactivities.has(object)) return reactivities.get(object);

    let proxy = new Proxy(object, {
      // 物件賦值
      set(obj, prop, val) {
        obj[prop] = val;

        if (callbacks.get(obj))
          if (callbacks.get(obj).get(prop))
            // 呼叫所有監聽回呼函式
            for (let callback of callbacks.get(obj).get(prop)) {
              callback();
            }

        return obj[prop];
      },
      // 物件取值
      get(obj, prop) {
        usedReactivities.push([obj, prop]);

        if (typeof obj[prop] === 'object') return reactive(obj[prop]);

        return obj[prop];
      },
    });

    reactivities.set(object, proxy);

    return proxy;
  }
</script>

然后我們在瀏覽器運行時,我們會看到 input 中的數字是 1,然后在 console 中輸入 po.r = 10,我們就會發現資料會被同步到我們的 input 中,

就是這樣我們就已經實作了初步的資料系結了,那么如果我們想實作 雙向系結需要怎么做呢?其實也很簡單,我們只需要加入 addEventListener 即可實作雙向系結,

在我們的 effect 呼叫的后面加入下面一行代碼即可:

/** ... 代碼省略 ... **/

// 加入一個監聽事件
effect(() => {
  // 加入了單向資料系結
  document.getElementById('r').value = po.r;
});
// 資料雙向系結 (從 input 到資料)
document.getElementById('r').addEventListener('input', event => po.r = event.target.value);

/** ... 代碼省略 ... **/

這個時候我們回到我們的瀏覽器,在 input 中嘗試輸入數字,我們就會發現我們的 po.r 的值也會回應到值的變化,

接下來我們嘗試實作一個 “回應的顏色選擇器”:

  • 我們之前已經有一個屬性 r,現在我們在 object 里面補上 bg
  • 也同時加上這兩個單獨的 input 元素,分別的 id 是 id="b"id="g"
  • 然后我們再多加兩個 effect 各自給到 bg 的 input 元素的,
  • 給 input 元素中加入 type="range",并且給他們都加上最大與最小值
  • 資料雙系結的代碼也要給 bg 加上
  • 建立一個 div 元素,加上屬性 id="color"style="width:100px; height: 100px"
  • 最后我們需要加入一個全域的 effect,這里面需要在任何 input 輸入變動的時候,回應改變我們 div 盒子的背景顏色

我們來把這些邏輯寫成代碼:

<input id="r" type="range" min="0" max="255" />
<input id="g" type="range" min="0" max="255" />
<input id="b" type="range" min="0" max="255" />
<div id="color" style="width: 100px; height: 100px"></div>

<script>
  let callbacks = new Map();
  let reactivities = new Map();
  let usedReactivities = [];

  let object = {
    r: 1,
    g: 1,
    b: 1,
  };

  let po = reactive(object);

  /**
     * effect 函式
     * @param {Function} callback 回呼函式
     * @return void
     */
  function effect(callback) {
    // callbacks.push(callback);
    usedReactivities = [];
    callback();

    for (let reactivity of usedReactivities) {
      if (!callbacks.has(reactivity[0])) {
        callbacks.set(reactivity[0], new Map());
      }

      if (!callbacks.has(reactivity[1])) {
        callbacks.get(reactivity[0]).set(reactivity[1], []);
      }

      callbacks.get(reactivity[0]).get(reactivity[1]).push(callback);
    }
  }

  // 紅色顏色輸入值變動
  effect(() => {
    document.getElementById('r').value = po.r;
  });
  // 綠色顏色輸入值變動
  effect(() => {
    document.getElementById('g').value = po.g;
  });
  // 藍色顏色輸入值變動
  effect(() => {
    document.getElementById('b').value = po.b;
  });

  // 資料雙向系結 (從 input 到資料)
  // 系結紅色輸入變動
  document.getElementById('r').addEventListener('input', event => (po.r = event.target.value));
  // 系結綠色輸入變動
  document.getElementById('g').addEventListener('input', event => (po.g = event.target.value));
  // 系結藍色輸入變動
  document.getElementById('b').addEventListener('input', event => (po.b = event.target.value));

  // 回應盒子背景顏色
  effect(() => {
    document.getElementById('color').style.backgroundColor = `rgb(${po.r}, ${po.g}, ${po.b})`;
  });

  /**
     * reactive 相應函式
     * @param {Object} object
     * @return Object
     */
  function reactive(object) {
    if (reactivities.has(object)) return reactivities.get(object);

    let proxy = new Proxy(object, {
      // 物件賦值
      set(obj, prop, val) {
        obj[prop] = val;

        if (callbacks.get(obj))
          if (callbacks.get(obj).get(prop))
            // 呼叫所有監聽回呼函式
            for (let callback of callbacks.get(obj).get(prop)) {
              callback();
            }

        return obj[prop];
      },
      // 物件取值
      get(obj, prop) {
        usedReactivities.push([obj, prop]);

        if (typeof obj[prop] === 'object') return reactive(obj[prop]);

        return obj[prop];
      },
    });

    reactivities.set(object, proxy);

    return proxy;
  }
</script>

在瀏覽器中運行,我們就可以看到上方有三個滑塊,通過拖動改變每一個滑塊的值,下面的盒子的背景顏色就會根據 rgb 三個值而變化,

我們這里所寫的代碼,僅僅是對它的變數和值進行了一下簡單的系結關系,

如果說我們再配合一定的語法糖,比如說我們的 build compiler 那么我們就完全可以把它變成一個零代碼的雙向系結模式,

這也正是雙向系結一個強大之處,在很多時候我們互動不需要使用代碼,即可實作互動,其實想想以前我們用 Jquery 做很多的互動邏輯代碼,我們需要寫很多的邏輯和 update 代碼來實作一個互動的程序,而有了雙向系結后,我們可以花更多時間精力專注于撰寫 Vue 和輸入的關系,

這一切都是基于我們擁有了 reactivity 這種回應式物件,那么 Vue 的 reactivity 包被拆出來之后,也會給大家帶來更有意思的想法和實踐,



博主開始在B站直播學習,歡迎過來《直播間》一起學習,

我們在這里互相監督,互相鼓勵,互相努力走上人生學習之路,讓學習改變我們生活!

學習的路上,很枯燥,很寂寞,但是希望這樣可以給我們彼此帶來多一點陪伴,多一點鼓勵,我們一起加油吧! (? ?????)?


我是來自《技術銀河》的三鉆,一位正在重塑知識的技術人,下期再見,


推薦專欄

小伙伴們可以查看或者訂閱相關的專欄,從而集中閱讀相關知識的文章哦,

  • 📖 《前端進階》 — 這里包含的文章學習內容需要我們擁有 1-2 年前端開發經驗后,選擇讓自己升級到高級前端工程師的學習內容(這里學習的內容是對應阿里 P6 級別的內容),

  • 📖 《資料結構與演算法》 — 到了如今,如果想成為一個高級開發工程師或者進入大廠,不論崗位是前端、后端還是AI,演算法都是重中之重,也無論我們需要進入的公司的崗位是否最后是做演算法工程師,前提面試就需要考演算法,

  • 📖 《FCC前端集訓營》 — 根據FreeCodeCamp的學習課程,一起深入淺出學習前端,穩固前端知識,一起在FreeCodeCamp獲得證書

  • 📖 《前端星球》 — 以實戰為線索,深入淺出前端多維度的知識點,內含有多方面的前端知識文章,帶領不懂前端的童鞋一起學習前端,在前端開發路上童鞋一起燃起心中那團火🔥

三鉆 CSDN認證博客專家 前端 Vue React
—— 起步于PHP,一入前端深似海,最后愛上了前端,Vue、React使用者,專于Web、移動端開發,特別關注產品和UI設計,專心、專注、專研,與同學們一起終身學習,關注我的微信公眾號《技術銀河》有更多最新知識文章與同學們分享,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/233136.html

標籤:其他

上一篇:Qt 6.0正式版2020-12-08發布

下一篇:Android -從淺到懂使用反射機制

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • vue移動端上拉加載

    可能做得過于簡單或者比較low,請各位大佬留情,一起探討技術 ......

    uj5u.com 2020-09-10 04:38:07 more
  • 優美網站首頁,頂部多層導航

    一個個人用的瀏覽器首頁,可以把一下常用的網站放在這里,平常打開會比較方便。 第一步,HTML代碼 <script src=https://www.cnblogs.com/szharf/p/"js/jquery-3.4.1.min.js"></script> <div id="navigate"> <ul> <li class="labels labels_1"> ......

    uj5u.com 2020-09-10 04:38:47 more
  • 頁面為要加<!DOCTYPE html>

    最近因為寫一個js函式,需要用到$(window).height(); 由于手寫demo的時候,過于自信,其實對前端方面的認識也不夠體系,用文本檔案直接敲出來的html代碼,第一行沒有加上<!DOCTYPE html> 導致了$(window).height();的結果直接是整個document的高 ......

    uj5u.com 2020-09-10 04:38:52 more
  • WordPress網站程式手動升級要做好資料備份

    WordPress博客網站程式在進行升級前,必須要做好網站資料的備份,這個問題良家佐言是遇見過的;在剛開始接觸WordPress博客程式的時候,因為升級問題和博客網站的修改的一些嘗試,良家佐言是吃盡了苦頭。因為購買的是西部數碼的空間和域名,每當佐言把自己的WordPress博客網站搞到一塌糊涂的時候 ......

    uj5u.com 2020-09-10 04:39:30 more
  • WordPress程式不能升級為5.4.2版本的原因

    WordPress是一款個人博客系統,受到英文博客愛好者和中文博客愛好者的追捧,并逐步演化成一款內容管理系統軟體;它是使用PHP語言和MySQL資料庫開發的,用戶可以在支持PHP和MySQL資料庫的服務器上使用自己的博客。每一次WordPress程式的更新,就會牽動無數WordPress愛好者的心, ......

    uj5u.com 2020-09-10 04:39:49 more
  • 使用CSS3的偽元素進行首字母下沉和首行改變樣式

    網頁中常見的一種效果,首字改變樣式或者首行改變樣式,效果如下圖。 代碼: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, ......

    uj5u.com 2020-09-10 04:40:09 more
  • 關于a標簽的講解

    什么是a標簽? <a> 標簽定義超鏈接,用于從一個頁面鏈接到另一個頁面。 <a> 元素最重要的屬性是 href 屬性,它指定鏈接的目標。 a標簽的語法格式:<a href=https://www.cnblogs.com/summerxbc/p/"指定要跳轉的目標界面的鏈接">需要展示給用戶看見的內容</a> a標簽 在所有瀏覽器中,鏈接的默認外觀如下: 未被訪問的鏈接帶 ......

    uj5u.com 2020-09-10 04:40:11 more
  • 前端輪播圖

    在需要輪播的頁面是引入swiper.min.js和swiper.min.css swiper.min.js地址: 鏈接:https://pan.baidu.com/s/15Uh516YHa4CV3X-RyjEIWw 提取碼:4aks swiper.min.css地址 鏈接:https://pan.b ......

    uj5u.com 2020-09-10 04:40:13 more
  • 如何設定html中的背景圖片(全屏顯示,且不拉伸)

    1 <style>2 body{background-image:url(https://uploadbeta.com/api/pictures/random/?key=BingEverydayWallpaperPicture); 3 background-size:cover;background ......

    uj5u.com 2020-09-10 04:40:16 more
  • Java學習——HTML詳解(上)

    HTML詳解 初識HTML Hyper Text Markup Language(超文本標記語言) 1 <!--DOCTYPE:告訴瀏覽器我們要使用什么規范--> 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <!--meta 描述性的標簽,描述一些 ......

    uj5u.com 2020-09-10 04:40:33 more
最新发布
  • 我的第一個NPM包:panghu-planebattle-esm(胖虎飛機大戰)使用說明

    好家伙,我的包終于開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ......

    uj5u.com 2023-04-20 07:59:23 more
  • 生產事故-走近科學之消失的JWT

    入職多年,面對生產環境,盡管都是小心翼翼,慎之又慎,還是難免捅出簍子。輕則滿頭大汗,面紅耳赤。重則系統停擺,損失資金。每一個生產事故的背后,都是寶貴的經驗和教訓,都是專案成員的血淚史。為了更好地防范和遏制今后的各類事故,特開此專題,長期更新和記錄大大小小的各類事故。有些是親身經歷,有些是經人耳傳口授 ......

    uj5u.com 2023-04-18 07:55:04 more
  • 記錄--Canvas實作打飛字游戲

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 打開游戲界面,看到一個畫面簡潔、卻又富有挑戰性的游戲。螢屏上,有一個白色的矩形框,里面不斷下落著各種單詞,而我需要迅速地輸入這些單詞。如果我輸入的單詞與螢屏上的單詞匹配,那么我就可以獲得得分;如果我輸入的單詞錯誤或者時間過長,那么我就會輸 ......

    uj5u.com 2023-04-04 08:35:30 more
  • 了解 HTTP 看這一篇就夠

    在學習網路之前,了解它的歷史能夠幫助我們明白為何它會發展為如今這個樣子,引發探究網路的興趣。下面的這張圖片就展示了“互聯網”誕生至今的發展歷程。 ......

    uj5u.com 2023-03-16 11:00:15 more
  • 藍牙-低功耗中心設備

    //11.開啟藍牙配接器 openBluetoothAdapter //21.開始搜索藍牙設備 startBluetoothDevicesDiscovery //31.開啟監聽搜索藍牙設備 onBluetoothDeviceFound //30.停止監聽搜索藍牙設備 offBluetoothDevi ......

    uj5u.com 2023-03-15 09:06:45 more
  • canvas畫板(滑鼠和觸摸)

    <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>canves</title> <style> #canvas { cursor:url(../images/pen.png),crosshair; } #canvasdiv{ bo ......

    uj5u.com 2023-02-15 08:56:31 more
  • 手機端H5 實作自定義拍照界面

    手機端 H5 實作自定義拍照界面也可以使用 MediaDevices API 和 <video> 標簽來實作,和在桌面端做法基本一致。 首先,使用 MediaDevices.getUserMedia() 方法獲取攝像頭媒體流,并將其傳遞給 <video> 標簽進行渲染。 接著,使用 HTML 的 < ......

    uj5u.com 2023-01-12 07:58:22 more
  • 記錄--短視頻滑動播放在 H5 下的實作

    這里給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 短視頻已經無數不在了,但是主體還是使用 app 來承載的。本文講述 H5 如何實作 app 的視頻滑動體驗。 無聲勝有聲,一圖頂百辯,且看下圖: 網址鏈接(需在微信或者手Q中瀏覽) 從上圖可以看到,我們主要實作的功能也是本文要講解的有: ......

    uj5u.com 2023-01-04 07:29:05 more
  • 一文讀懂 HTTP/1 HTTP/2 HTTP/3

    從 1989 年萬維網(www)誕生,HTTP(HyperText Transfer Protocol)經歷了眾多版本迭代,WebSocket 也在期間萌芽。1991 年 HTTP0.9 被發明。1996 年出現了 HTTP1.0。2015 年 HTTP2 正式發布。2020 年 HTTP3 或能正... ......

    uj5u.com 2022-12-24 06:56:02 more
  • 【HTML基礎篇002】HTML之form表單超詳解

    ??一、form表單是什么

    ??二、form表單的屬性

    ??三、input中的各種Type屬性值

    ??四、標簽 ......

    uj5u.com 2022-12-18 07:17:06 more