一句话总结:React 受控组件劫持了 input 的 value 属性,直接用 el.value = x 赋值不会更新 React 状态。正确做法是通过 Object.getOwnPropertyDescriptor 拿到原生 setter 绕过 React 的拦截,再派发 input/change 事件通知 React 同步。


目录

  1. 问题场景
  2. React 受控组件原理
  3. 错误的尝试
  4. 正确的做法:原生 setter 绕过
  5. 完整封装与使用
  6. 其他框架的类似问题
  7. 总结

问题场景

在某小说平台的作者后台,需要自动填写章节标题和内容的 input 框。最直观的想法——找到元素,设 value:

1
js(ws, "document.querySelector('#chapter-title').value = '第97章 突破'")

运行后,页面上的输入框确实显示了文字,但点击”保存”时,React 拿到的值是空的

仔细检查发现:输入框有值(视觉上),但 React state 中没有。

React 受控组件原理

React 的受控组件工作方式是这样的:

1
2
// React 内部管理 value,而非 DOM
<input value={this.state.title} onChange={this.handleChange} />

React 在 input 元素上覆写了 value 属性的 gettersetter

  1. 直接设 el.value = x → 调用了 React 覆写的 setter → React 忽略了这个修改
  2. 用户输入 → 触发 input 事件 → React 捕获事件 → 调用 setState → 更新 value

所以关键在于:React 监听的是事件,不是属性变化。单纯改属性不触发事件,React 不知道 value 变了。

错误的尝试

1
2
3
4
5
6
// ❌ 直接赋值——UI 变了但 React 不知道
el.value = '新标题';

// ❌ 派发 input 事件——但 value 仍然是 React 控制的旧值
el.dispatchEvent(new Event('input', {bubbles: true}));
// React 读取 el.value 时,getter 返回的是 React state 值,不是刚设的值

为什么派发 input 事件也不行?因为 React 覆写了 input 的 value getter——el.value 返回的是 React state 中的值,而不是实际 DOM 属性。所以派发事件时,React 读取到的 value 仍然是旧值。

正确的做法:原生 setter 绕过

解决方案的核心:拿到被 React 覆写之前的原始 value setter,用它设置值,再派发事件让 React 同步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def set_react_input(ws, css_selector, value):
"""设置 React 受控输入框的值"""
return js(ws, f"""
(() => {{
const el = document.querySelector('{css_selector}');
if (!el) return 'NOT_FOUND';

// 聚焦输入框
el.focus();

// 获取 HTMLInputElement 原型上的原始 value setter
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;

// 用原始 setter 设置值(绕过 React 的覆写)
nativeSetter.call(el, '{value}');

// 派发 input 事件通知 React 更新状态
el.dispatchEvent(new Event('input', {{bubbles: true}}));

// 派发 change 事件(部分框架需要)
el.dispatchEvent(new Event('change', {{bubbles: true}}));

return 'OK';
}})()
""")

分步解析

  1. Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set

    • 获取 HTMLInputElement 类原型上的 value setter
    • 这是浏览器原生提供的,尚未被 React 覆写
    • React 实际上覆写的是每个实例上的 value 属性,而不是原型上的
  2. nativeSetter.call(el, value)

    • call 将原生 setter 应用到具体元素上
    • 绕过 React 实例上的 getter/setter,直接修改 DOM 属性
  3. el.dispatchEvent(new Event('input', {bubbles: true}))

    • React 监听 input 事件来同步状态
    • 派发事件后 React 的 onChange 被触发,调用 setState 更新

完整示例

1
2
3
4
5
6
7
8
9
10
# 设置章节标题
result = set_react_input(ws, '#chapter-title', '第97章 突破')
print(result) # 'OK'

# 设置章节内容(textarea 同样适用)
result = set_react_input(ws, '.editor-content', '章节正文内容...')

# 验证是否设置成功
value = js(ws, "document.querySelector('#chapter-title').value")
print(f'当前值: {value}') # 应该输出:第97章 突破

完整封装与使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class ReactInputHandler:
"""处理 React 受控组件的输入"""

def __init__(self, ws):
self.ws = ws

def set_value(self, css_selector, value):
"""设置输入框值(React 受控组件安全)"""
v = value.replace("'", "\\'") # 防止引号冲突
return js(self.ws, f"""
(() => {{
const el = document.querySelector('{css_selector}');
if (!el) return 'NOT_FOUND';
el.focus();
const ns = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;
ns.call(el, '{v}');
el.dispatchEvent(new Event('input', {{bubbles: true}}));
el.dispatchEvent(new Event('change', {{bubbles: true}}));
return 'OK';
}})()
""")

def set_textarea(self, css_selector, value):
"""textarea 同样适用(HTMLTextAreaElement)"""
v = value.replace("'", "\\'")
return js(self.ws, f"""
(() => {{
const el = document.querySelector('{css_selector}');
if (!el) return 'NOT_FOUND';
el.focus();
const ns = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype, 'value'
).set;
ns.call(el, '{v}');
el.dispatchEvent(new Event('input', {{bubbles: true}}));
el.dispatchEvent(new Event('change', {{bubbles: true}}));
return 'OK';
}})()
""")

def clear_and_set(self, css_selector, value):
"""清空后设置新值"""
js(self.ws, f"""
(() => {{
const el = document.querySelector('{css_selector}');
if (!el) return;
el.focus();
const ns = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;
ns.call(el, '');
el.dispatchEvent(new Event('input', {{bubbles: true}}));
}})()
""")
return self.set_value(css_selector, value)

def get_react_value(self, css_selector):
"""获取 React 管理的实际值(而非 DOM 属性值)"""
return js(self.ws, f"""
document.querySelector('{css_selector}').value
""")

# 使用
handler = ReactInputHandler(ws)
handler.set_value('#title', '新标题')
handler.set_textarea('.content-area', '正文内容...')

其他框架的类似问题

不只是 React 有这个问题,其他框架也有类似的”受控组件”行为:

框架 问题 解决方案
React 覆写 value getter/setter 原生 setter + input 事件
Vue v-model 绑定 同上(Vue 也监听 input 事件)
Angular ngModel 绑定 同上
Arco Design 自定义 input 组件 可能需要额外触发自定义事件
Ant Design Form.Item + getFieldDecorator 原生 setter + input 事件通常有效

通用原则:先尝试原生 setter + input 事件,如果不行,查看组件库源码看它监听什么事件。

总结

  1. React 受控组件不能直接用 el.value = x 赋值,因为 React 覆写了 value 属性
  2. 正确方法:通过 Object.getOwnPropertyDescriptor 拿到原型上的原生 setter,绕过 React 的拦截
  3. 必须派发事件:设值后要 dispatch input + change 事件通知 React 更新状态
  4. textarea 同样适用HTMLTextAreaElement.prototype.value 的 setter 同理
  5. 先聚焦再操作:部分组件库要求输入框处于 focus 状态才能写入
  6. 注意特殊字符:值中包含引号或反斜杠时需要转义

总结:理解 React 受控组件的底层原理——value 的 getter/setter 被覆写——是解决这类问题的关键。原生 setter 绕过 + 事件通知的模式不仅适用于 React,也适用于 Vue、Angular 等使用类似机制的框架。