深入react技术栈学习——(二)漫谈react

事件系统

react基于虚拟dom实现了一个SynthetivEvent(合成事件)层,我们所定义的事件处理器会接收到一个SynthetivEvent对象的实例,它完全符合w3c标准,不会存在任何ie标准的兼容性。并且与原生浏览器事件一样拥有同样的接口,同样支持事件冒泡机制。

    合成事件的绑定方法

React 元素的事件处理和 DOM元素的很相似。但是有一点语法上的不同:

  • React事件绑定属性的命名采用驼峰式写法,而不是小写。
  • 如果采用 JSX 的语法你需要传入一个函数作为事件处理函数,而不是一个字符串(DOM元素的写法)

例如,传统的 HTML:

<button onclick="activateLasers()">
  Activate Lasers
</button>

React 中稍稍有点不同:

<button onClick={activateLasers}>
  Activate Lasers
</button>

    合成事件的实现机制

在react底层,主要对合成事件做了两件事:事件委派和自动绑定。

  1. 事件委派
    react并不会吧事件处理函数直接绑定到真实的节点上,而是把所有事件绑定到结构的最外层,使用一个统一的时间监听器,这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器处理,然后再映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率有很大提升。
  2. 自动绑定
    react组件中,每个方法的上下文都会指向该组件的实例,即自动绑定this为当前组件。而且react还会对这种引用进行缓存,以达到cpu和内存的最优化。当使用 es6 class 语法来定义一个组件的时候,事件处理器会成为类的一个方法,需要手动手动实现this的绑定。
    以下是几种绑定方法:
    (1)bind方法。这个方法可以帮助我们绑定事件处理器内的this,并可以向事件处理器中传递参数
      
    import React from 'react'
    class App extends React.Component{
    
        handleClick(e,arg){
            console.log(e,arg)
        }
    
        render(){
            return <button onClick={this.handleClick.bind(this,"test")}>Test</button>
        }
    }

    (2)构造器内声明。在组件的构造器内完成this的绑定,这种绑定方式的好处在于仅需要进行一次绑定,而不需要每次调用事件监听器去执行绑定操作

    import React from 'react'
    class App extends React.Component{
        constructor(props){
            super(props);
            this.handleClick=this.handleClick.bind(this)
        }
    
        handleClick(e){
            console.log(e)
        }
    
        render(){
            return <button onClick={this.handleClick}>Test</button>
        }
    }

    (3)箭头函数。箭头函数不仅是函数的语法糖,还自动绑定了定义此函数作用域的this,因此我们不需要再对它使用bind方法。

    import React from 'react'
    class App extends React.Component{
        
    
        handleClick=(e)=>{
            console.log(e)
        }
    
        render(){
            return <button onClick={this.handleClick}>Test</button>
        }
    }

    上述几种方法,都能实现在类定义的组件中绑定this上下文的效果

    在react中使用原生事件

react提供了完备的生命周期方法,其中componentDidMount会在组件已经完成挂载并在浏览器中以真是存在真实dom后调用,此时我们就可以完成原生事件的绑定。

import React from 'react'

class NativeEventDemo extends React.Component{
    componentDidMount(){
        this.button.addEventListener('click',e=>{
            this.handleClick(e)
        })
    }

    handleClick(e){
        console.log(e)
    }
    
    componentWillUnmount(){
        this.button.removeEventListener('click')
    }
    
    render(){
        return <button ref={btn=>this.button=btn}>test</button>
    }
}

在react中使用dom原生事件时,一定要在组件卸载时手动移除,否则很可能出现内存泄漏的问题。而是用合成事件系统时则不需要。

    对比react合成事件与js原生事件

  1. 事件传播与阻止事件传播
    浏览器原生dom事件的传播分为3个阶段:事件捕获阶段、目标对象本身的事件处理程序调用以及事件冒泡。事件捕获会优先调用结构树最外层的元素上绑定的事件监听器,然后依次向内调用,一直调用到目标元素上的事件监听器为止。可以在将e.addEventListenr()的第三个参数设置为true时,为元素e注册捕获事件处理程序,并且在事件传播第一个阶段调用。事件捕获在低于ie9版本浏览器中无法使用。而事件冒泡则与事件捕获相反,它会从目标元素向外传播事件,由内而外直到最外层。
    react的合成事件并没有实现事件捕获,仅仅持此了事件冒泡机制。
    阻止原生事件传播需要使用e.preventDefault(),不过对于不知此该方法的浏览器(ie9一下),只能使用e.cancekBubble=true来阻止。react合成事件中,只需要使用e.preventDefault()即可。
  2. 事件类型
    react合成事件的事件类型是js原生事件类型的一个子集
  3. 事件绑定方式
    受dom标准影响,绑定浏览器原生事件的方式也有很多种:
    (1)直接在dom元素中绑定
    <button onclick="alert(1)">test</button>
    (2)在js中,通过为元素的事件属性赋值的方式实现绑定:
    el.onclick=e=>{console.log(e)}
    (3)通过事件监听函数来实现绑定:
    el.addEventListener('click',()=>{},false)
    el.attachEvent('onclick',()=>{})
    相比而言,react合成事件绑定方式简单很多
    <button onClick={this.handleClick}>test</button>
  4. 事件对象
    原生dom事件对象在w3c标准和ie标准下存在差异。低版本ie浏览器中,只能使用window.event来获取事件对象。而在react合成事件系统中,不存在这种兼容性问题,在事件处理函数中可以得到一个合成事件对象

