前端笔记之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> } }