banner
sora

sora

编程心得 & 人文感悟

為什麼直接設置React輸入框的value沒有用

背景#

最近圖省事寫個油猴腳本做一些自動填寫表單的任務
直接找到該節點,修改對應的 value 是不生效的,再次 focus 後原來的值又會設置回去,這是為什麼呢

圖文無關
我有個朋友汗流浃背了

过程#

考慮到 React 框架使用範圍很廣,input 組件又很基礎
說不定這個問題已經被解決過了,所以可以先搜一遍

image

首先去翻了博客園和某臭名昭著的四字母盜庫網站
基本沒有靠譜的辦法,發什麼事件都不行
除了一位網頁稱 React 開發的網頁調用底下的_valueTracker 屬性就生效了

我調了,不管事啊

但這麼個問題,總不至於去模擬鍵盤輸入吧

雖然沒解決問題,但好歹知道網頁是用 React 框架做的了
除了這種碰巧的辦法,還可以使用 wappalyzer 插件,不過是要收費才能解鎖全部實際能力
但我覺得實際上手動翻 <head> 也不是不行

這點基礎問題,感覺不至於解決不了吧
碰碰運氣去翻 React 的源碼,發現解決方法竟然就寫在 input 組件的單元測試裡
不得不說大型開源項目,註釋比代碼多,文件名清晰易懂,真是又規範又好看

// https://github.com/facebook/react/blob/main/packages/react-dom/src/events/plugins/__tests__/ChangeEventPlugin-test.js#L273
const setUntrackedValue = Object.getOwnPropertyDescriptor(
  HTMLInputElement.prototype,
  'value',
).set;

...

    // Set it programmatically.
    input.value = 'bar';
    // Even if a DOM input event fires, React sees that the real input value now
    // ('bar') is the same as the "current" one we already recorded.
    input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    expect(input.value).toBe('bar');
    // In this case we don't expect to get a React event.
    expect(called).toBe(0);

    // However, we can simulate user typing by calling the underlying setter.
    setUntrackedValue.call(input, 'foo');
    // Now, when the event fires, the real input value ('foo') differs from the
    // "current" one we previously recorded ('bar').
    input.dispatchEvent(new Event('input', {bubbles: true, cancelable: true}));
    expect(input.value).toBe('foo');
    // In this case React should fire an event for it.
    expect(called).toBe(1);

原因#

那麼,為什麼我直接設置 value 屬性不能生效呢?
我們知道,JavaScript 備受詬病不是沒有原因的
我們現代 lisp 怎麼你了
裡面可以實現各種騷操作
各種變量混在一塊,在 ES6 之前幾乎沒有作用域的限制
可以輕而易舉地修改其他代碼的屬性,甚至換掉 undefined
著名的一個 evil 倉庫就是利用這點來做供應鏈投毒各種搞事

React 封裝 ReactDOMInput 組件時
替換掉了瀏覽器原生 input 組件的 value 對應的 descriptor

// https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/client/inputValueTracking.js#L79
  Object.defineProperty(node, valueField, {
    configurable: true,
    // $FlowFixMe[missing-this-annot]
    get: function () {
      return get.call(this);
    },
    // $FlowFixMe[missing-local-annot]
    // $FlowFixMe[missing-this-annot]
    set: function (value) {
      if (__DEV__) {
        checkFormFieldValueStringCoercion(value);
      }
      currentValue = '' + value;
      set.call(this, value);
    },
  });
  // We could've passed this the first time
  // but it triggers a bug in IE11 and Edge 14/15.
  // Calling defineProperty() again should be equivalent.
  // https://github.com/facebook/react/issues/11768
  Object.defineProperty(node, valueField, {
    enumerable: descriptor.enumerable,
  });

  const tracker = {
    getValue() {
      return currentValue;
    },
    setValue(value: string) {
      if (__DEV__) {
        checkFormFieldValueStringCoercion(value);
      }
      currentValue = '' + value;
    },
    stopTracking() {
      detachTracker(node);
      delete node[valueField];
    },
  };
  return tracker;

其中 currentValue 負責跟蹤 input 組件值的變化,是 tracker getValue的返回值
如果直接設置value屬性,會走到set方法裡,同時設置currentValue和調用原生的set方法

因為 tracker 跟蹤的currentValue和原生 input 組件裡的value值是一樣的
這樣,即使在觸發 input 事件後,外部強行寫入的值並不會更改 react 內部的值

// https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/client/inputValueTracking.js
export function updateValueIfChanged(node: ElementWithValueTracker): boolean {
  if (!node) {
    return false;
  }

  const tracker = getTracker(node);
  // if there is no tracker at this point it's unlikely
  // that trying again will succeed
  if (!tracker) {
    return true;
  }

  const lastValue = tracker.getValue(); // 注:currentValue
  const nextValue = getValueFromNode(node); // 注:input.value
  if (nextValue !== lastValue) {
    tracker.setValue(nextValue);
    return true;
  }
  return false;
}

在 input 重新獲得焦點時,ReactDOMInputrestoreControlledInputState被調用,原生組件強行寫入的值被 React 中原本記錄的值覆蓋掉

// https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/client/ReactDOMInput.js#L340
export function restoreControlledInputState(element: Element, props: Object) {
  const rootNode: HTMLInputElement = (element: any);
  updateInput(
    rootNode,
    props.value,
    props.defaultValue,
    props.defaultValue,
    props.checked,
    props.defaultChecked,
    props.type,
    props.name,
  );

    ...

}

那麼,為什麼要加入一個currentValue的機制呢?難道是因為 React 開發者們不喜歡油猴腳本?

通過git blame 翻到packages\react-dom\src\events\plugins\__tests__\ChangeEventPlugin-test.js的最後一次提交,是一次目錄結構整理,可以看到原本這裡是有一段非常詳細的註釋,介紹了為什麼要加入這個機制

// https://github.com/facebook/react/commit/a67757e11540936677b0a5d89d623b3e38b5fe69#diff-76a986d539bc7c94d63e46233a9537e03d8994f69ac4e4cd332631bc911a1c09R81

    // We try to avoid firing "duplicate" React change events.
    // However, to tell which events are duplicates and should be ignored,
    // we are tracking the "current" input value, and only respect events
    // that occur after it changes. In this test, we verify that we can
    // keep track of the "current" value even if it is set programatically.

    // Set it programmatically.
    input.value = 'bar';

在 2016 年,這個機制只針對設置了 onChange 屬性的 Input 組件生效
從這段註釋中,可以得出這樣的結論:
加入這個機制一開始在當時是為了去重 change 事件,所以在input.value之外加了一个currentValue,避免多次觸發 onChange 綁定的函數

感想#

開源對行業和個人成長的影響真的都是非常正面的
遇到問題可以自己去查去學習
而不是從抄來抄去的三手資料和寫的模糊不清的閉源軟件文檔裡收穫花費再多時間也是拳頭打棉花的無力感

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。