Skip to content

ref分析

为什么有ref,虽然官方总是不推荐使用这种破坏整体框架的api,但是实际开发,总有一些场景需要直接操作DOM元素,所以有了这个api.但是如果能不使用尽量不使用.

  • 破坏了"属性和状态去映射视图",正常流程中的组件属性均有数据映射而来,绑定了ref相当于提供直接修改属性的额外途径,导致属性不可控.
  • 破坏了"属性不可变性,单向数据流",增加额外了操作数据的途径,可能改变属性不可变性,让数据的流动不可控.
  • 降低了可读性,破坏了整体代码风格和组织结构.

虽然,有种种不利,但是在一些场景确实有效并且真香~

ref使用场景

  • 绘图

    1. 通过canvas元素获取画板上下文
    2. 通过canvas元素获取父元素宽度和高度,自适应自身高度和宽度
    3. 监听父元素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变化,到底应该在什么周期中进行操作(留到后面生命周期章节再详细分析)。

  • 购物车动画

    1. 通过ref获取购物车组件,父组件调用子组件的方法(goods=》父组件=》cart组件),创建小球并开始动画。
    2. 通过ref获取购物车的位置,即小球的落点
    3. 通过ref获取小球包裹元素。通过监听过渡结束事件移除元素
    4. 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组件即可。