JSX 语法的转化过程 (了解)
演示 : babel中文网试一试 let h1 =
- JSX 仅仅是createElement() 方法的语法糖 (简化语法)
- JSX 语法 被 @babel/preset-react 插件编译为 createElement() 方法
- React 元素:是一个对象,用来描述你希望在屏幕上看到的内容
- React 元素 最后 被
ReactDOM.render(<Child/>,document.getElementById('root'))
渲染显示到页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9hqEG5rw-1596329786917)(C:/Users/wangyu123/Desktop/新建文件夹/面试题/md-imgs/jsx.png)]
- 演示:
render () {const el = <h1 className='greeting'>Hello JSX</h1>console.log(el);return el
}react更新机制
简化 : 虚拟DOM
然后呢??
1. jsx => 虚拟DOM => 真实的DOM
2. jsx数据发生变化 => 新的虚拟DOM
3. 新的虚拟DOM 和 旧的虚拟DOM对比 => 通过Diff算法找到有差异的地方 => 更新1步骤的真实的DOM
组件更新机制
- setState 的两个作用
- 修改state
- 重新调用render , 更新组件(UI)
- 过程 : 父组件重新渲染时, 也会重新染当前组件子树
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l4SpZboe-1596329786920)(C:/Users/wangyu123/Desktop/新建文件夹/面试题/md-imgs/更新机制.png)]
演示代码 :
- App > P1+P2> C1+C2
//2. 类组件
class App extends React.Component {state = {}render() {return (<div style={{ background:'pink', display:'flex' }}><P1></P1><P2></P2></div>)}
}
class P1 extends React.Component {render() {return (<div style={{ background:'red',flex:1 }}><p>P1</p></div>)}
}
class P2 extends React.Component {state = {name :'zs'}render() {return (<div style={{ background: 'skyblue',flex:1 }}><p onClick={this.handle}>P2- { this.state.name }</p><C1></C1><C2></C2></div>)}handle = () => { this.setState({name : 'ls'})}
}
class C1 extends React.Component {render () {console.log('child1 更新了');return (<div style={{background:'yellow' }}><p>child1</p></div>)}
}
class C2 extends React.Component {render () {console.log('child2 更新了');return (<div style={{background:'lime' }}><p>child2</p></div>)}
}
- 这样效果是出来了,但是它确实有很严重的性能问题, 因为子组件都没有任何变化,如果重新渲染,那么就会重新调用render() 渲染页面, 有损性能
- 所以需要进行处理 组件性能优化
组件性能优化
优化1: 减轻 state
- 原则 : state 中 只存储跟组件渲染相关的数据 (比如 : count / 列表数据 等)
- 不用做渲染的数据 不要放在 state 中, (比如 定时器 id )
// 定时器的 timerId 就 不需要放到state 中,, 只是用来清除 定时器, 和渲染无关componentDidMount() {// timerId存储到this中,而不是state中this.timerId = setInterval(() => {}, 2000)}componentWillUnmount() {clearInterval(this.timerId) }
优化2 : 避免不必要的重新渲染
- 组件更新机制 : 父组件更新会引起子组件也被更新
- 问题 : 子组件没有任何变化时, 也会重新渲染
- 如何避免不必要的重新渲染呢 ?
- 解决方式 : 使用 钩子函数` shouldComponentUpdate(nextProps, nextState)
- nextProps : 最新的属性
- nextState : 最新的状态
- 场景 : 比较更新前后的 state 或者 props 是否相同, 来决定是否 更新组件
- 作用 : 通过返回值 决定该组件是否需要重新渲染, 返回true ,表示重新渲染, false 表示不重新渲染
- 触发时机 : 更新阶段的钩子函数, 组件重新渲染 前 执行
- 顺序 : shouldComponentUpdate() ==> render() ==> componentDidMount()
- 演示 : 点击父组件的计算器的数据count
// 演示1 : 子组件里 通过 shouldComponentUpdate() { return true or false } - true-更新- false-不更新// 演示2 : 获取 父传过来的属性, 奇数更新,偶数不更新, shouldComponentUpdate(nextProps,nextState)- nextProps : 最新的属性- nextState : 最新的状态- if(nextState.count % 2 == 0) {return false }else { return true}// 演示3 : 就一个组件里有number 值, 点击产生随机值, 比较前后生成的随机值是否一致,不一致就更细, 一致就不需要更新
// count : Math.floor(Math.random()*3)shouldComponentUpdate(nextProps,nextState) {// 上一个的值 最新的值console.log(this.state.count === nextState.count)// 本次的 nextState.number// 上一次的 this.state.number// 或者 通过 if..else..判断 return this.state.count !== nextState.count}
优化3 : 纯组件 - PureComponent
- 纯组件
- 作用 : 自动实现了 shouldComponentUpdate() 钩子函数, 不需要再手动对比更新前后的props 或者 state , 来阻止不必要的更新了
- 原理 : PureComponent 内部, 会别对比更新前后的props 以及更新前后的state , 只要有一个不同, 就会让组件更新, 只有在两者都相同的情况下, 才会阻止组件更新
class Hello extends React.PureComponent {}
// 把那个方法 shouldComponentUpdate() 删除掉, 已经 PureComponent已经封装好了
PureComponent 内部原理
参考 : API Reference => React => React.PureComponent
-
PureComponent
-
说明 : PureComponent 内部会比较更新前后的 props 和 state 分别进行浅对比
-
对于简单/值类型来说, 比较两个值是否相同 (直接赋值即可, 没有坑)
// 将 React.Component 替换为: React.PureComponent
class Hello extends React.PureComponent {state = {// number 就是一个普通的值类型的数据number: 0}handleClick = () => {const newNumber = Math.floor(Math.random() * 3)console.log(newNum);// 对于 值类型 来说,没有任何坑,直接使用即可// 更新前的 number:0// 更新后的 number:2// PureComponent 内部会进行如下对比:// 更新前的number === 更新后的number// 如果这两个值相同,内部,相当于在 shouldComponentUpdate 钩子函数中返回 false ,阻止组件更新。// 如果这两个值不同,内部,相当于在 shouldComponentUpdate 钩子函数中返回 true ,更新组件。this.setState({number: newNumber})}render() {return (<div><h1>随机数:{this.state.number}</h1><button onClick={this.handleClick}>随机生成</button></div>)}
}
- 对于引用类型来说, 只比较对象的引用(地址) 是否相同
- 造成的结果 : 对象里 的数据变化,不更新
const { obj } = this.statelet newNum = Math.floor(Math.random() * 3)
console.log(newNum);// 虽然value 值 也可以从 react-dev-tools 里调试 value值确实都变了,但是 obj 一直没有变
obj.value = newNumthis.setState({obj
})
- 正确做法 : 根据现有状态生成一个新对象, 然后再更新状态
const newObj = {...this.state.obj} // 创建一个新对象
newObj.value = Math.floor(Math.random() * 3)
this.setState({obj: newObj
})
- 正确做法说明:
- 在 PureComponent 中 使用引用类型 的状态时, 应该每次都创建一个新的状态, 而不是直接修改当前状态
- 因为PureComponent 是浅对比, 所以,如果直接修改当前对象中的属性, 会造成: 对象中的值变了, 但是引用地址没有改变, 而导致组件不会被更新, 这样的话就出现bug了
- 注意 , 在 React 中, ( 不管是PureComponent 还是 Component ) , 都不要直接修改引用类型的状态值, 而是要创建一个新的状态, 修改新的状态,然后再更新
在 React 组件 中更新应用类型的状态
- 文档 : 不可变数据的力量
- 注意 : 对于引用类型的状态来说, 应该创建新的状态, 而不要直接修改当前状态
- 原则 : 状态不可变!!! 数据不要变, 直接创建新的
- 对象状态 :
// 对象
state = {obj : {value : 123}
}//ES5
// 新加 之前的值 修改内容
const newObj = Object.assign( {}, this.state.obj, {value : '新的值'} )
this.setState({obj:newObj
})
// es6
this.setState({obj : {...this.state.obj, value: '新的值'}
})
- 数组状态
// 数组:
state = {list: ['a', 'b']
}// ES5:
this.setState({list: this.state.list.concat([ 'c' ]) // ['a', 'b', 'c']
})// ES6:
this.setState({list: [...this.state.list, 'c']
})// 删除数组元素:[].filter()
虚拟DOM的真正价值
- 虚拟 DOM 的真正价值从来都不是性能。
- 真正的价值:虚拟DOM 能够让 React 摆脱浏览器的限制(束缚)。也就是,只要能够运行JS代码的地方,就能够运行 React。
- 跨平台
- JSX => 虚拟DOM => react-dom => DOM 元素=> 浏览器
- JSX => 虚拟DOM => React-Native => ios和安卓的元素 => 移动混合开发
- JSX => 虚拟DOM => 工具 => VR
React 组件
(state, props) => UI
路由基础
路由介绍
- 路由 : 就是一套映射规则, 是url中
哈希值
与展示视图
之间的一种对应关系 - 为什么要学习路由 ?
- 现代的前端应用大多都是 SPA(单页应用程序),也就是只有一个 HTML 页面的应用程序。
- 因为它的用户体验更好、对服务器的压力更小,所以更受欢迎。
- 为了有效的使用单个页面来管理原来多页面的功能,前端路由 应运而生。
- 使用React路由简单来说,就是配置 路径 和 组件(配对)
spa缺点1. 学习成本大 学习路由2. 不利于SEO
基本使用
- 安装 :
yarn add react-router-dom
// 1 导入路由中的三个组件
import { BrowerRouter , Link, Route } from 'react-router-dom'const Hello = () => {// 2 使用 BrowerRouter 组件包裹整个应用(才能使用路由)return (<BrowerRouter><div><h1>React路由的基本使用:</h1>{/* 3 使用 Link 组件,创建一个导航菜单(路由入口) */}<Link to="/first">页面一</Link>{/* 4 使用 Route 组件,配置路由规则以及要展示的组件(路由出口) */}<Route path="/first" component={First} /></div></BrowerRouter>)
}
常用组件的使用介绍
- 引入的三个组件
// HashRouter => 哈希模式 => 带 # => 修改的是 location.hash
import { HashRouter , Link, Route } from 'react-router-dom' //带#
// BrowserRouter => history模式 => 不带 # => 修改的是 location.pathname
import { BrowserRouter , Link, Route } from 'react-router-dom' //不带#
- BrowserRouter 组件 : 使用 Router 组件包裹整个应用 (才能使用路由)
- Link 组件 : 创建一个导航菜单 (路由入口)
- 最终会生成一个a标签, 通过 to 属性指定 pathname(history /) 或 hash(哈希模式 #)
- Route 组件 : 用来配置路由规则和要展示的组件 (路由出口)
- path : 配置路由规则
- component : 指定当前路由 规则匹配时要展示的组件
- Route 组件放在哪, 组件内容就展示在哪, 并且每一个路由都是一个单独的Route组件
路由的执行过程
- 当点击 Link 的时候,就会修改浏览器中的 pathname
- 只要 浏览器地址栏中的 pathname 发生改变,React 路由就会监听到这个改变
- React 路由监听到 pathname 改变后,就会遍历所有 Route 组件,分别使用 Route 组件中的 path 路由规则,与当前的 浏览器地址栏中的pathname进行匹配
- 只要匹配成功,就会把当前 Route 对应的组件,展示在页面中
- 注意:匹配时,不是找到第一个匹配的路由就停下来了。而是: 所有的 Route 都会进行匹配,只要匹配就会展示该组件。
- 也就是说:在一个页面中,可以有多个 Route 同时被匹配
使用 Switch 组件 ,匹配一个
{/* Switch 只会让 组件显示出来一个 */}
<Switch><Route path="/one" component={One}></Route><Route path="/two" component={Three}></Route><Route path="/two" component={Two}></Route>
</Switch>
编程式导航
-
改变入口的三种方式 :
-
手动输入
-
声明式导航 : (html)
- 编程式导航 : 通过js代码来实现的跳转/返回 (js)
-
编程式导航 :
-
可以通过props 拿到 跳转和返回的方法
-
正常的组件, 打印 props => 默认是 一个空对象 {}
-
凡是参与
路由匹配
出来的组件 , 路由都会给他们传入三个属性 history, location, match -
history : (主要用来编程式导航)
- push() 跳转到另外一个页面 push(path,state)
- goBack() 返回上一个页面
- replace() 跳转到另外一个页面
-
location : (位置路径的)
-
pathname : 路径
- state : 通过跳转传递的数据
-
match : 获取参数
- params : 可以拿到动态路由里的参数 params : {id : 123}
* - push() 跳转到另外一个页面* - goBack() 返回上一个页面* - replace() 跳转到另外一个页面** - push 和 replace 区别* - push() 跳转 - 记录访问的历史 (可逆)* - replace() 跳转 - 不记录访问的历史 (不可逆)* One : <button onClick={this.jump}>跳转到two</button> 演示* Two : <button onClick={this.back}>返回到One</button> 返回
备
HashRouter 传参的方式和 BrowserRouter 传参的方式不一样this.props.history.push({pathname: '/pay',state: {name: 'zs'}})
默认路由 - 根路径 /
- 默认路由地址为:
/
- 默认路由在进入页面的时候,就会自动匹配
{/* / 表示默认路由规则 */}
<Route path="/" component={Home} />
匹配模式
问题:当 Link组件的 to 属性值为 “/login”时,为什么 默认路由
/
也被匹配成功?
- 默认情况下,React 路由是: 模糊匹配模式
- 模糊匹配:只要 pathname 以 path 开头就会匹配成功
- path 代表Route组件的path属性
- pathname 代表Link组件的to属性(也就是url中 location.pathname)
- 精确匹配:只有当 path 和 pathname 完全匹配时才会展示该路由
- 解决办法 : 给 Route 组件添加 exact 属性,让其变为精确匹配模式
// 添加 exact 之后, 此时,该组件只能匹配 pathname=“/” 这一种情况
<Route exact path="/" component=... />// 再演示 : /one/two/three
重定向
- 需求 :
使用 重定向 '/' => '/one'
- 方式1 : render-props
<Route exact path='/'render={ () => {return <Redirect to='/one' />}
}></Route>
- 方式2 - children
<Route exact path='/'><Redirect to='/one' />
</Route>
路由两种模式的说明
哈希模式
1. 访问路径 : http://localhost:8080/#/one http://localhost:8080/#/two2. 服务器接收到的 (服务器是不会 接收 # 后面的内容的)3. 不管访问的路径是什么样的 http://localhost:8080 ==> 服务器返回的默认 的就是 index.html4. 后面的 /one 和 /two 由路由来使用, 根据路由匹配规则找到对应的组件显示
5. 哈希模式 不管是 开发阶段还是发布阶段,都是没有问题的
history 模式
1. 访问路径 : http://localhost:8080/one http://localhost:8080/two2. 服务器接收到的 http://localhost:8080/one 和 http://localhost:8080/two但是,/one 和 /two 这个路径是不需要服务器端做任何处理的。
3. http://localhost:8080/getNewshttp://localhost:8080/detail 它们都是 接口地址 , 后面遇到 类似 /one 和 /two 都会以为是接口 是要返回数据的呢?
3. 所以,应该在服务器端添加一个路由配置,直接返回 SPA 的 index.html 页面就行啦。
4. 类似处理app.get('/getNews', (req,res) => {// 根据 res 返回 对应的数据res.json { ..... }})app.get('/detail', (req,res) => {// 根据 res 返回 对应的数据res.json { ..... }})// 最后 额外再多加一个, 专门用来返回 index.htmlapp.use('*', (req,res) => {res.sendFile('index.html')})
总结 :
history模式 :
- 开发阶段 : webpack脚手架已经处理好了,
- 发布阶段 : 服务器是公司的服务器, 可能就会报错
- 我们要做的就是`告诉后台`,我们使用的 是 history模式,让他专门处理一下,就可以了
- 如果后台不给处理,或者处理不好, 我们就使用 `哈希模式`
类似 /one 和 /two 都会以为是接口 是要返回数据的呢?
3. 所以,应该在服务器端添加一个路由配置,直接返回 SPA 的 index.html 页面就行啦。
4. 类似处理
app.get(’/getNews’, (req,res) => {
// 根据 res 返回 对应的数据
res.json { … }
})
app.get(’/detail’, (req,res) => {
// 根据 res 返回 对应的数据
res.json { … }
})
// 最后 额外再多加一个, 专门用来返回 index.html
app.use('*', (req,res) => {res.sendFile('index.html')
})
#### 总结 : ```js
history模式 :
- 开发阶段 : webpack脚手架已经处理好了,
- 发布阶段 : 服务器是公司的服务器, 可能就会报错
- 我们要做的就是`告诉后台`,我们使用的 是 history模式,让他专门处理一下,就可以了
- 如果后台不给处理,或者处理不好, 我们就使用 `哈希模式`