React

98 阅读10分钟

React的特点

1. 声明式编码

2. 组件化编码

3. React Native 编写原生应用

4. 高效(优秀的Diffing算法)

React高效的原因

1. 使用虚拟(virtual)DOM, 不总是直接操作页面真实DOM。

2. DOM Diffing算法, 最小化页面重绘。

React的基本使用

在html文件中要想写react的代码,需要引入三个js文件,react.js react-dob.js,babel.min.js

1. react.js:React核心库。

2. react-dom.js:提供操作DOM的react扩展库。

3. babel.min.js:解析JSX语法代码转为JS代码的库。

创建虚拟DOM的两种方式

js方式 和 jsx方式

//jsx方式
<script type="text/javascript">
    const VDOM = React.createElement('h1',{id:'title'},React.createElement('span',null,'hello,react'))
    ReactDOM.render(VDOM,document.getElementById('root'))
</script>
//jsx方式
<script type="text/babel">
    const VDOM = (
        <h1 id="title">
            <span>hello,react</span>
        </h1>
    )
    ReactDOM.render(VDOM,document.getElementById('root'))
</script>
JSX 语法规则
1、定义虚拟dom时,不要写引号
2、标签内混入js表达式时 要用{}
3、样式的类名指定不要用class 要用className
4、内联样式要用 style={{key:value}}的形式去写
5、标签必须闭合
6、只能有一个根标签
7、标签首字母:
   (1)、如果是小写,则会将标签转成html的同元素,如果没有这个元素,会报错
   (2)、如果是大写,就会被认为是react定义的组件,如果没被定义,会报错

React中创建组件

创建组件可以分为两种方式

1)函数式组件

<script type="text/babel">
    // 创建函数式组件
    function MyComponent(){
        console.log(this) // 此处的 this 是 undefined,因为babel编译后开启了严格模式   
        return <h1>我是函数式定义的组件</h1>
    }
    ReactDOM.render(<MyComponent/>,document.getElementById('root'))
    /**
     * ReactDOM.render(<MyComponent/>....之后发生了什么?
     * 1.React解析组件标签,找到MyComponent组件
     * 2.发现组件是函数式申明的,随后调用该函数,将返回的虚拟dom转为真实dom,随后呈现在页面上
     * */
</script>

2)类式组件

<script type="text/babel">
    class MyComponent extends React.Component {
        render(){
            console.log(this) // this 就是 MyComponent的实例对象 《=》MyComponent组件实例对象
            return <h1>我是类式定义的组件</h1>
        }
    }

    ReactDOM.render(<MyComponent/>,document.getElementById('root'))

    /**
     *ReactDOM.render(<MyComponent/>....之后发生了什么?
     * 1.React解析组件标签,找到MyComponent组件
     * 2.发现组件是类式申明的,随后new出来该类的实例,并通过该实例调用原型上的 render方法
     * 3.将render返回的虚拟dom转成真实dom,最后呈现在页面上
    */
</script>

组件的三大属性

state

1. state是组件对象最重要的属性, 值是对象(可以包含多个key-value的组合)

2. 组件被称为"状态机", 通过更新组件的state来更新对应的页面显示(重新渲染组件).

props

1. 每个组件对象都会有props(properties的简写)属性

2. 组件标签的所有属性都保存在props中

<script type="text/babel">
    class Person extends React.Component{
        //对标签属性进行类型,必要性的限制
        static propTypes = {       
            name : PropTypes.string.isRequired,   //限制name必传,且为字符串
            age : PropTypes.number, //限制age为数字
            sex : PropTypes.string  //sex为字符串
        }
        // 指定默认标签属性值
        static defaultProps = {
            age:18,  //默认age = 18
            sex:'男' //默认sex = 男
        }

        render(){
            const { name,age,sex } = this.props
            return (
                <ul>
                    <li>姓名:{name} </li>    
                    <li>年龄:{age} </li>    
                    <li>性别: {sex} </li>    
                </ul>
            )
        }
    }
    ReactDOM.render(<Person name='张三' age={18} sex="男"/>,document.getElementById('root'))
</script>

ref

组件内的标签可以定义ref属性来标识自己

<script type="text/babel">
    class Input extends React.Component{
        // React.createRef调用后可以返回一个容器,该容器可以返回被ref所标识的节点,该容器是专人专用的
        myRef1 = React.createRef()
        myRef2 = React.createRef()
        submitData = () =>{
            console.log(this.myRef1.current.value);
        }
        blurData = () => {
            console.log(this.myRef2.current.value);
        }
        render(){
            return (
                <div>
                    <input ref={this.myRef1} type="text" />
                    <button onClick={this.submitData}>提交</button>
                    <input ref={this.myRef2} onBlur={this.blurData} type="text" />
                </div>
            )
        }
    }
    ReactDOM.render(<Input/>,document.getElementById('root'))
</script>

组件的生命周期

旧版本

2_react生命周期(旧).png

