React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > react修改input的defaultValue

react中实现修改input的defaultValue

作者:东都花神

这篇文章主要介绍了react中实现修改input的defaultValue方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

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 的原因。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文