表单

HTML表单元素与React中的其他DOM元素有所不同,因为表单元素生来就保留一些内部状态。

     受控组件

每当表单的状态发生变化时,都会被写入到组件的state中,这种组件在react中被称为受控组件。受控组件中,组件渲染出的状态与它的value或checked prop相对应。react通过这种方式消除了组件的局部状态,使得应用的整个状态更加可控。react官方同样推荐使用受控表单组件。总结react受控组件更新state流程:

(1)可以通过在初始state中设置表单默认值。

(2)每当表单的值发生变化时,调用onChange事件处理器

(3)事件处理器通过合成事件对象e拿到改变后的状态,并更新应用的state

(4)setState触发视图重新渲染,完成表单组件值得更新

与原生表单组件相比,受控组件模式确实复杂很多。每次表单发生变化,都会执行上面几步,这样统一了组件内部状态,使得表单状态更可靠。这也意味着我们可以在执行最后一步setState前,对表单值进行清洗和校验。

    非受控组件

简单的说,如果一个表单组件没有value props(单选按钮和复选框对应的是checked props)时,就可以称为非受控组件。相应,可以使用defaultValue和defaultChecked prop来表示组件的默认状态。

    对比受控组件和非受控组件

通过defaultValue和defaultChecked来设置表单默认值,它仅会被渲染一次,在后续渲染时并不起作用。非受控组件的状态并不会受应用状态的控制,应用中也多了局部组件状态,而受控组件的值来自于组件的state。

  1. 性能上的问题
    受控组件中,每次表单的值发生变化,都会调用一次onChange事件处理器,这确实会有一些性能上的损耗。
  2. 是否需要事件绑定
    使用受控组件最令人头疼的就是,我们需要为每个组件绑定一个change事件,并且定义一个事件处理器来同步表单值和组件状态。当然,某些情况下,也可以使用一个事件处理器来处理多个表单域:
    class Reservation extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          isGoing: true,
          numberOfGuests: 2
        };
    
        this.handleInputChange = this.handleInputChange.bind(this);
      }
    
      handleInputChange(event) {
        const target = event.target;
        const value = target.type === 'checkbox' ? target.checked : target.value;
        const name = target.name;
    
        this.setState({
          [name]: value
        });
      }
    
      render() {
        return (
          <form>
            <label>
              Is going:
              <input
                name="isGoing"
                type="checkbox"
                checked={this.state.isGoing}
                onChange={this.handleInputChange} />
            </label>
            <br />
            <label>
              Number of guests:
              <input
                name="numberOfGuests"
                type="number"
                value={this.state.numberOfGuests}
                onChange={this.handleInputChange} />
            </label>
          </form>
        );
      }
    }

     

    样式处理

react组件最终会生成html,所以你可以使用给普通html设置css一样的方法设置样式。如果给组件添加类名,为了避免命名冲突,react中需要设置className prop。也可以通过style prop来给组件设置行内样式,这里需要注意style prop需要一个对象。

组件间通信

react是以组合组件形式组织的,组件因为彼此互相独立。从传递信息内容上看,几乎所有类型信息都可以实现传递。在嵌套关系上,会有3中不同可能性:父组件向子组件通信、子组件向父组件通信和没有嵌套关系的组件之间通信

    父组件向子组件通信

父组件通过props向子组件传递需要的信息。

import React from 'react'

function ListItem({value}){
    return (<li><span>{value}</span></li>)
}

function List({list,title}){
    return (
    <div>
        <ul>
            {
                list.map((el,index)=>{
                    <ListItem key={key} value={el.text}/>
                })
            }
        </ul>
    </div>
    )
}

    子组件向父组件通信

react之前的组件开发模式,常常需要接收组件运行时的状态,这时我们常用的方法有以下两种。

  • 利用回掉函数:这是js灵活方便之处,这样就可以拿到运行时状态
  • 利用自定义事件机制:这种方法更通用,使用也更广泛。设计组件时,考虑加入事件机制往往可以达到简化组件API目的
    import React from 'react'
    
    class ListItem extends React.Component{
        static defaultProps={
            text:""
            checked:false
        }
        
        render(){
            return (
                <li>
                    <input type="checked" checked={this.props.checked} 
                    onChange={this.props.onChange}/>
                      <span>{this.props.value}</span>
                </li>
            )
        }
        
    }
    
    class List extends React.Component{
        constructor(props){
            super(props)
            this.state={
                list=[{text:"sds",value:"sds",checked:false}]
            }
        }
    
        onItemChange=(el)=>{
            const {list} =this.state
            this.setState({
                list:list.map(preEl=>{
                    text:preEl.text,
                    checked:preEl.text==el.text?!preEl.checked:preEl.checked
                })
            })
        }
    
        render(){
            return <div>
                <ul>
                    {
                        this.state.list((el,index)=>( <ListItem key={index} 
                         value={el.text} checked={el.checked} 
                         onChange={()=>this.onItemChange(el)}>))
                    }
                </ul>
            </div>
        }
        
     }

     

    跨级组件通信

