前言
之前一直用react框架来写前端工程,最近单位一个项目因使用了vue框架,于是对照着react项目,选择对应的生命周期函数、router、 redux/vuex、jsx/template、async await/axios,三下五除二组装起了vue项目,当然,也因为对vue整体没有太多的了解,在有些复杂页面的如组件的复用遇到了些问题,重点就是不知道数据如何复写到页面上,于是便有了这篇文章。
一.js原生写法对DOM节点上数据的改变
从最基本最原始js说起,当html上有个dom节点p标签时候,我们可以通过js来改变p标签的内容
//html代码
<a id='a1'>a标签</a>
<p id='p2'>p标签</p>
<button id='btn3' onClick='btnClick'>按钮</button >
//js代码
btnClick(){
$("#p1").html("新内容");
}
我们知道,浏览器的刷新按钮是对整个页面进行重新渲染,而上述代码,我们通过jquery实现了对P标签内容的改变,并且这个改变是局部的改变,并不会引起a标签和button按钮,也不会引起整个页面重新渲染
记住两点,浏览器刷新是整个页面渲染
,jquery是局部渲染
二.Ajax/setTime等具有延迟操作特性函数对DOM节点上数据的改变
我们请求ajax获取数据后,对页面上p标签数据的改变跟上面js改变其实在本质上并没有什么改变,都是局部刷新。但往往我们一个页面有多个ajax请求,这些请求都是异步的,而我们知道JavaScript是单线程,这是怎么实现的?
浏览器线程
js运作在浏览器中,是单线程的,即js代码始终在一个线程上执行,这个线程称为js引擎线程。
浏览器是多线程的,除了js引擎线程,它还有:
UI渲染线程,用于渲染页面
浏览器事件触发线程,用于控制交互,响应用户
http请求线程,用于处理请求,ajax是委托给浏览器新开一个http线程
EventLoop轮询的处理线程,处理线程用于轮询消息队列
……..
当我们页面运行在页面上,展现在我们眼前的是js引擎线程,当我们操作时,虽然处理代码是写在了js里面,但最终还是会触发浏览器事件线程,并将该事件放在消息队列里,如果该操作包含一个网络请求,则会调用http请求线程(异步
),获取数据改变页面数据则会调用UI渲染线程。同理,setTime也是将操作放入消息队列里,改变数据,调用UI渲染线程。
记住一点,当一个页面运行在浏览器上,至少有四五个线程协作才能完成数据变化页面刷新
。
三.vue的数据追踪
基于js的前端页面,包含三个部分html、style、script,而基于vue的页面也是包含这个三个部分,template对应html,script和style;基于js的页面只有一些window.onload可以被归类为生命周期函数,而基于vue的页面极大的丰富了生命周期函数,其中的watch和computed就用于来追踪数据的变化。
下面我们就来分析vue是怎么追踪数据的
1.在data()函数中定义数据初始值
2.在template模板中用{{}}对某个控件(一个实例,如p标签)实现数据绑定。这里要介绍ES5中一个神奇的方法 Object.defineProperty,他有三个参数,实例对象、需要定义的属性或方法的名字、目标属性所拥有的特性。在第三个参数中,vue.js悄悄的给实例添加了setter和getter方法。这个函数对数据实现的拦截,当我们给数据赋值的时候,setter方法会实现2个操作:1.给数据赋值 2.发送一个事件给浏览器,告诉浏览器的UI渲染线程,我需要渲染页面了。个人认为这是vue数据追踪最核心的地方。
3.vue也可以用watch和computed来追踪数据的变化。watch用来监测当A数据变化时候,其它非A数据有没有什么变化(一对一,一对多,A是前者,监测前者的变化来改变后者),computed用来监测当非B数据或者多个非B数据变化时,B数据有什么变化(一对一,多对一,B是后者,监测前者的变化来改变前后者)。一般我们程序中要computed即可,很少会使用到watch。这里在程序中我们对每一个绑定的数据通过computed来追踪变化。
//templa代码
<a id='a1'>{{a1}}</a>
<p id='p2'>{{p2}}</p>
<button id='btn3' onClick='btnClick3'>按钮3</button >
<button id='btn4' onClick='btnClick4'>服务器取值</button >
//script中代码
data(){
a1:'a标签';
p2:'p标签';
}
watch:{
p2:function(){
this.a1='改变了';
}
},
computed(){
p2:function(){
return vuex中.state.p2;
}
},
methods:{
btnClick3(){
this.p2='新内容';
}
btnClick4(){
发送服务器请求getdata();
}
}
//vuex中代码
state:{
p2:'';
}
mutations:{
getdata(state,res){
const data=服务器返回数据;
state.p2=data;
}
}
点击页面按钮3进行赋值的时候,会默认调用p标签的setter方法完成1.更新数据2.发送事件给浏览器,调用UI渲染线程局部渲染页面
点击服务器请求按钮,获取数据后,computed中p2:function(){}也是一个赋值行为,从vuex中获取数据后,跟按钮3完成同样的更新数据及局部渲染页面;此外,watch监听到了p2的变化,也同步改变了a1的值,渲染了页面。这里实际上是局部渲染了两次页面。
记住一点,vue是追踪每一条通过{{}}绑定的数据,每次赋值后局部渲染
。
react状态管理
与vue数据追踪不同的是,react采用的时候状态变化来引起页面的变化。
我们先剔除状态里的函数,单纯讨论数据部分。
初学react,要比vue难理解的多,什么一组确定的状态对应一个确定的页面,什么更新组件的状态然后根据新的状态重新渲染用户界面,感觉跟大学里辩证唯物唯心主义一样绕人。
先看代码
//render代码
<a id='a1'>{a1}</a>
<p id='p2'>{p2}</p>
<button id='btn3' onClick='btnClick3'>按钮3</button >
<button id='btn4' onClick='btnClick4'>服务器取值</button >
//class中代码
state:{
a1:'a标签';
p2:'p标签';
}
btnClick3(){
const p='新内容';
this.setState({p2:p});
}
btnClick4(){
发送服务器请求getdata();
}
componentWillReceiveProps(nextProps):function{
if(nextProps.p!=this.pros.p2) const a='改变了';
const p=nextProps.p;
this.setState({p2: p,a1:a});
}
//redux中代码
state: {
p:"",
},
effects: {
*getdata({payload}, {call, put}){
const p=服务器返回数据;
yield put({type: 'savedata',payload: {p}});
},
}
在react中,state的作用有点类似vue中data(不考虑state里的函数),componentWillReceiveProps作用相当于vue的computed,我们可以发现,不管是在按钮3本页面直接赋值还是从服务器取值用componentWillReceiveProps接受新数据后,都需要显式的调用用this.setState,它完成2个动作:1.更新state里的数据 2.通知浏览器调用UI渲染线程来渲染页面。这里跟vue中最大的区别就是,vue是通过{{}}自动的给p标签实例添加setter,setter会自动的告诉浏览器数据变化了,而react需要手动的调用this.setState来通知浏览器我们要改变数据了。此外,vue是每条数据单独实现局部自动更新,而react数据改变后,手动整体更新
。
五.总结
vue和react说到底还是js,既然是js,刷新页面数据必然少不了浏览器重绘(repaint)和重排(reflow),事实上,vue的setter和react的setState给浏览器发送事件调用UI渲染线程的时候就是告诉浏览器我要重绘还是重排。
vue逐条更新和react整体更新到底谁效率更高?
重排是告诉浏览器我要重新布局,而重绘是根据已经布置好的布局实现样式外观等改变,所以说重排的消耗要比重绘多的多。
react是整体更新,不管你什么数据变了,我都是不计代价的触发一次重排
vue是逐条更新,理论上只是根据数据的变化,每次触发一下重绘即可,可能10次重绘都赶不上react的一次重排,看上去似乎vue效率更高。但是!vue有个v-if属性,这个是很讨厌的东西,它会根据值来影藏这个显示那个,实际上调用了两次display:block/none,而给display赋值必然会引起重排,在复杂页面中,我们必然会使用v-if,每个v-if都会重排两次,开销很大。