react中修改input的defaultValue

在使用 react 进行开发时,我们一般使用类组件的 setState 或者 hooks 实现页面数据的实时更新,但在某些表单组件中,这一操作会失效,元素的数据却无法更新,令人苦恼

比如下面这个例子

import React, { useState } from "react";
function Demo() {
    const [num, setNum] = useState(0);
    return (
        <>
            <input defaultValue={num} />
            <button onClick={() => setNum(666)}>button</button>
        </>
    );
}
export default Demo;

理论上按钮点击后会执行 setNum 函数,并触发 Demo 组件重新渲染,input 展示最新值,但实际上 Input 值并没有更新到最新

如下截图:

从截图可以看出,num 值确实已经更新到了最新,但是 Input 中的值却始终没有同步更新,如何解决这个问题呢,很简单,在 input 上添加一个 key 即可。

但是仅仅知道解决方案还不够,奔着打破砂锅问到底的态度,我们今天就来探究下为啥通过修改 key 可以强制更新?

在开始之前,首先要明确一点: input 元素本身是没有 defaultValue 这个属性,如下图(点我查看),这个属性是 react 框架自己添加,一直以为是原生属性的我留下了没有技术的眼泪。

换句话说,如果不使用 react 框架,在 input 中是无法使用 defaultValue 属性的。

下面是一个使用 defaultValue 的简单例子

<head>
  <script type="text/javascript">
    function GetDefValue() {
      var elem = document.getElementById("myInput");
      var defValue = elem.defaultValue;
      var currvalue = elem.value;
      if (defValue == currvalue) {
        alert("The contents of the input field have not changed!");
      } else {
        alert("The default contents were " + defValue +
          "\n  and the new contents are " + currvalue);
      }
    }
  </script>
</head>
<body>
  <button onclick="GetDefValue ();">Get defaultValue!</button>
  <input type="text" id="myInput" value="Initial value">
  The initial value will not be affected if you change the text in the input field.
</body>

虽然 input 标签上不能直接设置 defaultValue,但是却可以通过操作 HTMLInputElement 对象设置和获取 defaultValue,需要注意的是,这里通过设置 defaultValue 也会同步修改 value 的值,但是因为 react 内部自定实现了 input 组件,所以在 react 中通过修改 defaultValue 并不会影响到 value 值,具体参看 ReactDOMInput.js。

以上是一些前置知识,接下来是具体的分析。

通过上面的介绍,我们首先要看下 react 是如何处理 defaultValue 这个属性的,这个属性是在 postMountWrapper 中设置的,源码如下:

export function postMountWrapper(
  element: Element,
  props: Object,
  isHydrating: boolean,
) {
  const node = ((element: any): InputWithWrapperState);
  if (props.hasOwnProperty('value') || props.hasOwnProperty('defaultValue')) {
    const type = props.type;
    const isButton = type === 'submit' || type === 'reset';
    if (isButton && (props.value === undefined || props.value === null)) {
      return;
    }
    const initialValue = toString(node._wrapperState.initialValue);
    if (!isHydrating) {
      if (initialValue !== node.value) {
        node.value = initialValue;
      }
    }
    node.defaultValue = initialValue;
  }
}

通过源码可以看出,react 内部会获取传入的 defaultValue,然后同时挂载到 node 的 value 和 defaultValue上,这样初次渲染的时候页面就会展示传入的默认属性,注意这个函数只会在初始化的时候执行。

接下来我们看下点击按钮后的逻辑,重点关注 mapRemainingChildren 函数:

function mapRemainingChildren(
  returnFiber: Fiber,
  currentFirstChild: Fiber,
): Map<string | number, Fiber> {
  // Add the remaining children to a temporary map so that we can find them by
  // keys quickly. Implicit (null) keys get added to this set with their index
  // instead.
  const existingChildren: Map<string | number, Fiber> = new Map();
  let existingChild = currentFirstChild;
  while (existingChild !== null) {
    if (existingChild.key !== null) {
      existingChildren.set(existingChild.key, existingChild);
    } else {
      existingChildren.set(existingChild.index, existingChild);
    }
    existingChild = existingChild.sibling;
  }
  return existingChildren;
}

这个函数会给每一个子元素添加一个 key 值,并添加到一个 set 中,之后会执行 updateFromMap 方法

