今天我们站在框架开发者的角度来聊聊如何实现受控组件
。
在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
传递给,达到
value
受控的目的。
如果让你来设计,你会怎么做?
我相信大部分同学第一个想法是:将value prop
与其他attribute prop
一样处理就行。
我们知道React
内部运行有3个阶段:
schedule 调度更新阶段
render 进行diff算法的阶段
commit 进行DOM操作的阶段
假设我们要在onChange
中触发更新改变className
,只需要在render
阶段记录要改变的className
,在commit
阶段执行对应的addClass DOM
操作。
同样的,如果我们要在onChange
中触发更新改变value
,只需要在render
阶段记录要改变的value
,在commit
阶段执行对应的inputDOM.setAttribute('value', 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
。
总结
可以看到,要实现一个完备的前端框架,是有非常多细节的。
为了实现受控组件
,就得脱离整体更新流程,单独实现一套流程。
更多React
源码细节,欢迎关注我的公众号魔术师卡颂
。