不知道大家对于
Controlled Input
的概念好不好奇,我在最开始用React的时候就对其非常感兴趣,然而奈何那时候能力不够,也没那么多时间去看源码,所以一直处于猜测而没有去证实的阶段。在后来使用Vue进行开发的时候,我还自己实现过类似的组件,那时候是通过
preventDefault keyDown
事件来阻止自动更新,实现了大致的功能,所以当时觉得这可能也是React的做法吧。现在看了源码之后,发现自己还是
too young,React根本就不是这么做的,我还花了很多时间去找他阻止默认行为的证据,结果才发现走了歪路。闲话到此为止,我们来看看React中如何实现
Controlled Input
在看这个之前最好了解过React的事件系统,相关内容我已经准备好,但是还没整理成文章,大家可以先去自行了解下,我更新上来之后会在这里放链接,所以也可以关注我获取最新内容。
先来看一下batchedUpdates
的代码:
function batchedUpdates(fn, bookkeeping) {
if (isBatching) {
return fn(bookkeeping);
}
isBatching = true;
try {
return _batchedUpdates(fn, bookkeeping);
} finally {
isBatching = false;
var controlledComponentsHavePendingUpdates = needsStateRestore();
if (controlledComponentsHavePendingUpdates) {
_flushInteractiveUpdates();
restoreStateIfNeeded();
}
}
}
在finally
里面执行了一个方法叫做restoreStateIfNeeded
,如果你通过代码调试把debugger
放之前,并且你写一个不更新state
的demo,你会发现,在执行这个方法之前,input
里面的内容其实是先变了的,然后执行完之后再变回来。哈哈,现在你应该能大致猜到了吧,是的,React的做法并不关心event
,他等内容执行完了,然后看是否需要回滚再滚回来。
那么接下去我们就来看看细节
function restoreStateIfNeeded() {
if (!restoreTarget) {
return;
}
var target = restoreTarget;
var queuedTargets = restoreQueue;
restoreTarget = null;
restoreQueue = null;
restoreStateOfTarget(target);
if (queuedTargets) {
for (var i = 0; i < queuedTargets.length; i++) {
restoreStateOfTarget(queuedTargets[i]);
}
}
}
这里涉及两个公共变量:restoreTarget
和restoreQueue
,那么这两个变量会在什么情况下变化呢?
在同一个文件ReactControlledComponent
里面有这么一个方法
export function enqueueStateRestore(target) {
if (restoreTarget) {
if (restoreQueue) {
restoreQueue.push(target);
} else {
restoreQueue = [target];
}
} else {
restoreTarget = target;
}
}
那么这个方法什么时候调用呢?答案是在ChangeEventPlugin
里面
ChangeEventPlugin = {
extractEvents() {
// ...
const event = createAndAccumulateChangeEvent(
inst,
nativeEvent,
nativeEventTarget,
);
}
}
function createAndAccumulateChangeEvent(inst, nativeEvent, target) {
const event = SyntheticEvent.getPooled(
eventTypes.change,
inst,
nativeEvent,
target,
);
event.type = 'change';
// Flag this event loop as needing state restore.
enqueueStateRestore(target); // 这里
accumulateTwoPhaseDispatches(event);
return event;
}
我们先不关心这里具体是什么含义,我们只需要知道只有change event
才需要回滚内容,所以在生成change
事件的时候会把对应的节点。而生成事件的时间和batchUpdates
是一起的,这就把整个内容串联起来了
inpute content -> trigger change event -> create event object -> enqueueStateRestore -> events trigger finsihed -> restoreStateIfNeeded
如何restore
function restoreStateOfTarget(target) {
var internalInstance = getInstanceFromNode(target);
if (!internalInstance) {
// Unmounted
return;
}
var props = getFiberCurrentPropsFromNode(internalInstance.stateNode);
fiberHostComponent.restoreControlledState(internalInstance.stateNode, internalInstance.type, props);
}
在事件系统执行完之后,获取input
节点对应的Fiber
对象,然后读取新的props
,这里的props
是执行完绑定事件之后的最新的props
,这是对应controlled component
真正应该显示的props
,然后再去对比props
里面的内容和输入框内的实际内容,如果需要退回就执行。
function restoreControlledState(domElement, tag, props) {
switch (tag) {
case 'input':
restoreControlledState(domElement, props);
return;
case 'textarea':
restoreControlledState$3(domElement, props);
return;
case 'select':
restoreControlledState$2(domElement, props);
return;
}
}
function restoreControlledState(element, props) {
var node = element;
updateWrapper(node, props);
updateNamedCousins(node, props);
}
function updateWrapper(element, props) {
var node = element;
updateChecked(element, props);
var value = getSafeValue(props.value);
// 更新的重点在这里
if (value != null) {
if (props.type === 'number') {
if (value === 0 && node.value === '' ||
// eslint-disable-next-line
node.value != value) {
node.value = '' + value;
}
} else if (node.value !== '' + value) {
node.value = '' + value;
}
}
if (props.hasOwnProperty('value')) {
setDefaultValue(node, props.type, value);
} else if (props.hasOwnProperty('defaultValue')) {
setDefaultValue(node, props.type, getSafeValue(props.defaultValue));
}
if (props.checked == null && props.defaultChecked != null) {
node.defaultChecked = !!props.defaultChecked;
}
}
后面具体更新的细节就不分析了,相信大家都看的懂(可以忽略一些不重要的函数)