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>
组件的生命周期
旧版本
/*
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');
}
新版本
/*
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 提出了两个假设:
- 两个不同类型的元素会产生出不同的树
- 开发者可以通过
keyprop来暗示哪些子元素在不同的渲染下能保持稳定
(2)三个策略
基于这上述两个假设,React 针对性的提出了三个策略以对 diff 算法进行优化:
- Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计
- 拥有相同类型的两个组件将会生成相似的树形结构,拥有不同类型的两个组件将会生成不同树形结构
- 对于同一层级的一组子节点,它们可以通过唯一 key 进行区分
(3)diff 具体优化
基于上述三个策略,React 分别对以下三个部分进行了 diff 算法优化
tree diffcomponent diffelement diff
tree diff------- 只对虚拟 DOM 树进行分层比较,不考虑节点的跨层级比较。
如上图,React 通过 updateDepth 对虚拟 Dom 树进行层级控制,只会对相同颜色框内的节点进行比较,根据对比结果,进行节点的新增和删除。如此只需要遍历一次虚拟 Dom 树,就可以完成整个的对比。
如果发生了跨层级的移动操作,如下图:
通过分层比较可知,React 并不会复用 B 节点及其子节点,而是会直接删除 A 节点下的 B 节点,然后再在 C 节点下创建新的 B 节点及其子节点。因此,如果发生跨级操作,React 是不能复用已有节点,可能会导致 React 进行大量重新创建操作,这会影响性能。所以 React 官方推荐尽量避免跨层级的操作。
component diff--------React 是基于组件构建的,对于组件间的比较所采用的策略如下:
- 如果是同类型组件,首先使用
shouldComponentUpdate()方法判断是否需要进行比较,如果返回true,继续按照 React diff 策略比较组件的虚拟 DOM 树,否则不需要比较 - 如果是不同类型的组件,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点
如上图,虽然组件 C 和组件 H 结构相似,但类型不同,React 不会进行比较,会直接删除组件 C,创建组件 H。
从上述 component diff 策略可以知道:
- 对于不同类型的组件,默认不需要进行比较操作,直接重新创建。
- 对于同类型组件, 通过让开发人员自定义
shouldComponentUpdate()方法来进行比较优化,减少组件不必要的比较。如果没有自定义,shouldComponentUpdate()方法默认返回true,默认每次组件发生数据(state & props)变化时,都会进行比较。
element diff-------- 涉及三种操作:移动、创建、删除。对于同一层级的子节点,对于是否使用 key 分别进行讨论。
对于不使用 key 的情况,如下图:
React 对新老同一层级的子节点对比,发现新集合中的 B 不等于老集合中的 A,于是删除 A,创建 B,依此类推,直到删除 D,创建 C。这会使得相同的节点不能复用,出现频繁的删除和创建操作,从而影响性能。
对于使用 key 的情况,如下图:
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是没有问题的。
*/