/* 
1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
    1.	constructor()
    2.	componentWillMount()
    3.	render()
    4.	componentDidMount() =====> 常用
	一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
2. 更新阶段: 由组件内部this.setSate()或父组件render触发
    1.	shouldComponentUpdate()
    2.	componentWillUpdate()
    3.	render() =====> 必须使用的一个
    4.	componentDidUpdate()
3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
    1.	componentWillUnmount()  =====> 常用
	一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
*/

   // 组件即将要挂在的钩子
   componentWillMount(){
     console.log('componentWillMount');
   }
    // 组件挂载完毕的钩子
   componentDidMount(){
    console.log('componentDidMount');
   }
    //组件即将要卸载的钩子
   componentWillUnmount(){
    console.log('componentWillUnmount');
   }
    //控制组件更新的“阀门”,必须有返回值,true代表继续,false则终止,一旦false就不会执行后面的钩子函数
   shouldComponentUpdate(){
     console.log('shouldComponentUpdate');
     return true
   }
    //组件将要更新的钩子
   componentWillUpdate(){
    console.log('componentWillUpdate');
   }
    //组件更新完毕的钩子
   componentDidUpdate(){
    console.log('componentDidUpdate');
   }
   

新版本

3_react生命周期(新).png

	/* 
1. 初始化阶段: 由ReactDOM.render()触发---初次渲染
    1.	constructor()
    2.	getDerivedStateFromProps 
    3.	render()
    4.	componentDidMount() =====> 常用
	一般在这个钩子中做一些初始化的事,例如:开启定时器、发送网络请求、订阅消息
2. 更新阶段: 由组件内部this.setSate()或父组件重新render触发
    1.	getDerivedStateFromProps
    2.	shouldComponentUpdate()
    3.	render()
    4.	getSnapshotBeforeUpdate
    5.	componentDidUpdate()
3. 卸载组件: 由ReactDOM.unmountComponentAtNode()触发
    1.	componentWillUnmount()  =====> 常用
	一般在这个钩子中做一些收尾的事,例如:关闭定时器、取消订阅消息
*/
//若state的值在任何时候都取决于props,那么可以使用getDerivedStateFromProps

static getDerivedStateFromProps(props,state){
    console.log('getDerivedStateFromProps',props,state);
    return null
}

//在更新之前获取快照   
getSnapshotBeforeUpdate(){
    console.log('getSnapshotBeforeUpdate');
    return 'atguigu'
}

//组件挂载完毕的钩子
componentDidMount(){
    console.log('Count---componentDidMount');
}

//组件将要卸载的钩子
componentWillUnmount(){
    console.log('Count---componentWillUnmount');
}

//控制组件更新的“阀门”
shouldComponentUpdate(){
    console.log('Count---shouldComponentUpdate');
    return true
}

//组件更新完毕的钩子
componentDidUpdate(preProps,preState,snapshotValue){
    console.log('Count---componentDidUpdate',preProps,preState,snapshotValue);
}

getSnapshotBeforeUpdate指的是数据更新之前的数据 类似于 vue中的 beforeUpdate 使用场景:

 class List extends React.Component{
      state = {
        list:[]
      }

      ulRef = React.createRef()

      componentDidMount(){
        this.timer = setInterval(()=>{
          let {list} = this.state
          let newList = '新闻'+ (list.length+1)
          this.setState({
            list : [newList,...list]
          })
        },500)
      }
    //这里return的数据 将作为更新前的数据  传递给 componentDidUpdate  
      getSnapshotBeforeUpdate(){
        return (this.ulRef.current.scrollHeight)
      }
    // preProps :更新前的 props 
    // PreState : 更新前的 state
    // height :getSnapshotBeforeUpdate 传过来的指
      componentDidUpdate(preProps,PreState,height){
        this.ulRef.current.scrollTop += this.ulRef.current.scrollHeight - height
      }

      componentWillUnmount(){
        clearInterval(this.timer)
        this.setState({
          timer:null
        })
      }
      render(){
        return(
          <ul className="ul_box" ref={this.ulRef}>
            {
              this.state.list.map((n,index)=>{
                return <li className="item" key={index}>{n}</li>  
              })
            }
          </ul>
        )
      }
    }
    ReactDOM.render(<List/>,document.getElementById('root'))

react中的 diff算法

虚拟DOM

写前端的人都知道 React框架 采用的是虚拟 DOM。而虚拟 DOM 就是 js 对象到真实 DOM 的一种映射。

<ul id='list'>
    <li class='item'>Item 1</li> 
</ul>
  
