VUE
说说对于MVVM的理解
1.英文缩写:
MVVM 是 Model-View-ViewModel 的缩写;
2.层级解释:
Model:数据层;泛指后端进行的业务逻辑处理和数据操控,主要围绕数据库系统展开。前端人员大多不需要管理数据层,只要后端保证对外接口足够简单调用即可,同时保证数据结构方便识别和调用,前端请求API,后端把数据按照规定结构返回来让我用就行。
View:视图层;用户界面层;前端主要使用HTML和CSS构件视图层
ViewModel:业务逻辑层;前端人员生成和维护的视图数据层,是MVVM的核心;注意:ViewModel层封装的数据模型包括视图的状态和行为两部分(视图样式和点击交互行为),而Model层的数据模型只包括视图数据,没有视图的动画交互;View层不是直接展示Mode层的数据,而是直接展示ViewModel层的数据和行为,间接的使用Model层数据,这样就完全实现了前后端分离。
3.三者联系:
数据层只写数据;视图层是图片显示;业务逻辑层负责将数据转化为可读的视图,同时将视图的变化反馈到数据的变化。
4.MVVM模式的意义:
MVVM模式促进了前后端的业务逻辑分离;提高前端开发效率;后端负责数据修改,前端负责调用数据进行视图更新。
MVVM模式最重要的是ViewModel层:相当于一个中转站,将数据和视图进行转化;一方面和视图层进行双向数据绑定,一方面调用数据层的接口进行数据交互
5.图示解释:
6.优点:
(1)双向绑定技术;单向绑定就是ViewModel层变化时,自动更新View层,不会反着来;双向绑定就是在单向绑定基础上监测View层变化来更新ViewModel层;
(2)提高了可维护性和可测试性;视图界面的直接测试是比较难的,而现在测试可以针对ViewModel来写。
(3)低耦合可重用:一个ViewModel可以重用在不同View上,可以把一些视图逻辑封装在ViewModel中,重用这段视图逻辑在不同View上。
7.缺点:
(1)BUG难调试;因为双向绑定的原因,当你看到界面异常时,这个错误可能出现在View层也可能是Model层;数据绑定使得这个bug被快速传递到别的位置,要定位这个bug的原位置,就很不容易了。
(2)大型的图形应用程序,视图状态较多,ViewModel的构件和维护成本比较高。
(3)大的模块中的MOdel数据很多,虽然使用方便了,保证数据的一致性,但是长期使用,不释放内存时就会花费更多的内存。
8.MVVM的使用范围:
MVVM不是万能的,目的是为了解决复杂的前端逻辑,尤其是解决需要大量DOM操作的逻辑;需要SEO的页面,不能使用MVVM展示数据(页面需要被搜索引擎搜索,而搜索引擎无法获取使用MVVM并通过API加载的数据);如果前端逻辑复杂,就适合使用MVVM展示数据(例如:工具类页面、复杂的表单页面、用户登录后才能操作的页面等)
9.常见的MVVM框架有:
Angular:Google出品,名气大,但是学习难度有些大;适合PC,代码结构会比较清晰;
Backbone.js:入门非常困难,因为自身API太多;
Ember:一个大而全的框架,想写个Hello world都很困难。
Avalon:属于轻量级的,并且对老的浏览器支持程度较高,最低支持到IE6,所以适合兼容老刘浏览器的项目;
Vue:主打轻量级,仅作为MV*中的视图部分使用,优点轻量级,易学易用,缺点是大项目的时候还要配合其他框架或者库来使用,比较麻烦
详细说下你对VUE生命周期的理解:
1.是什么:vue生命周期是指vue实例对象从创建之初到销毁的过程,vue所有功能的实现都是围绕其生命周期进行的,在生命周期的不同阶段调用对应的钩子函数实现组件数据管理和DOM渲染两大重要功能。
1.生命周期8阶段:创建前/后、载入前/后、更新前/后、销毁前/后
(1)beforeCreate(创建前):vue实例的挂载元素$el和数据对象 data都是undefined, 还未初始化(未赋值);场景:可以在这里加个loading事件,在加载实例时触发
(2)created (创建后) :vue实例已经创建;完成了 data数据初始化;el还未初始化()当这个函数执行的时候,我们已经可以拿到data下的数据以及methods下的方法了,所以在这里,我们可以开始调用方法进行数据请求了
(3)beforeMount (载入前): vue实例的el和data都初始化了, 相关的render函数首次被调用。首先我们会先生产一个虚拟dom(用于后续数据发生变化时,新老虚拟dom对比计算),进行保存,然后再开始将render渲染成为真实的dom。渲染成真实dom后,会将渲染出来的真实dom替换掉原来的vm.el append到我们的页面内。
(4)mounted (载入后) :在el 被新创建的 vm.$el替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互(mounted是平常使用最多的函数)场景:挂载元素,获取到DOM节点
**第一次加载页面时加载会触发的钩子:beforeCreate, created, beforeMount, mounted **
(5)beforeUpdate (更新前) :重新生成新的虚拟DOM(Vnode),然后和旧的比较计算,算出最小的更新范围,更新render函数中的最新数据,渲染render函数为真实dom。
(6)updated (更新后) :可以拿到更新后的DOM(mouted和updated的执行并不会等待所以子组件都被挂载完毕后再执行,所以如果你希望所有视图都更新完毕后再做些什么行为,那你最好在mouted/updated中加一个$nextTick(),然后把要做的事情放在里面)(在这一阶段DOM会和更改过的内容同步)场景:如果对数据统一处理,在这里写上相应函数
(7)beforeDestroy (销毁前): 此时实例未销毁,可以操作实例。场景:可以做一个确认停止事件的确认框
(8)destroyed (销毁后):所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用
- created和mounted的区别:
created:在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图。(场景:渲染html前先请求将数据请求完毕,然后使用数据进行渲染)
mounted:在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作。(场景:异步请求一般都谢在这里)
3.生命周期函数的作用:
以案例来说明:我们在created函数里写入数据请求函数,在渲染html页面前使用API获取后端数据(否则无法使用数据进行布局)这里created钩子函数的作用就是:让请求后台数据这个行为更加具有逻辑性,能让我们知道应该在什么阶段进行后台数据请求
4.图例理解整个流程: 详细解释生命周期整个流程
重点是:判断el对象的存在、检测是否有手动挂载vm.$mount(el)、检测template存在、转化render函数、虚拟dom替换真实dom
(1)此时设置el(#app),并没有设置template模板
<body>
<div id="app">
<p>{{message}}</p>
<button @click="changeMsg">改变</button>
</div>
</body>
<script>
var vm = new Vue({
el: '#app', //挂载
data: {
message: 'hello world'
},
methods: {
changeMsg () {
this.message = 'goodbye world'
}
},
beforeCreate() {
console.log('------初始化前------') //正常输出
console.log(this.message) //输出为undefined ;因为此时我们拿不到data数据,数据还未赋值
console.log(this.$el) //输出为undefined
},
created () {
console.log('------初始化完成------') //正常输出
console.log(this.message) //输出为:hello world;data数据已经初始化
console.log(this.$el) //输出为:undefined:$el未初始化
},
beforeMount () {
console.log('------挂载前---------') //正常输出
console.log(this.message) //输出:hello world
console.log(this.$el) //为什么输出的是源代码?
因为流程是先判断el为(#app)后会判断是否有template模板,没有则会将其源代码替换
vm.$el(vm.$el的作用:获取Vue实例关联的DOM元素:也就是获取初始设置的渲染信息);有
template模板的话就会将el(#app)编译为template模板,再转化为render函数,最后渲染为
真实dom,用真实dom替换初始的vm.$el。
},
mounted () {
console.log('------挂载完成---------')
console.log(this.message)
console.log(this.$el) //这时候输出的是渲染完毕后的$el,因为$el已经是被替换为了源代码,所以输出依然是源代码
},
beforeUpdate () {
console.log('------更新前---------')
console.log(this.message)
console.log(this.$el)
},
updated() {
console.log('------更新后---------')
console.log(this.message)
console.log(this.$el)
}
})
</script>
(2)加上template模板
var vm = new Vue({
el: '#app',
data: {
message: 'hello world'
},
template: '<div>我是模板内的{{message}}</div>', //template模板
methods: {
changeMsg () {
this.message = 'goodbye world'
}
},
beforeCreate() {
console.log('------初始化前------')
console.log(this.message)
console.log(this.$el)
},
created () {
console.log('------初始化完成------')
console.log(this.message)
console.log(this.$el)
},
beforeMount () {
console.log('------挂载前---------')
console.log(this.message)
console.log(this.$el) //此时输出的仍是源代码,因为仅仅是进行的判断,并没有真正的进行替换参数,所以还是原来的源代码。
},
mounted () {
console.log('------挂载完成---------')
console.log(this.message)
console.log(this.$el) //此时执行的是真正的替换,将源代码替换为template模板中的内容
},
beforeUpdate () {
console.log('------更新前---------')
console.log(this.message)
console.log(this.$el)
},
updated() {
console.log('------更新后---------')
console.log(this.message)
console.log(this.$el)
}
})
5.生命周期流程图示:
注意:钩子函数是包含上面的,不是从下面的开始执行
Vue的双向数据绑定原理是什么
1.实现原理:Vue 则采用的是数据劫持与发布订阅相结合的方式实现双向绑定,数据劫持主要通过 Object.defineProperty 来实现。
2.需要的三个步骤
Observer 监听器:遍历地为每个属性加上set和get函数,实现所有数据监听使用Object.defineProperty来监听:当内容被浏览时触发get函数;当内容被更改时触发set函数
Watcher 订阅者:是Observe和Complie之间通信的桥梁;Observe监听到数据更改后,Compile解析数据,Watcher缓存自身并强制调用update函数进行数据修改
Compile 解析器:可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器
思路总结:
1.总体思路:数据变化更新视图、视图变化更新数据
2.Model-到-View(难点)
关键是如何知道model层数据改变了?
通过set的触发与否来知道数据是不是被修改了
为什么使用发布订阅者模式?
因为一个model数据对应的可能是多个视图层的数据展示,所以需要一对多的改变视图层,就要使用发布订阅者模式(相当于明星,很多粉丝是订阅者=关注/受众者,明星只有一个就是model数据;也就是说改变1个model的值可以改变多个view中的值)
3.View-到-Model
很简单,只需要事件监听input输入框里的值,然后获取后改变model层的data数据即可
4.思路:首先我们为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;然后在编译的时候在该属性的数组dep中添加订阅者,v-model会添加一个订阅者,{{}}也会,v-bind也会,只要用到该属性的指令理论上都会,接着为input会添加监听事件,修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。
源码:
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>双向绑定</title>
</head>
<body>
<!-- 实现vue -->
<div id="app">
<input type="text" v-model="text">
{{ text }}
</div>
<script type="text/javascript">
1.遍历监听数据
function defineReactive(obj, key, val){
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function(){
if(Dep.target){
dep.addSub(Dep.target); // 添加订阅者
}
return val
},
set: function(newVal){
if(newVal === val){
return
}
val = newVal;
console.log('新值:' + val);
// 一旦更新立马通知
dep.notify();
}
})
}
/*观察者函数*/
函数封装:遍历所有数据设置set和get
function observe(obj,vm){
for(let key of Object.keys(obj)){
defineReactive(vm, key, obj[key]);
}
}
function nodeToFragment(node,vm){
var fragment = document.createDocumentFragment();
var child;
while(child = node.firstChild){
compile(child, vm);
fragment.appendChild(child);
}
return fragment
}
2.Compile:数据解析:数据修改后通知订阅者Watcher
/*编译函数*/
function compile(node, vm){
var reg = /\{\{(.*)\}\}/; // 来匹配 {{ xxx }} 中的xxx
// 如果是元素节点
if(node.nodeType === 1){
var attr = node.attributes;
// 解析元素节点的所有属性
for(let i=0;i<attr.length;i++){
if(attr[i].nodeName == 'v-model'){//遍历属性节点找到v-model的属性
var name = attr[i].nodeValue; // 获取v-model绑定的属性名,看看是与哪一个数据相关
node.addEventListener('input', function(e){
vm[name] = e.target.value; // 给相应的data属性赋值,进而触发该属性的set方法
});
node.value = vm[name]; // 将data的值赋给该node
node.removeAttribute('v-model');
}
};
}
// 如果是文本节点
if(node.nodeType === 3){
if(reg.test(node.nodeValue)){
var name = RegExp.$1; // 获取到匹配的字符串
name = name.trim();
// node.nodeValue = vm[name]; // 将data的值赋给该node
new Watcher(vm, node, name); // 不直接通过赋值的操作,而是通过绑定一个订阅者////创建新的watcher,会触发函数向对应属性的dep数组中添加订阅者
}
}
}
3.Watcher订阅者:收到解析数据时调用update函数(get函数)进行更新
/*Watcher构造函数*/
function Watcher(vm, node, name){
Dep.target = this; // Dep.target 是一个全局变量
this.vm = vm;
this.node= node;
this.name = name;
this.update(); //强行调用get函数
Dep.target = null; //释放变量
}
/*update更新函数封装*/
Watcher.prototype = {
update(){
this.get();
this.node.nodeValue = this.value; // 注意,这是更改节点内容的关键
},
get(){
this.value = this.vm[this.name]; // 触发相应的get
}
}
/*dep构造函数:收集订阅者,为每个属性添加订阅者*/
function Dep(){
this.subs = [];//数组形式储存
}
Dep.prototype = {
//添加订阅者
addSub(sub){
this.subs.push(sub);
},
notify(){
this.subs.forEach(function(sub){
sub.update();
})
}
}
/*Vue构造函数*/
function Vue(options){
this.data = options.data;
var data = this.data;
observe(data, this);
var id = options.el;
var dom = nodeToFragment(document.getElementById(id), this);
// 处理完所有dom节点后,重新将内容添加回去
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});
</script>
</body>
</html>