function updateFromMap(
  existingChildren: Map<string | number, Fiber>,
  returnFiber: Fiber,
  newIdx: number,
  newChild: any,
  lanes: Lanes,
): Fiber | null {
  // ...
  if (typeof newChild === 'object' && newChild !== null) {
    switch (newChild.$$typeof) {
      case REACT_ELEMENT_TYPE: {
        const matchedFiber =
          existingChildren.get(
            newChild.key === null ? newIdx : newChild.key,
          ) || null;
        return updateElement(returnFiber, matchedFiber, newChild, lanes);
      }
    }
  }
  // ...
  return null;
}

在这个方法会通过最新传入的 key 获取 上面 set 中的值,然后将值传入到 updateElement 中

function updateElement(
  returnFiber: Fiber,
  current: Fiber | null,
  element: ReactElement,
  lanes: Lanes,
): Fiber {
  const elementType = element.type;
  if (current !== null) {
    if (
      current.elementType === elementType ||
      (enableLazyElements &&
        typeof elementType === 'object' &&
        elementType !== null &&
        elementType.$$typeof === REACT_LAZY_TYPE &&
        resolveLazy(elementType) === current.type)
    ) {
      // Move based on index
      const existing = useFiber(current, element.props);
      existing.ref = coerceRef(returnFiber, current, element);
      existing.return = returnFiber;
      if (__DEV__) {
        existing._debugSource = element._source;
        existing._debugOwner = element._owner;
      }
      return existing;
    }
  }
  // Insert
  const created = createFiberFromElement(element, returnFiber.mode, lanes);
  created.ref = coerceRef(returnFiber, current, element);
  created.return = returnFiber;
  return created;
}

因为我们在更新的时候修改了 key 值,所以这里的 current 是不存在的,走的是重新创建的代码,如果我们没有传入 key 或者 key 没有改变,那么走的的就是复用的代码,所以,如果使用 map 循环了多个 input 然后使用下标作为 key,就会出现修改后多个 input 状态不一致的详情,因此,表单组件不推荐使用下标作为 key,容易出 bug。

之后是更新代码的逻辑,input 属性的更新操作是在 updateWrapper 中进行的,我们看下这个函数的源码:

export function updateWrapper(element: Element, props: Object) {
  const node = ((element: any): InputWithWrapperState);
  updateChecked(element, props);
  // 重点,这里只会获取 value 的值,不会再获取 defaultValue 的值
  const value = getToStringValue(props.value);
  const type = props.type;
  if (value != null) {
    if (type === 'number') {
      if (
        (value === 0 && node.value === '') ||
        // We explicitly want to coerce to number here if possible.
        // eslint-disable-next-line
        node.value != (value: any)
      ) {
        node.value = toString((value: any));
      }
    } else if (node.value !== toString((value: any))) {
      node.value = toString((value: any));
    }
  } else if (type === 'submit' || type === 'reset') {
    // Submit/reset inputs need the attribute removed completely to avoid
    // blank-text buttons.
    node.removeAttribute('value');
    return;
  }
  // 根据设置的 value 或者 defaultValue 来 input 元素的属性
  if (props.hasOwnProperty('value')) {
    setDefaultValue(node, props.type, value);
  } else if (props.hasOwnProperty('defaultValue')) {
    setDefaultValue(node, props.type, getToStringValue(props.defaultValue));
  }
}

这里的 element 其实就是 input 对象,但是由于在设置时仅获取 props 中的 value,而没有获取 defaultValue,第 21 行不会执行,所以页面中的值也不会更新,但是第34行依然还是会执行,而且页面还出现了十分诡异的现象

如下图:

页面展示状态和源码状态不一致,HTML中的属性已经修改为了 666,但是页面依然展示的 0,估计是 react 在实现 input 时留下的一个隐藏 bug。

总结一下

react 内部会给 Demo 组件中的每一个子元素添加一个 key(传入或下标),然后将 key 作为 set 的键,之后通过最新的 key 去获取 set 中储存的值,如果存在复用原来元素,更新属性,如果不存在,重新创建,修改 key 可以达到每次都重新创建元素,而不是复用原来的元素,这就是修改 key 进而达到修改 defaultValue 的原因。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持阿兔在线工具。

点赞(0)

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部