{
  tagName: 'ul',
  props: {
    id: 'list'
  },
  children: [
    {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
  ]
}

从上述案例中可以看到,真实 DOM 映射出来的 js 对象(虚拟 DOM)是一种树形结构。其中 tagName 是 string 类型,节点的标签名称。props 是 object 类型,节点的属性集,包括 id,class 等。children 则是 array 类型,包含节点的子节点。某个节点的文本内容也是其子节点。譬如 Item 1 就是 li 节点的子节点。

diff算法,作用

在使用 setState 方法更新组件的时候,新旧组件会进行对比。这个对比的过程也就是 diff 算法。对比之后再对局部差异部分进行更新。这就是 diff 算法的简易过程。

diff 算法的作用就是寻找两个虚拟 DOM 节点的差异,并只针对该部分修改原生节点。而不是重新渲染整个页面,以此来提高页面内容更新速度。

传统 diff 算法其时间复杂度最优解是 O(n^3),那么如果有 1000 个节点,则一次 diff 就将进行 10 亿次比较,这显然无法达到高性能的要求。而 React 通过大胆的假设,并基于假设提出相关策略,成功的将 O(n^3) 复杂度的问题转化为 O(n) 复杂度的问题。

(1)两个假设

为了优化 diff 算法,React 提出了两个假设:

  1. 两个不同类型的元素会产生出不同的树
  2. 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定

(2)三个策略

基于这上述两个假设,React 针对性的提出了三个策略以对 diff 算法进行优化:

  1. Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计
  2. 拥有相同类型的两个组件将会生成相似的树形结构,拥有不同类型的两个组件将会生成不同树形结构
  3. 对于同一层级的一组子节点,它们可以通过唯一 key 进行区分

(3)diff 具体优化

基于上述三个策略,React 分别对以下三个部分进行了 diff 算法优化

  • tree diff
  • component diff
  • element diff

tree diff ------- 只对虚拟 DOM 树进行分层比较,不考虑节点的跨层级比较。

image.png

如上图,React 通过 updateDepth 对虚拟 Dom 树进行层级控制,只会对相同颜色框内的节点进行比较,根据对比结果,进行节点的新增和删除。如此只需要遍历一次虚拟 Dom 树,就可以完成整个的对比。

如果发生了跨层级的移动操作,如下图:

image.png 通过分层比较可知,React 并不会复用 B 节点及其子节点,而是会直接删除 A 节点下的 B 节点,然后再在 C 节点下创建新的 B 节点及其子节点。因此,如果发生跨级操作,React 是不能复用已有节点,可能会导致 React 进行大量重新创建操作,这会影响性能。所以 React 官方推荐尽量避免跨层级的操作。

component diff--------React 是基于组件构建的,对于组件间的比较所采用的策略如下:

  • 如果是同类型组件,首先使用 shouldComponentUpdate()方法判断是否需要进行比较,如果返回true,继续按照 React diff 策略比较组件的虚拟 DOM 树,否则不需要比较
  • 如果是不同类型的组件,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点

image.png 如上图,虽然组件 C 和组件 H 结构相似,但类型不同,React 不会进行比较,会直接删除组件 C,创建组件 H。

从上述 component diff 策略可以知道:

  1. 对于不同类型的组件,默认不需要进行比较操作,直接重新创建。
  2. 对于同类型组件, 通过让开发人员自定义shouldComponentUpdate()方法来进行比较优化,减少组件不必要的比较。如果没有自定义,shouldComponentUpdate()方法默认返回true,默认每次组件发生数据(state & props)变化时,都会进行比较。

element diff -------- 涉及三种操作:移动、创建、删除。对于同一层级的子节点,对于是否使用 key 分别进行讨论。

对于不使用 key 的情况,如下图:

image.png React 对新老同一层级的子节点对比,发现新集合中的 B 不等于老集合中的 A,于是删除 A,创建 B,依此类推,直到删除 D,创建 C。这会使得相同的节点不能复用,出现频繁的删除和创建操作,从而影响性能。

对于使用 key 的情况,如下图:

image.png React 首先会对新集合进行遍历,通过唯一 key 来判断老集合中是否存在相同的节点,如果没有则创建,如果有的,则判断是否需要进行移动操作。并且 React 对于移动操作也采用了比较高效的算法,使用了一种顺序优化手段,这里不做详细讨论。

从上述可知,element diff 就是通过唯一 key 来进行 diff 优化,通过复用已有的节点,减少节点的删除和创建操作。

小结

React 通过大胆的假设,制定对应的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题

  • 通过分层对比策略,对 tree diff 进行算法优化
  • 通过相同类生成相似树形结构,不同类生成不同树形结构以及shouldComponentUpdate策略,对 component diff 进行算法优化
  • 通过设置唯一 key 策略,对 element diff 进行算法优化 综上,tree diff 和 component diff 是从顶层设计上降低了算法复杂度,而 element diff 则在在更加细节上做了进一步优化。

虚拟dom中key的作用

1、简单说:key是虚拟dom对象的标识,在更新显示时key起着极其重要的作用

2、详细说:当状态中的数据发生变化时,react/vue 会根据 【新数据】 生成 【新虚拟dom】,
随后react/vue会根据【新虚拟dom】 和 【旧虚拟dom】进行逐层比较,规则如下:

        a.旧虚拟dom找到了与新虚拟dom相同的 key 
        (1).若虚拟DOM中内容没变, 直接使用之前的真实DOM
        (2).若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM

        b. 旧虚拟DOM中未找到与新虚拟DOM相同的key
        根据数据创建新的真实DOM,随后渲染到到页面

    -------用index作为key可能会引发的问题:
    
        1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:
              会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。

        2. 如果结构中还包含输入类的DOM:
              会产生错误DOM更新 ==> 界面有问题。
                
        3. 注意!如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,
          仅用于渲染列表用于展示,使用index作为key是没有问题的。
  */