需求 与 Bug
项目的 C# 桌面端使用 CefSharp 内嵌了一个三方网站,在外部实现了一个登录控件,外部登录后希望内嵌的三方网站自动登录,实现代码如下:
browser.ExecuteScriptAsync($"document.getElementsByName('username')[0].value = '{_login}'");browser.ExecuteScriptAsync($"document.getElementsByName('password')[0].value = '{_pwd}'");browser.ExecuteScriptAsync($"document.getElementsById('submitBtn').click()");
复制代码
通过 CefSharp 提供的方法在外部执行脚本,将登录控件的值填充至网站,旧版中正常,三方网站改版后无法正常填充。
尝试解决
直接修改 input 的 value 值无效,通过控制台 sources 面板查看,发现网站用 React 重构:
React 是状态驱动的视图库,直接修改元素的 value 值不会导致 React 应用状态的改变,我们需要让 React 感知到数据变化,即事件模拟:
const inputEl = document.querySelector('input');inputEl.value = "changed";inputEl.dispatchEvent(new Event('input', { bubbles: true, cancelable: false, composed: true }));
复制代码
结果是页面显示已经改变:
但内部的状态依然未变:
查询资料找到一个 React Issues,有人在讨论中给出解决方案 Trigger simulated input value,代码如下:
let input = someInput; let lastValue = input.value;input.value = 'new value';let event = new Event('input', { bubbles: true });// hack React15event.simulated = true;// hack React16 内部定义了 descriptor 拦截 value,此处重置状态let tracker = input._valueTracker;if (tracker) { tracker.setValue(lastValue);}input.dispatchEvent(event);
复制代码
问题解决。
知其然而知其所以然
上述问题激起了个人好奇心,尝试通过调试理解模拟输入事件无效的原因。
通过控制台 -> Elements -> Event Listeners 面板,找到 input 事件,查看已注册的事件处理程序:
React 实现了自己的合成事件系统,几乎所有的事件统一注册在 React 应用根元素上,这里即是截图中的 root 元素,点击右侧链接定位至源码:
所有事件都经过此函数,避免时间浪费,我们加上条件,只有 input 事件才会断点:
输入触发断点,中间会经过很多我们不关心的流程,最终进入 updateValueIfChanged 函数:
function getTracker(node) { return node._valueTracker;}
function getValueFromNode(node) { var value = '';
if (!node) { return value; }
if (isCheckable(node)) { value = node.checked ? 'true' : 'false'; } else { value = node.value; }
return value;}
function updateValueIfChanged(node) { if (!node) { return false; }
// 获取 tracker var tracker = getTracker(node);
if (!tracker) { return true; }
// 获取上次的值 var lastValue = tracker.getValue(); // 获取变更值 var nextValue = getValueFromNode(node);
// 不同则更新,此处是关键节点 if (nextValue !== lastValue) { tracker.setValue(nextValue); return true; }
return false;}
复制代码
tracker 的定义如下:
function trackValueOnNode(node) { // 判断 input 类型 var valueField = isCheckable(node) ? 'checked' : 'value';
// 定义属性描述符 var descriptor = Object.getOwnPropertyDescriptor(node.constructor.prototype, valueField);
// 设置初始值 var currentValue = '' + node[valueField];
if ( node.hasOwnProperty(valueField) || typeof descriptor === 'undefined' || typeof descriptor.get !== 'function' || typeof descriptor.set !== 'function' ) { return; }
var get = descriptor.get, set = descriptor.set;
// 对 value 属性的获取与设置进行拦截 Object.defineProperty(node, valueField, { configurable: true, get: function () { return get.call(this); }, set: function (value) { { checkFormFieldValueStringCoercion(value); }
/** * 注意此处,每当设置元素的 value 属性时,currentValue 也被修改, * 导致 updateValueIfChanged 函数始终返回 false */ currentValue = '' + value; set.call(this, value); } });
Object.defineProperty(node, valueField, { enumerable: descriptor.enumerable }); var tracker = { getValue: function () { return currentValue; }, setValue: function (value) { { checkFormFieldValueStringCoercion(value); }
currentValue = '' + value; }, stopTracking: function () { detachTracker(node); delete node[valueField]; } }; return tracker;}
复制代码
我们来看看真实输入的流程:
触发输入
进入 updateValueIfChanged
获取旧值和新值
两者不同,设置 value,updateValueIfChanged 返回 true(重要)
再来看看模拟的输入流程:
input.value = 'changed'
触发 value 属性的拦截,将内部的 tracker currentValue 更新为 changed
触发输入事件
进入 updateValueIfChanged
获取旧值和新值
两者相同,updateValueIfChanged 返回 false(重要)
这里模拟输入时,新旧值是相同的,所以 updateValueIfChanged 函数会返回 false,当返回 false 时不会触发 React 的更新机制去更新状态,即无法进入下图的流程:
我们再来看看之前的解决方案:
// 暂存旧值let lastValue = input.value;
// 设置新值input.value = 'new value';
// ...
// 获取 trackerlet tracker = input._valueTracker;
if (tracker) { // 将 tracker 设置为 旧值,此时新旧值不同,updateValueIfChanged 会返回 true tracker.setValue(lastValue);}
// ...
复制代码
---end
评论