当需要让子组件跨级访问信息时,我们可以像之前说的方法那样向更高级别的组件层层传递props,但此时的代码显得不那么优雅,甚至有些冗余。react中,还可以使用context来实现跨级父子组件间的通信。

参考链接:https://react.docschina.org/docs/context.html

    没有嵌套关系的组件通信

没有嵌套关系的,那只能通过可以影响全局的一些机制去考虑。自定义时间机制不失为一种上佳的方法。

在处理事件的过程中需要注意,在componentDidMount事件中,如果组件挂在完成,在订阅事件;当组件卸载的时候,在componentWillUnmount事件中取消事件的订阅。

 组件间抽象

    高阶组件

高阶组件类似于高阶函数,它接受react组件作为输入,输出一个新的react组件。用通俗的解释就是,当react组件被包裹时,高阶组件会返回一个增强的react组件。

实现高阶组件的方法有如下两种:

  • 属性代理。高阶组件通过被包裹的react组件来操作props
    import React from 'react'
    const MyContainer=(WrappedComponent)=>(
        class extends React.Component{
            render(){
                return < WrappedComponent {...this.props} />
            }
        }
    )
    
    

    这样就可以通过高阶组件来传递props,这样组件就可以一层层地作为参数被调用,原始组件就具备了高阶组件对它的修饰。

  • 反向继承。高阶组件继承于被包裹的react组件
    const MyContainer=(WrappedComponent)=>(
        class extends WrappedComponent{
            render(){
                return super.render()
            }
        }
    )

    高阶组件返回的组件继承于WrappedComponent。因为被动地继承了WrappedComponent,所有的调用都会反向,这也是这种方法的又来。反向继承中,高阶组件可以使用WrappedComponent引用,这意味着它可以使用WrappedComponent的state、props、生命周期和render方法。但它不能保存完整的子组件树被解析。它有两个特点:
    (1)渲染劫持
    指的是高阶组件可以控制WrappedComponent的渲染过程,并渲染各种各样的结果。我们可以在这个过程中在任何react元素输出的结果中读取、增加、修改、删除props,或读取或修改react元素树,或条件显示元素树,又或是用样式控制包裹元素树。

    const MyContainer=(WrappedComponent)=>(
        class extends WrappedComponent{
            render(){
                if(this.props.loggedIn){
                    return super.render();
                }else{
                    return null;
                }
            }
        }
    )

    (2)控制state
    高阶组件可以读取、修改或删除WrappedComponent实例中的state,如果需要,也可以增加state。但这样做,可能会让WrappedComponent组件内部状态一团糟。大部分高阶组件都应该限制读取或增加state,尤其是后者,可以重新命名state,以防止混淆。

    const MyContainer=(WrappedComponent)=>(
        class extends WrappedComponent{
            render(){
                return (
                    <div>
                        <p>props</p><pre>{JSON.stringify(this.props)}</pre>
                        <p>state</p><pre>{JSON.stringify(this.state)}</pre>
                        {super.render()}
                    </div>
                )
            }
        }
    
    )

     

  •  组件参数
    有时,我们调用高阶组件时需要参入一些参数,这可以用非常简单的方式来实现。
    import React from 'react'
    
    function HOCFactoryFactory(...params){
        return function HOCFactory(){
           return  class HOC extends React.Component{
                render(){
                    return <WrappedComponent {...this.props} />
                    }
            }
        }
    }
    
    //使用的时候
    HOCFactoryFactory(params)(WrappedComponent )

     

组件性能优化 

影响网页性能最大的因素是浏览器的重绘(reflow)和重排版(repaint)。react背后的虚拟dom就是尽可能的减少浏览器的重绘和重排版。

    纯函数

纯函数非常方便进行方法级别的测试以及重构,可以让程序具有良好的扩展性及适应性。react在设计时带有函数式编程的基因,因为react组件本身就是纯函数,react的creacteElement方法保证了组件是纯净的,即传入指定props得到一定的虚拟dom,整个过程都是可预测的。我们可以通过查分组件为子组件,进而对组件做更细粒度的控制。这也是函数编程的魅力之一,保持纯净状态,可以让方法或组件更加专注,体积更小,更独立,更具复用性和可测试性。

    key

写动态子组件的时候,如果没有给动态子项添加key prop,则会报一个警告。key是用来标识当前项的唯一性的props。

将key设置成序号,虽然会避免警告,但非常低效。这个key是每次用来做虚拟dom diff的,和数据信息没有关联性,相当于一个随机键,那么不论有没有相同项,更新都会重新渲染。key有一个原则,就是独一无二,且能不用遍历或随机值就不用,除非列表内容也并不是唯一的表示,且没有可以相匹配的属性。当key相同时,react会只渲染第一个相同key的项。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值