前端笔记之React(四)生命周期&Virtual DOM和Diff算法&日历组件开发
一、React生命周期
一个组件从出生到消亡,在各个阶段React提供给我们调用的接口,就是生命周期。
生命周期这个东西,必须有项目,才知道他们干嘛的。
1.1 Mouting阶段【装载过程】
这个阶段在组件上树的时候发生,依次是:
constructor(props) 构造函数 作用:初始化state值,此时可访问props、发Ajax请求
componentWillMount() 组件将要上树 作用:常用于根组件中的引用程序配置,不能做任何涉及DOM的事情完成一些计算工作
render() 渲染组件 作用:创建虚拟DOM,组建的UI样式
componentDidMount() 组件已经上树 作用:启动AJAX调用,加载组件的数据,还能用ref得到DOM,添加事件监听
App.js:
import React from "react"; import Child from "./Child.js"; export default class App extends React.Component{ constructor(){ super(); this.state = { a:100, isShow:true } } render(){ return <div> <button onClick={()=>{this.setState({a:this.state.a+1})}}>改变a值</button> <button onClick={()=>{this.setState({isShow:!this.state.isShow})}}> 显示/隐藏组件 </button> { this.state.isShow ? <Child a={this.state.a}></Child> : null } </div> } };
Child组件:
import React from 'react'; export default class Child extends React.Component { constructor(){ super(); console.log("我是constructor构造函数"); this.state = { m : 200 } } //组件将要上树 componentWillMount(){ console.log("我是componentWillMount"); } //组件已经上树 componentDidMount(){ console.log("我是componentDidMount"); } render(){ console.log("我是render"); return ( <div> <button onClick={()=>{this.setState({m:this.state.m+1})}}>改变m值</button> 子组件的m值:{this.state.m}, 子组件接收a值:{this.props.a} </div> ); } }
1.2 Updating阶段【更新过程】
当组件的props改变或state改变的时候触发,依次是:
componentWillReceiveProps(nextProps)
当收到新的props的时候触发
shouldComponentUpdate(nextProps,nextState)
【门神函数】当组件state或props改变时触发,这个函数需要return true或者false,表示是否继续进Updating阶段。如return false,视图将不再更新,大致是为了增加效率。
componentWillUpdate(nextProps, nextState)
当组件state或props改变时触发,用来在update的时候进行一些准备。
render()渲染方法,创建虚拟DOM
componentDidUpdate(prevProps, prevState)
当组件state或props改变时触发,用来进行一些更新的验证。组件更新完成后调用,此时可以获取最新的DOM节点,用来验证信息的。
在Updating阶段中,绝对不允许改变state、props,否则会死循环。
1.3 unmounting阶段【卸载过程】
就一个函数:componentWillUnmount()组件将要下树。
完整的Child.js子组件:
import React from 'react'; export default class Child extends React.Component { constructor() { super(); console.log("我是constructor构造函数") this.state = { m:200 } } //组件将要上树 componentWillMount(){ console.log("我是componentWillMount将要上树") } //组件已经上树 componentDidMount(){ console.log("我是componentDidMount已经上树") } //************Updataing阶段【更新阶段】************** */ // 当组件的props或state改变时触发 componentWillReceiveProps(nextProps){ console.log("更阶段的:componentWillReceiveProps", nextProps) } shouldComponentUpdate(nextProps, nextState) { console.log("更阶段的:shouldComponentUpdate", nextProps, nextState) return true; } componentWillUpdate(nextProps, nextState){ console.log("更阶段的:componentWillUpdate", nextProps, nextState) } componentDidUpdate(prevProps, prevState){ console.log("更阶段的:componentDidUpdate", prevProps, prevState) } //组件下树 componentWillUnmount(){ console.log("componentWillUnmount组件下树了"); } render(){ console.log("我是render") return <div> <button onClick={()=>{ this.setState({m: this.state.m + 1 })}}>改变m值</button> <h2>子组件的m值:{this.state.m}</h2> <h2>子组件接收父组件a值:{this.props.a}</h2> </div> } }
上树阶段:
constructor
componentWillMount
render
componentDidMount
更新阶段:
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
下树阶段
componentWillUnmount
二、Virtual DOM和Diff算法(理论知识)
React会在内存中存储一份DOM的镜像,当有render发生的时候,此时会在内存中用Diff算法进行最小差异比较,实现最小的更新。
Virtual DOM是React、Vue中的一个很重要的概念,在日常开发中,前端工程师们需要将后台的数据呈现到界面中,同时要能对用户的操作提供反馈,作用到UI上…… 这些都离不开DOM操作。但是我们知道,频繁的DOM操作会造成极大的资源浪费,也通常是性能瓶颈的原因。于是React、Vue引入了Virtual DOM。Virtual DOM的核心就是计算比较改变前后的DOM区别,然后用最少的DOM操作语句对DOM进行操作。
现在需要将下图左边的DOM结构替换成右边的结构,这种情景在实战项目中是经常会遇到的。但是如果直接操作DOM的话,进行移除的话可能就是四次删除,五次插入,这种消耗是很大的。但是使用Virtual DOM,那就是比较两个结构的差异,发现仅仅改变了四次内容,一次插入。这种消耗就小很多,无非加上一个比较的时间。
React告诉我们的是在内存中维护一颗和页面一样的DOM树,这颗DOM树不是真正渲染在html中的,而是放在内存中的,因此修改它将会特别的快,并且资源消耗也少很多,当我们render一个页面的时候首先先将我们最新的DOM去和内存中的这棵虚拟DOM树去做对比(脏检查),然后对比出差异点,然后再用这棵虚拟DOM差异的部分去替换真正DOM树中的一部分。
这就是所谓的 Virtual DOM 算法。包括几个步骤:
用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中。
当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异。
把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了。
Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。这个概念就和我们当初学操作系统一样,可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。
App.js
import React from "react"; export default class App extends React.Component { constructor() { super(); this.state = { a : 100 } } componentDidMount(){ $(this.refs.list).find("li").css("position","relative").animate({"left":500},5000) } render(){ console.log("我是render函数") return <div> <button onClick={()=>{this.setState({a : this.state.a + 1})}}>按我</button> <ul ref="list"> <li>A</li> <li>B</li> <li>{this.state.a}</li> <li>D</li> </ul> </div> } }
当某一个组件状态、属性被更改时,它的子组件、孙组件都会被重新渲染,Virtual DOM也将会计算这些组件的DOM更新。
三、日历组件
3.1业务
日选择视图 |
年选择视图 |
月选择视图 |
|
|
|
3.2划分组件
组件划分就是根据第一直观印象即可,按结构、功能独立进行划分。
最大组件Canlendar,里面有数据year、month、date
点击一个按钮,可以显示弹出层,弹出层的组件名:Canlendar_menu
下辖五个组件,五个组件都兄弟:
ChooserDate
|
ChooserYear
|
ChooserMonth
|
PickerDate
PickerYear
|
所有的组件不需要对兄弟组件负责,只需要对最大组件的数据负责。
【先实现日历视图】
index.html
<html> <head> <title>日历组件</title> <link rel="stylesheet" href="css/style.css" /> </head> <body> <div id="app"></div> <script type="text/javascript" src="lib/jquery-2.2.4.min.js"></script> <script type="text/javascript" src="dist/bundle.js"></script> </body> </html>
创建app/components/canlendar/index.js文件,这是最大的组件。
import React from "react"; export default class Canlendar extends React.Component { constructor() { super(); } render() { return <div> 我是Canlendar组件 </div> } }
app/App.js引入canlendar最大组件
import React from "react"; import Canlendar from "./components/canlendar"; export default class App extends React.Component { constructor() { super(); } render(){ return <div> <div> 出生日期:<Canlendar></Canlendar> </div> </div> } }
开始写app/components/canlendar/index.js
import React from "react"; import CanlendarMenu from "./CanlendarMenu.js"; export default class Canlendar extends React.Component { constructor() { super(); this.state = { year : 2018 , month : 8 , date : 8, isShowMenu : false } } render() { return <div className="canlendar"> <div className="inputBox"> {this.state.year}年{this.state.month}月{this.state.date}日 </div> { this.state.isShowMenu ? <CanlendarMenu year={this.state.year} month={this.state.month} date={this.state.date} ></CanlendarMenu> : null } </div> } }
css样式:
*{margin:0;padding:0;} .canlendar{position: relative;} .canlendar .inputBox{ width: 150px;height: 20px;border: 1px solid #BDBDBD; border-radius:2px;position:relative;font-size:13px; padding:0 10px;color:#424242;line-height: 20px; } .canlendar .inputBox::before{ content:"";position: absolute;right:0;top:0; width:20px;height:20px;background: #F5F5F5; }
开始写app/components/canlendar/CanlendarMenu.js弹出层
import React from "react"; import PickerDate from "./PickerDate.js"; import ChooserDate from "./ChooserDate.js"; export default class CanlendarMenu extends React.Component { constructor() { super(); } render() { //解构得到年、月、日 const {year, month, date} = this.props; return <div className="canlendar_menu"> <PickerDate year={year} month={month}></PickerDate> <ChooserDate year={year} month={month} date={date}></ChooserDate> </div> } }
css样式:
.canlendar .canlendar_menu{ position: absolute;top: 26px;left:0;z-index: 999; width:360px; height:260px; border: 1px solid #BDBDBD; box-shadow: 0px 0px 8px #00000036; border-radius: 5px;background:white; }
app/components/canlendar/PickerDate.js
import React from "react"; export default class PickerDate extends React.Component { constructor() { super(); } render() { const {year, month} = this.props; return <div className="picker"> <div className="left"> <a className="btn" href="###">上1月</a> </div> <div className="center"> <a href="###">{year}</a>年 <a href="###">{month}</a>月 </div> <div className="right"> <a className="btn" href="###">下1月 </a> </div> </div> } }
css样式:
.canlendar .picker{padding-top:10px;overflow: hidden;margin-bottom: 13px;} .canlendar .picker .left{float: left;width:33.33%;text-align: center;} .canlendar .picker .right{float:left;width:33.33%;text-align: center;} .canlendar .picker .center{ float: left;width:33.33%;text-align: center;font-size: 18px;font-weight: bold; } .canlendar .picker .btn{ padding:4px 10px;background: #2196F3;font-size: 12px; border-radius: 4px;text-decoration: none;color: white; } .canlendar .chooserDate{color:#333;font-size: 12px;} .canlendar .chooserDate span{display: block;} .canlendar .chooserDate table{ width:100%;text-align:center;line-height: 14px;border-collapse:collapse; } .canlendar .chooserDate span.cd{font-size: 10px;} .canlendar .chooserDate table td{padding-bottom: 2px;cursor: pointer;} .canlendar .chooserDate table th{line-height: 26px;} .canlendar .chooserDate table td.gray{color: #c1bcbc;} .canlendar .chooserDate table td.cur{background-color: #FFCDD2;}
app/components/canlendar/ChooserDate.js
import React from "react"; export default class ChooserDate extends React.Component { constructor() { super(); } render() { return <div className="chooserDate"> <table> <tbody> <tr> <th>日</th> <th>一</th> <th>二</th> <th>三</th> <th>四</th> <th>五</th> <th>六</th> </tr> <tr> <td> <span>31</span> <span className="cd">初一</span> </td> </tr> tr*6>td*7 </tbody> </table> </div> } }
import React from "react"; import { solar2lunar } from "solarlunar"; import classnames from "classnames"; export default class ChooserDate extends React.Component { constructor() { super(); } //显示表格 showTable(){ const {year , month , date} = this.props; //三要素 var thisMonth1Day = new Date(year, month - 1 , 1).getDay(); var thisMonthDateAmount = new Date(year, month, 0).getDate(); var prevMonthDateAmount = new Date(year, month - 1, 0).getDate(); var arr = []; //上个月的尾巴 while(thisMonth1Day--){ var d = prevMonthDateAmount--; var sl = solarLunar.solar2lunar(year, month - 1, d); arr.unshift({ "d" : d, "cd": sl.term || sl.dayCn, }) } //本月 var count = 0; while (thisMonthDateAmount--){ count++; var d = count; var sl = solarLunar.solar2lunar(year, month, d); arr.push({ "d": d, "cd": sl.term || sl.dayCn, }); } //下月开头 var nextCount = 0; while(arr.length != 42){ nextCount++; var d = nextCount; var sl = solarLunar.solar2lunar(year, month + 1, d); arr.push({ "d": d, "cd": sl.term || sl.dayCn, }); } //表格上树显示 var domArr = []; for(var i = 0; i < arr.length / 7; i++){ domArr.push( <tr key={i}> { // 数组会自动展开 arr.slice(i * 7, i * 7 + 7).map((item, index)=>{ return <td key={index}> <span>{item.d}</span> <span className="cd">{item.cd}</span> </td> }) } </tr> ) } return domArr; } render() { return <div className="chooserDate"> <table> <tbody> <tr> <th>日</th> <th>一</th> <th>二</th> <th>三</th> <th>四</th> <th>五</th> <th>六</th> </tr> {this.showTable()} </tbody> </table> </div> } }
app/components/canlendar/index.js实现切换上月、下月
import React from "react"; import CanlendarMenu from "./CanlendarMenu.js"; export default class Canlendar extends React.Component { constructor() { super(); this.state = { year : 2018 , month : 8 , date : 8, isShowMenu : false } } setShowMenu(isShowMenu){ this.setState({isShowMenu}); } setYear(year){ this.setState({ year }); } setMonth(month){ this.setState({ month }); } setDate(date){ this.setState({date}); } render() { return <div className="canlendar"> <div className="inputBox" onClick={()=>{this.setState({"isShowMenu" : true})}}> {this.state.year}年{this.state.month}月{this.state.date}日 </div> { this.state.isShowMenu ? <CanlendarMenu year={this.state.year} month={this.state.month} date={this.state.date} setYear={this.setYear.bind(this)} setMonth={this.setMonth.bind(this)} setDate={this.setDate.bind(this)} setShowMenu={this.setShowMenu.bind(this)} ></CanlendarMenu> : null } </div> } }
然后通过app/components/canlendar/CanlendarMenu.js继续往下传
import React from "react"; import PickerDate from "./PickerDate.js"; import ChooserDate from "./ChooserDate.js"; export default class CanlendarMenu extends React.Component { constructor() { super(); } render() { //解构得到年、月、日 const {year, month, date, setYear, setMonth, setDate, setShowMenu} = this.props; return <div className="canlendar_menu"> <PickerDate setYear={setYear} setMonth={setMonth}></PickerDate> <ChooserDate setYear={setYear} setMonth={setMonth} setDate={setDate}></ChooserDate> </div> } }
canlender/PickerDate.js
import React from "react"; export default class PickerDate extends React.Component { constructor() { super(); } //下一月 nextMonth(){ //如果不是12月,此时月份加1 if(this.props.month != 12){ this.props.setMonth(this.props.month + 1); }else{ //如果是12月,此时月份变为1,年加1 this.props.setMonth(1); this.props.setYear(this.props.year + 1); } } //上一月 prevMonth(){ if(this.props.month != 1) { this.props.setMonth(this.props.month - 1); }else{ this.props.setMonth(12); this.props.setYear(this.props.year - 1); } } render() { const {year, month} = this.props; return <div className="picker"> <div className="left"> <a className="btn" href="###" onClick={()=>{this.prevMonth()}}>上1月</a> </div> <div className="center"> <a href="###">{year}</a>年 <a href="###">{month}</a>月 </div> <div className="right"> <a className="btn" href="###" onClick={()=>{this.nextMonth()}}>下1月</a> </div> </div> } }
完善app/components/canlendar/ChooserDate.js
添加类名,点击单元格切换
import React from "react"; import { solar2lunar } from "solarlunar"; import classnames from "classnames"; export default class ChooserDate extends React.Component { constructor() { super(); } //点击某一个小格格改变年月日 clickTd(d, isPrevMonth, isNextMonth){ this.props.setDate(d); //设置日子 this.props.setShowMenu(false); //关闭菜单 if(isPrevMonth){ var dd = new Date(this.props.year, this.props.month - 2, d); //月份要重算 this.props.setMonth(dd.getMonth() + 1); //改变月份 this.props.setYear(dd.getFullYear()); //改变年 }else if(isNextMonth){ var dd = new Date(this.props.year, this.props.month, d); //月份要重算 this.props.setMonth(dd.getMonth() + 1); //改变月份 this.props.setYear(dd.getFullYear()); //改变年 } } //显示表格 showTable(){ const {year , month , date} = this.props; //三要素 ....... var arr = []; //上个月的尾巴 var count = thismonth1day; while(count--){ var d = prevmonthdateamount - count; var sl = solar2lunar(year, month - 1, d); arr.push({ "d" : d, "cd": sl.term || sl.dayCn, "gray" : true , "cur" : false , "prevMonth" : true }) } //本月 var count = 1; while (count <= thismonthdateamount){ var d = count; var sl = solar2lunar(year, month, d); arr.push({ "d": d, "cd": sl.term || sl.dayCn, "gray": false , "cur": date == d }); count++; } //下月开头 var count = 1; while(arr.length != 35 && arr.length != 42){ var d = count++; var sl = solar2lunar(year, month + 1, d); arr.push({ "d": d, "cd": sl.term || sl.dayCn, "gray" : true , "cur" : false , 'nextMonth' : true }); } var domArr = []; for(var i = 0 ; i < arr.length / 7 ; i++){ domArr.push( <tr key={i}> { // 数组会自动展开 arr.slice(i * 7, i * 7 + 7).map((item, index) => { return <td key={index} className={classnames({"gray":item.gray, "cur":item.cur})} onClick={()=>{this.clickTd(item.d, item.prevMonth, item.nextMonth)}} > <span className="d">{item.d}</span> <span className="cd">{item.cd}</span> </td> }) } </tr> ) } return domArr; } render() { return <div className="chooserDate"> <table> ... </table> </div> } }
app/components/canlendar/CanlendarMenu.js切换视图
import React from "react"; import PickerDate from "./PickerDate.js"; import ChooserDate from "./ChooserDate.js"; import ChooserDate from "./ChooserYear.js"; export default class CanlendarMenu extends React.Component { constructor() { super(); } render() { //解构得到年、月、日 const {year, month, date} = this.props; return <div className="canlendar_menu"> <PickerDate year={year} month={month}></PickerDate> {/*<ChooserDate year={year} month={month} date={date}></ChooserDate>*/} <ChooserYear year={year} setYear={setYear}></ChooserYear> </div> } }
app/components/canlendar/ChooserYear.js
import React from "react"; import classnames from "classnames"; export default class chooserYear extends React.Component { constructor() { super(); } //组件上树之后 componentDidMount(){ var self = this; //事件委托,因为td太多了 $(this.refs.table).on("click","td", function(){ //得到你点击的小格格里面的内容,内容就是年份 var year = $(this).html(); self.props.setYear(year); //设年 self.props.setView("date"); //回到日视图 }); } //显示表格 showTable(){ //算出基数年,比如当前2018年,基数年就是2010年。就是年份减去“零头”。 const baseYear = this.props.year - this.props.year % 10; var arr = []; for(var i = 0; i < 10 ; i++){ arr.push( <tr key={i}> <td>{baseYear + i - 20}</td> <td>{baseYear + i - 10}</td> <td className={classnames({"cur":baseYear + i == this.props.year})}> {baseYear + i} </td> <td>{baseYear + i + 10}</td> <td>{baseYear + i + 20}</td> </tr> ) } return arr; } render() { return <div className="chooserYear"> <table ref="table"> <tbody> {this.showTable()} </tbody> </table> </div> } }
CSS样式:
.canlendar .chooserYear table .cur{color:red;font-weight: bold;} .canlendar .chooserMonth table{ width:100%;text-align: center;line-height: 40px; } .canlendar a{ color: #2196F3;text-decoration: none;padding: 0 3px; }
canlendar/CanlendarMenu.js
import React from "react"; import PickerDate from "./PickerDate.js"; import PickerYear from "./PickerYear.js"; import ChooserDate from "./ChooserDate.js"; import ChooserYear from "./ChooserYear.js"; import ChooserMonth from "./ChooserMonth.js"; export default class CanlendarMenu extends React.Component { constructor() { super(); this.state = { view : "date" //当前的视图date、month、year } } //设置视图 setView(view){ this.setState({view}); } render() { //解构得到年、月、日 const { year, month, date, setYear, setMonth, setDate, setShowMenu} = this.props; //定义Chooser组件 const Chooser = ()=>{ //根据state的view属性的值,来决定真实的chooser if(this.state.view == "date"){ return <ChooserDate year={year} month={month} date={date} setYear={setYear} setMonth={setMonth} setDate={setDate} setShowMenu={setShowMenu} ></ChooserDate> }else if(this.state.view == "year"){ return <ChooserYear year={year} setYear={setYear} setView={this.setView.bind(this)} ></ChooserYear> } else if (this.state.view == "month") { return <ChooserMonth setMonth={setMonth} setView={this.setView.bind(this)} ></ChooserMonth> } } //定义Picker组件 const Picker = ()=>{ if(this.state.view == "date"){ return <PickerDate year={year} month={month} setYear={setYear} setMonth={setMonth} setView={this.setView.bind(this)} ></PickerDate> }else if(this.state.view == "year"){ return < PickerYear year={year} setYear={setYear} ></PickerYear > }else if(this.state.view == "month"){ return null; } } return <div className="canlendar_menu"> <Picker></Picker> <Chooser></Chooser> </div> } }
canlendar/PickerYear.js
import React from "react"; export default class PickerYear extends React.Component { constructor() { super(); } render() { const {year, setYear} = this.props; return <div className="picker"> <div className="left"> <a className="btn" href="javascript:;" onClick={()=>{setYear(year-1)}}> 上1年 </a> </div> <div className="center"> {year}年 </div> <div className="right"> <a className="btn" href="javascript:;" onClick={()=>{ setYear(year + 1)}}> 下1年 </a> </div> </div> } }
canlendar/ChooserMonth.js
import React from "react"; import classnames from "classnames"; export default class chooserMonth extends React.Component { constructor() { super(); } componentDidMount(){ //事件委托 var self = this; $(this.refs.table).on("click","td", function(){ self.props.setMonth(parseInt($(this).data("m"))); self.props.setView("date"); }) } render() { return <div className="chooserMonth"> <table ref="table"> <tbody> <tr> <td data-m="1">1月</td> <td data-m="7">7月</td> </tr> <tr> <td data-m="2">2月</td> <td data-m="8">8月</td> </tr> <tr> <td data-m="3">3月</td> <td data-m="9">9月</td> </tr> <tr> <td data-m="4">4月</td> <td data-m="10">10月</td> </tr> <tr> <td data-m="5">5月</td> <td data-m="11">11月</td> </tr> <tr> <td data-m="6">6月</td> <td data-m="12">12月</td> </tr> </tbody> </table> </div> } }
文章都是本人学习时的笔记整理,希望看完后能对您有所帮助,欢迎大家提意见,多多交流。
也有些文章是转载的,如果存在转载文章且没有标注转载地址的,请与我联系,马上处理。
自由转载-非商用-非衍生-保持署名。