ref分析
为什么有
ref,虽然官方总是不推荐使用这种破坏整体框架的api,但是实际开发,总有一些场景需要直接操作DOM元素,所以有了这个api.但是如果能不使用尽量不使用.
- 破坏了"属性和状态去映射视图",正常流程中的组件属性均有数据映射而来,绑定了ref相当于提供直接修改属性的额外途径,导致属性不可控.
- 破坏了"属性不可变性,单向数据流",增加额外了操作数据的途径,可能改变属性不可变性,让数据的流动不可控.
- 降低了可读性,破坏了整体代码风格和组织结构.
虽然,有种种不利,但是在一些场景确实有效并且真香~
ref使用场景
绘图
- 通过canvas元素获取画板上下文
- 通过canvas元素获取父元素宽度和高度,自适应自身高度和宽度
- 监听父元素resize,更新视图。
<div id="root-canvas" ></div>class DrawPanel extends React.Component { constructor(props) { super(props) this.state = { value: 1 } } handleChange = (e) => { this.setState({ value: e.target.value }) } render () { return ( <div style={{ width: "100%", height: "400px" }}> <DrawRing value={this.state.value} /> {this.state.value} <input type="range" value={this.state.value} onChange={ this.handleChange } /> </div> ) } } class DrawRing extends React.Component { canvas = React.createRef() constructor(props) { super(props); } componentDidMount () { this.currentValue = 0; this.ctx = this.canvas.current.getContext('2d'); this.clear(); this.draw(); this.canvas.current.parentNode.addEventListener('resize', () => { this.clear(); this.draw(); }) } componentDidUpdate () { this.clear(); this.draw(); } clear () { const canvas = this.canvas.current; const parentNode = canvas.parentNode; canvas.width = parentNode.offsetWidth; canvas.height = parentNode.offsetHeight; this.centerPos = [canvas.width / 2, canvas.height / 2]; } draw () { let { value = 10, color = 'red', duration = 1000, bgColor = '#e3e3e3', wd = 0 } = this.props, ctx = this.ctx, centerPos = this.centerPos, r = 1.5 * Math.min.apply(null, centerPos) / 2, currentValue = this.currentValue, speed = 3.6 * 10 * (value - currentValue) / duration; speed = Math[speed >0 ? 'max': 'min'](speed > 0 ? 0.0001 : -0.0001, speed); wd = wd || r / 5; currentValue += speed; if (speed > 0 && currentValue + speed > value) { currentValue = value } if (speed < 0 && currentValue + speed < value) { currentValue = value } ctx.beginPath(); ctx.arc(centerPos[0], centerPos[1], r, 0, Math.PI * 2 * (currentValue / 100), false); ctx.strokeStyle = color; ctx.lineWidth = wd; ctx.stroke() ctx.closePath(); ctx.beginPath(); ctx.arc(centerPos[0], centerPos[1], r, Math.PI * 2 * (currentValue / 100), Math.PI * 2, false); ctx.strokeStyle = bgColor; ctx.lineWidth = wd; ctx.stroke() ctx.closePath(); ctx.beginPath(); ctx.font = "normal normal normal " + r / 4 + "px arial"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillStyle = color; ctx.fillText((Math.ceil(currentValue*100)/100).toFixed(2) + "%", centerPos[0], centerPos[1]) ctx.closePath(); this.currentValue = Number(currentValue); clearTimeout(this.timer); if (currentValue != value) { this.timer = setTimeout(() => { this.clear(); this.draw(); }) } } render () { return <div style={{ height: "100%", width: "100%" }}><canvas height={0} width={0} ref={this.canvas}>Your browser does not support the canvas element.</canvas></div> } } ReactDOM.render(<DrawPanel />, document.querySelector('#root-canvas'))ps:写这个demo过程遇到一个问题,父组件中state变化,触发子组件的props变化,到底应该在什么周期中进行操作(留到后面生命周期章节再详细分析)。
购物车动画
- 通过ref获取购物车组件,父组件调用子组件的方法(goods=》父组件=》cart组件),创建小球并开始动画。
- 通过ref获取购物车的位置,即小球的落点
- 通过ref获取小球包裹元素。通过监听过渡结束事件移除元素
- cart组件中直接通过
React.createRef创建ref并绑定,父组件获取子组件通过传递一个函数onRef,子组件在建立的时候通过调用此函数传递自己this,绑定到父组件的属性上。
<style> .ball { width: 10px; height: 10px; border-radius: 50%; position: fixed; background: chartreuse; transition: all 2s cubic-bezier(.21, .74, .3, .83); /* animation: run-left 4s linear, run-bottom 4s linear; animation-fill-mode: forwards; */ } .shopcart { display: flex; } .shopcart>img { width: 40px; height: 40px; } </style> <div id="root-cart"></div>class Shop extends React.Component { state = { goods: [ { name: "苹果", count: 0 }, { name: "香蕉", count: 1 }, { name: "樱桃", count: 0 } ], } buy = (value, index, e) => { let newGoods = this.state.goods.map(function (good) { if (value.name == good.name) good.count += 1 return good }) this.setState({ goods: newGoods }) e.persist() this.cartRef.add({ left: e.nativeEvent.x, top: e.nativeEvent.y, }) } onRef = ref => { this.cartRef = ref } del = (value, index) => { let newGoods = this.state.goods.map(function (good) { if (value.name == good.name) good.count -= 1 return good }) this.setState({ goods: newGoods }) } render () { const goods = this.state.goods, buyGoods = goods.filter(value => { return value.count != 0 }) return <div style={{ position: 'relative' }}> <Goods data={goods} buy={this.buy}></Goods> <Cart onRef={this.onRef} data={buyGoods} del={this.del}></Cart> </div> } } class Goods extends React.Component { buy = (value, index, e) => { this.props.buy(value, index, e) } render () { const data = this.props.data; return <ul> { data.map((value, index) => (<li onClick={(e) => { this.buy(value, index, e) }} key={index}><span>{value.name}</span><span>{value.count}</span></li>)) } </ul> } } class Cart extends React.Component { refWrap = React.createRef() cartRef = React.createRef() constructor(props) { super(props) this.props.onRef(this); } del = (value, index) => { this.props.del(value, index) } state = { balls: [] } ballId = 0 add = (ball) => { ball = { style: ball, ballId: this.ballId++ } this.setState({ balls: [ ball, ...this.state.balls ] }) setTimeout(() => { this.animated(); }, 0) } del = (index) => { if (!index) index = 0; var newBalls = this.state.balls.map(value => value) newBalls.pop(); this.setState({ balls: newBalls }) } componentDidMount () { const rect = this.cartRef.current.getBoundingClientRect() this.target.left = rect.x + rect.width / 2; this.target.top = rect.y + rect.height / 2; this.refWrap.current.addEventListener('webkitTransitionEnd', (e) => { this.del() }) } target = { left: 0, top: 0 } animated = () => { if (this.state.balls.length > 0 && this.state.balls.some(value => value.left != 0)) { var newBalls = this.state.balls.map(value => { return Object.assign({}, { style: this.target }, { ballId: value.ballId }) }); this.setState({ balls: newBalls }) console.log(this.state) } } render () { const data = this.props.data; const balls = this.state.balls; return <div ref={this.refWrap} className={'shopcart'}> <img ref={this.cartRef} src="./imgs/cart.png" /> <ul > { balls.map((ball, index) => <li className="ball" key={'ball' + ball.ballId} style={ball.style}></li>) } { data.map((value, index) => (<li onClick={() => { this.del(value, index) }} key={index}><span>{value.name}</span><span>{value.count}</span></li>)) } </ul> </div> } } ReactDOM.render(<Shop />, document.querySelector('#root-cart'))越级绑定ref
React.forwardRef:首先为什么要使用这个方法,因为React中不支持想传递普通属性props一样传递ref这个命名的属性。
<Test testname={'普通属性'} ref={testref} />如上,在Test组件中testname可以通过this.props.testname获取到父组件传递的值(传入子组件的参数)。但是ref不同,因为ref的作用是绑定组件或者DOM,可以通过绑定值操作组件或者DOM,所以ref的值是它绑定的组件或者DOM,并不是传入到组件内,所以并不能使用props属性获取。那么如果我们要传递一个ref进入子组件怎么做呢?那么就不能使用ref,可以换个名字:
class App extends React.Component {
sonRef = React.createRef()
grandsonRef = React.createRef()
onClickHandle=()=>{
console.log(this.sonRef)
console.log(this.grandsonRef)
}
render () {
return <div onClick={this.onClickHandle}> <Son ref={this.sonRef} diyref={this.grandsonRef} /></div>
}
}
class Son extends React.Component {
render () {
const {diyref} = this.props;
return <div><div>Son</div><GrandSon ref={diyref} ></GrandSon></div>
}
}
class GrandSon extends React.Component {
render () {
console.log(this.props)
return <div>GrandSon</div>
}
}
ReactDOM.render(<App />, document.querySelector('#root'))如上,我们通过diyref传递一个父组件定义的ref到子组件中,然后通过子组件绑定到孙组件上。同理,我们也可以采用传递函数的方式传递一个函数子组件,然后子组件把这个函数传递到孙组件,在孙组件中调用这个函数返回自己,最终绑定到父组件的属性上。 那么除了换名称,还有其他什么方式传递ref吗,事实上有的,官方还提供了一个React.forwardRef来做这件事。
class App extends React.Component {
sonRef = React.createRef()
grandsonRef = React.createRef()
onClickHandle=()=>{
console.log(this.sonRef)
console.log(this.grandsonRef)
}
render () {
return <div onClick={this.onClickHandle}> <Son ref={this.sonRef} diyref={this.grandsonRef} /></div>
}
}正常情况下,上面的ref直接绑定了Son,所以在Son中我们获取不到ref,但是如果我希望在Son中能获取ref传递的参数,而不是把当做绑定操作,我们可以是用React.forwardRef来定义Son:
const Son = React.forwardRef((props,ref)=>{
return <div><div>Son</div><GrandSon ref={ref} ></GrandSon></div>
})这种情况下,ref当做第二参数直接传入了,而不是绑定在组件上了。
看到这里,官网上的例子就不难理解了,结合上面,我们有更好的实践:
function logProps (Component) {
class LogProps extends React.Component {
componentDidUpdate (prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render () {
const { forwardedRef, ...rest } = this.props;
// 将自定义的 prop 属性 "forwardedRef" 定义为 ref
return <Component ref={forwardedRef} {...rest} />;
}
}
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}上面调用logProps时候加得ref被挂载到被 LogProps 包裹的子组件上,同时利用常规 prop 属性传递ref和React.forwardRef,而我们只需要正常的编写实际的LogProps组件即可。
