Web框架概述——React.js
目前,在前端Web开发中,三大热门框架为React.js,Vue.js,Angular.js 。当然,三大框架各有各的优缺点,这里就不多说了,下面我就针对前段时间所学的React框架做一下整体知识点的概述。
React是什么?
我们在用js脚本操作DOM元素时如果存在大量的dom元素操作时就很消耗浏览器性能。有个贴切的比喻:把 DOM
和 Javascript
各自想象成一个岛屿,它们之间用一个桥梁连接,每过一次桥就需要交纳 ”过桥费“,当 js 频繁操作 DOM 时,”过桥费“ 就越高昂,有没有一种途径来减少费用呢?推荐方法就是尽量少过桥(即非必须情况下),都呆在各自的岛屿上。所以 React 就创造了一个叫 虚拟DOM 的方法,它创造了虚拟dom并把它们储存起来,每当状态变化的时候就会创造新的虚拟节点和以前的进行对比,让变化的部分进行渲染。整个过程没有对dom进行获取和操作,只有一个渲染过程。所以说 React 是一种 ui框架。
React的组件化
React 的 diff 算法用在了什么地方呢?当组件更新的时候,react会创建一个新的虚拟dom树 并且会和之前存储的 dom树进比较,这个比较的过程就用到了 diff 算法
,所以组件初始化的时候是用不到的。react提出了一种假设,相同的节点具有类似的结构,而不同的节点具有不同的结构。在这种假设上进行逐层的比较,如果发现对应的节点是不同的,那就直接删除旧的节点以及它所包含的所有子节点然后替换成新的节点。如果是相同的节点,则只是进行属性的更改。
对于列表的 diff算法稍有不同,因为列表同程具有相同的结构,在对列表节点进行删除、插入、排序的时候,单个节点的整体操作远比一个个对比一个个替换要好很多,所以在创建列表的时候需要 设置key值,这样 react 才能分清楚具体哪一个节点。当然不写 key值也没有错误(会有警告提示),会提示加上key值以提高 react 的性能。
diff算法图示:
1 class Index extends React.Component{ 2 constructor(props){ 3 super(props) 4 this.state = { // 初始状态 5 productList:[] // 商品列表 6 params:'', // 传入的参数 7 } 8 } 9 }
当我们再使用 <Index /> 组件时,其实是对 Index 类的实例化 — new Index,只不过 react 对这个过程进行了封装,让它看起来更像是一个标签。
注:1.定义的类名的首字母必须大写;2.因为 class 变成了关键字,所以在添加类名的时候不能用 class,需要使用 className 代替;3.类和模块内部默认使用 严格模式
,所以不需要使用 use strict 指定运行模式。
组件的生命周期函数
在 react 中经常会使用到生命周期函数,下面就列举了一些可能会用到的生命周期函数:
组件在初始化时会触发的5个钩子函数:
-
getDefaultProps()
设置默认的 props,也可以用 defaultProps 设置组件的默认属性
-
getInitalState()
在使用 es6 的 class 语法时是没有这个钩子函数的,可以直接在 constructor 中定义 this.state。此时可以访问 this.props
-
componentWillMount()
组件初始化时调用,以后组件更新不调用,整个生命周期只调用一次,此时可以修改 state
-
render()
react 中最重要的步骤,创建虚拟dom,进行diff算法,更新(渲染)dom树都在该函数中进行,此时就不能更改 state了
-
componentDidMount()
组件渲染之后调用,可以通过 this.getDOMNode()获取和操作 dom 节点,只调用一次
在更新时也会触发的5个钩子函数:
-
componentWillReceiveProps(nextProps)
组件初始化时不调用,在接收新的 props 时调用
-
shouldComponentUpdate(nextProps,nextState)
react 性能优化非常重要的一环,在组件接收新的 state 或者 props 时调用,我们可以设置在此对比前后两个 props 和 state 是否相同,如果相同则返回 false 阻止更新,因为相同的属性状态一定会生成相同的 dom树,这样就不需要创造新的dom树和旧的dom树进行 diff算法对比,从而节省大量性能,尤其是在 dom结构复杂的时候。(该函数存在返回值)
-
componentWillUpdate(nextProps,nextState)
组件初始化时不调用,只有在组件将要更新时才调用,此时可以修改 state
-
render()
和第一阶段 render 函数相同
-
componentDidUpdate()
组件初始化时不调用,组件更新完成后调用,此时可以获取 dom节点
在组件卸载时会触发的1个钩子函数:
-
componentWillUnmount()
组件将要被卸载时调用,一些事件监听和定时器需要在此清除
以上可以看出来 react 在整个生命周期中存在 10个周期函数(render重复一次),这10个函数可以满足我们对所有组件操作的需求,利用的好可以提高开发效率和组件性能
React-Router路由
Router 就是 React 的一个组件,它并不会渲染,只是一个创建内部路由规则的配置对象,根据匹配的路由地址展现相应的组件。Route 则对路由地址和组件进行绑定,Route 具有嵌套功能,表示路由地址的包含关系(多级路由),这和组件之间的嵌套并没有直接联系。Route可以绑定的组件传递七个属性:children,history,location,params,route,routeParams,routes,每个属性都包含路由的相关信息。比较常用的有children(以路由的包含关系来区分的组件),location(包括地址,参数,地址切换方式,key值,hash值)。react-router 提供 link 标签,这只是对 a 标签的封装,注:点击链接跳转的并不是默认的方式,react-router阻止了a标签的默认行为并用pushState进行hash值的转变。切换页面的过程是在点击Link标签或者后退前进按钮时,会先发生url地址的转变,Route监听到地址的改变根据Route的path属性匹配到对应的组件,将state值改成对应的组件并调用setState触发render函数重新渲染dom。
当页面比较多时,项目就会变得越来越大,尤其是对于单页面应用来说,初次渲染的速度就会很慢(需要加载大量资源),这时候就需要按需加载,只有切换页面的时候才去加载对应的js文件。react配合webpack(react-cli)进行按需加载的方法很简单,Route的Component改为getComponent,组件用require.ensure的方式获取,并在webpack中配置chunkFilename。
1 const chooseProducts = ( loaction,cb )=>{ 2 require.ensure( [],require =>{ 3 cb(null,require('../Component/chooseProducts').default) 4 },'chooseProducts' ) 5 } 6 const helpCenter = ( loaction,cb )=>{ 7 require.ensure( [],require =>{ 8 cb(null,require('../Component/helpCenter').default) 9 },'helpCenter' ) 10 } 11 const saleRecord = ( loaction,cb )=>{ 12 require.ensure( [],require =>{ 13 cb(null,require('../Component/saleRecord').default) 14 },'saleRecord' ) 15 } 16 const RouteConfig = { 17 <Router history = {history}> 18 <Route path = '/' component = {Roots}> 19 <Route path = 'index' component = {index} /> 20 <Route path = 'helpCenter' component = {helpCenter} /> 21 <Route path ='saleRecord' component = {saleRecord} /> 22 <Redirect from ='*' to = '/' /> // 路由重定向 23 </Route> 24 </Router> 25 }
react 推崇的是单向数据流
,自上而下进行数据的传递,但是由下而上或者不在一条数据流上的组件之间的通信就会变的很复杂。解决通信问题的方法很多,如果只是父子级关系,父级可以将一个需要传递的值当做一个属性传递给子级,子级通过this.props
获取父级传来的值。
组件层级嵌套到比较深,可以使用上下文getChildContext
来传递信息,这样不需要将函数一层一层往下传,任何一层都可以通过this.context
直接访问。
兄弟关系的组件之间无法直接通信,它们只能利用同一层的上级(父级)作为中间件进行传递。而如果兄弟组件都是最高层的组件,为了能够让它们通信,必须在它们外层再嵌套一层组件,这个外层的组件起着保存数据,传递信息的作用,这其实就是redux所做的事情。
组件之间的信息还可以通过全局事件来传递。不同页面可以通过参数传递数据,下个页面可以用location.param
来获取。这就是react中的核心知识点
首先,声明一下,redux并不是项目开发所必须的,它的作用相当于在顶层组件之上又加了一个组件,作用就是进行逻辑运算、储存数据和实现组件尤其是顶层组件的通信。如果组件之间的通信不多,逻辑不复杂,只是单纯的进行视图渲染,这时候用回调,context就足够了,没必要使用redux,用了反而影响开发效率。但是如果组件通信特别多,逻辑也很复杂,那使用redux就方便多了。
先来说一下react和redux是怎么配合的。react-redux提供了connect
和Provider
两个方法,它们一个将组件与redux
关联起来,一个将store
传给组件。组件通过dispatch发出 action,store根据 action 的type属性调用对应的reducer
并传入 state 和这个 action,reducer对 state 进行处理并返回一个新的 state 放入 store,connect监听到 store 发生变化,调用 setState 更新组件,此时组件的props
也就跟着变化。
流程图:
注:connect,Provider,mapStateToProps,mapDispatchToProps是 react-redux提供的,redux本身和react没有关系,它只是数据处理中心,没有和react产生任何耦合,是react-redux让它们联系在一起的。
再具体分析 react 和 react-redux 的实现过程:
先具体介绍Redux
redux主要由三部分组成:store
,reducer
,action
store
是一个对象,它有四个主要的方法:
-
dispatch
用于action的分支—在createStore中可以用 middleware 中间件对 dispatch 进行改造,比如当 action 传入 dispatch会立即触发 reducer,有些时候我们不希望它立即触发,而是等异步操作完成之后再触发,这时候用 redux-thunk 对 dispatch进行改造,以前只能传入一个对象,改造完成之后可以传入一个函数,在这个函数里可以手动 dispatch 一个 action 对象,这个过程是可控的,就实现了异步。
-
subscribe
监听 state 的变化—这个函数在store调用dispatch时会注册一个listener监听state变化,当我们需要知道state是否变化时可以调用,它返回一个函数,调用这个返回的函数可以注销监听。
let unsubsrible = store.subscrible(()=>{
console.log('state发生了变化')
}) -
getState
获取store中的state—当我们用action触发reducer改变了state时,需要再拿到新的state数据,毕竟数据才是我们想要的。getState主要在两个地方需要用到,一是在dispatch拿到action后store需要用它来获取state里的数据,并把这个数据传给reducer,这个过程是自动执行的,二是在我们利用subscribe监听state发生变化后调用它来获取新的state数据,做到这一步,说明我们已经成功了。
-
replaceReducer
替换reducer,改变state修改的逻辑。
store可以通过createStore()
方法创建,接收三个参数,经过combineReducers合并的reducer和state的初始状态一级改变dispatch的中间件,后两个参数并不是必须的。store的主要作用是将action和reducer联系起来并改变state。
action
:
action是一个对象,其中type属性是必须的,同时可以传入一些数据。action可以用actionCreator
进行创造。dispatch就是把action对象发送出去。
reducer
:
reducer是一个函数,它接收一个state和一个action,根据action的type返回一个新的state。根据业务逻辑可以分为很多个reducer,然后通过combineReducers将它们合并,state树中有很多对象,每隔state对象对应一个reducer,state对象的名字可以在合并时定义。如下:
1 const reducer = combineReducers({ 2 a:doSomethingWithA, 3 b:processB, 4 c:c 5 })
combineReducers
:
其实它也是一个reducer,它接收整个state和一个action,然后将整个state拆分发送给对应的reducer进行处理,所有的reducer会接收到相同的action,不过它们会根据action的type进行判断,有这type就进行处理然后返回新的state,没有就返回默认值,然后这些分散的state又会整合在一起返回一个新的state树。
总结一下整体的流程:首先调用store.dispatch将action作为参数传入,同时使用getState获取当前的状态树state并注册subscribe的listener监听state的变化,再调用combineReducers并将获取的state和action传入。combineReducers会将传入的state和action传给所有reducer,并根据action的type返回新的state,触发state树的更新,我们调用subscribe监听到state发生变化后用getState获取新的state数据。
注:redux的state 和 react的state完全没有关系,名字一样而已。
再介绍React-Redux
如果只使用redux,那么流程是这样:
component --> dispatch(action) --> reducer --> subscrilbe --> getState --> component
用了reac-redux之后的流程为:
component --> actionCreator(data) --> reducer --> component
store的三大功能:dispatch,subscribe和getState都不需要手动来写了,react-redux帮我们做了这些,同时它提供了两个方法:Provider和connect。
Provider是一个组件,它接收store作为props,然后通过context往下传,这样react中任何组件都可以通过context获取store。也就意味着我们可以在任何一个组件里利用dispatch(action)
来触发reducr改变state,并用subscribe监听state的变化,然后用getState获取变化后的值。但是并不推荐这样做,它会让数据流变的婚恋,过度的耦合也会影响组件的复用,维护起来也更麻烦。
connect是一个函数,connect(mapStateToProps,mapDispatchToProps,mergeProps,options)
接收四个参数并且再返回一个函数wrapWithConnect(component)
,该函数接收一个组件作为参数,其内部定义一个新组件Connect(容器组件)并将传入的组件(ui组件)作为Connect的子组件然后return出去。
完整写法:connect(mapStateToProps,mapDispatchToProps,mergeProps,options)(component)
mapStateToProps(state,[ownProps])
:
mapStateToProps接收两个参数,store的state和自定义的props,并返回一个对象,这个对象会作为props的一部分传入ui组件。我们可以根据组件所需要的数据自定义返回一个对象。ownProps的变化也会触发mapStateToProps
1 function mapStateToProps(state){ 2 return { todos:state.todos } 3 }
mapDispatchToProps(dispatch,[ownProps])
:
mapDispatchToProps如果是对象,那么会和store绑定作为props的一部分传入ui组件。如果是个函数,它接收两个参数,bindActionCreators会将action和dispatch绑定并返回一个对象,这个对象会和ownProps的一部分传入ui组件。所有不论是对象还是函数,它最终都会返回一个对象,如果是函数,这个对象的key值时可以自定义的
1 function mapDispatchToProps(dispatch){ 2 return { 3 todoActions: bindActionCreators(todoActionCreators,dispatch), 4 counterActions: bindActionCreators(counterActionCreators,dispatch) 5 }; 6 }
mapDispatchToProps返回的对象其属性其实就是一个个actionCreator,因为已经和dispatch绑定,所以当调用actionCreator时会立即发送action,而不同手动dispatch。ownProps的变化也会触发mapDispatchToProps。
mergeProps(stateProps,dispatchProps,ownProps)
:
将mapStateToProps() 与 mapDispatchToProps() 返回的对象和组件自身的props合并成心的props并传入组件。默认返回 Object.assign( {},ownProps,stateProps,dispatchProps )
的结果
options
:
pure = true 表示 Connect容器组件将在shouldComponentUpdate
中对store的state和ownProps进行浅对比,判断是否发生变化,优化性能。为false则不对比。
其实connect函数并没有做什么,大部分的逻辑都是在它返回的wrapWithConnect函数内实现的,确切的说是在wrapWithConnect内定义的Connect组件里实现的。
复杂版:
-
Provider组件接受redux的store作为props,然后通过context往下传
-
connect函数在初始化的时候会将mapDispatchToProps对象绑定到store,如果mapDispatchToProps是函数则在Connect组件获得store后,根据传入的store.dispatch和action通过bindActionCreators进行绑定,再将返回的对象绑定到store,connect函数会返回一个wrapWithConnect函数,同时wrapWithConnect会被调用且传入一个ui组件,wrapWithConnect内部使用class Connect extends Component定义了一个Connect组件,传入的ui组件就是Connect的子组件,然后Connect组件会通过context获得store,并通过store.getState获得完整的state对象,将state传入mapStateToProps返回stateProps对象、mapDispatchToProps对象或mapDispatchToProps函数会返回一个dispatchProps对象,stateProps、dispatchProps以及Connect组件的props三者通过Object.assign(),或者mergeProps合并为props传入ui组件。然后在ComponentDidMount中调用store.subscribe,注册了一个回调函数handleChange监听state的变化;
-
简化版:
-
Provider组件接受redux的store作为props,然后通过context往下传
-
connect函数收到Provider传出的store,然后接受三个参数mapStateToProps,mapDispatchToProps和组件,并将state和actionCreator以props传入组件,这时组件就可以调用actionCreator函数来触发reducer函数返回新的state,connect监听到state变化调用setState更新组件并将新的state传入组件
1 connect(state => state,action)(Component)
以上就是我对整个React框架的知识体系做出的简单整理,希望对大家有所帮助(Tips:有错误望指出,谢谢!!!)