今天我们站在框架开发者的角度来聊聊如何实现受控组件
。
在React
中一个简单的受控组件
如下:
function App() {
const [num, updateNum] = React.useState(0);
const onChange = ({target: {value}}) => {
updateNum(value);
}
return (
<input value={num} onChange={onChange}/>
)
}
在onChange
中会更新num
,num
作为value prop
传递给<input/>
,达到value
受控的目的。
如果让你来设计,你会怎么做?
我相信大部分同学第一个想法是:将value prop
与其他attribute prop
一样处理就行。
我们知道React
内部运行有3个阶段:
假设我们要在onChange
中触发更新改变className
,只需要在render
阶段记录要改变的className
,在commit
阶段执行对应的addClass DOM
操作。
同样的,如果我们要在onChange
中触发更新改变value
,只需要在render
阶段记录要改变的value
,在commit
阶段执行对应的inputDOM.setAttribute('value', value)
操作。
这样逻辑非常通顺。那么事实上呢?
className
只是inputDOM
上的一个普通属性。而value
则涉及到输入框光标的位置。
如果我们直接修改value
,那么属性改变后input
的光标输入位置也会丢失,光标会跳到输入框的最后。
想想我们将1234
修改为12534
。
1234 --> 12534
需要先将光标位置移动到2之后,再输入5。
如果setAttribute('value', '12534')
,那么光标不会保持在5后面而是跳到4后面。
那么React
如何解决这个问题呢?
你没有看错,React
用非受控
形式实现了受控组件
的逻辑。
简单的说,不同于className
在commit
阶段受控更新,value
则完全是非受控的形式,只在必要的时候受控更新。
因为一旦更新value
,那么光标位置就会丢失。
我们稍微修改下Demo,input
为受控组件,value
始终为1:
function App() {
const num = 1;
return (
<input value={num}/>
)
}
当我们在源码中打上断点,输入2后,实际上会先显示12,再删掉2。
只不过这个删除的过程是同步的所以看起来输入框内始终只有1。
所以,不同于React
其他组件props
的更新会经历schedule - render - commit
流程。
对于input
、textarea
、select
,React
有一条单独的更新路径,这条路径触发的更新被称为discreteUpdate
。
这条路径的工作流程如下:
非受控
的形式更新表单DOM同步
的优先级开启一次更新value
在commit
阶段并不会像其他props
一样作用于DOM
restoreStateOfTarget
方法,比较DOM的实际value
(即步骤1中的非受控value)与步骤3中更新的value
,如果相同则退出,如果不同则用步骤3的value
更新DOM什么情况下这2个value
会相同呢?
我们正常的受控组件就是相同的情况:
function App() {
const [num, updateNum] = React.useState(0);
const onChange = ({target: {value}}) => {
updateNum(value);
}
return (
<input value={num} onChange={onChange}/>
)
}
什么情况下这2个value
会不同呢?
上面的Demo中,虽然受控,但是没有调用updateNum
更新value
的情况:
function App() {
const num = 1;
return (
<input value={num}/>
)
}
在这种情况下,步骤1的非受控value
变为了12,步骤3的受控value
还是1,所以最终会用1再更新下DOM的value
。
可以看到,要实现一个完备的前端框架,是有非常多细节的。
为了实现受控组件
,就得脱离整体更新流程,单独实现一套流程。