MVVM模式的理解
`MVVM`全称`Model-View-ViewModel`是基于`MVC`和`MVP`体系结构模式的改进,`MVVM`就是`MVC`模式中的`View`的状态和行为抽象化,将视图`UI`和业务逻辑分开,更清楚地将用户界面`UI`的开发与应用程序中业务逻辑和行为的开发区分开来。
描述
`MVVM`模式简化了界面与业务的依赖,有助于将图形用户界面的开发与业务逻辑或数据模型的开发分离开来。在`MVVM`中的`ViewModel`作为绑定器将视图层`UI`与数据层`Model`链接起来,在`Model`更新时,`ViewModel`通过绑定器将数据更新到`View`,在`View`触发指令时,会通过`ViewModel`传递消息到`Model`,`ViewModel`像是一个黑盒,在开发过程中只需要关注于呈现`UI`的视图层以及抽象模型的数据层`Model`,而不需要过多关注`ViewModel`是如何传递的数据以及消息。
组成
Model
- 以面向对象来对对事物进行抽象的结果,是代表真实状态内容的领域模型。
- 也可以将
Model
称为数据层,其作为数据中心仅关注数据本身,不关注任何行为。
View
View
是用户在屏幕上看到的结构、布局和外观,即视图UI
。- 当
Model
进行更新的时候,ViewModel
会通过数据绑定更新到View
。
ViewModel
ViewModel
是暴露公共属性和命令的视图的抽象。ViewModel
中的绑定器在视图和数据绑定器之间进行通信。- 在
Model
更新时,ViewModel
通过绑定器将数据更新到View
,在View
触发指令时,会通过ViewModel
传递消息到Model
。
优点
-
低耦合: 视图
View
可以独立于Model
变化和修改,一个ViewModel
可以绑定到不同的View
上,当View
变化的时候Model
可以不变,当Model
变化的时候View
也可以不变。 -
可重用性: 可以把一些视图逻辑放在一个
ViewModel
里面,让很多View
重用这段视图逻辑。 -
独立开发: 开发人员可以专注于业务逻辑和数据的开发
Model
,设计人员可以专注于页面设计。 -
可测试: 界面素来是比较难于测试的,测试行为可以通过
ViewModel
来进行。
不足
-
对于过大的项目,数据绑定需要花费更多的内存。
-
数据绑定使得
Bug
较难被调试,当界面异常,可能是View
的代码有问题,也可能是Model
的代码有问题,数据绑定使得一个位置的Bug
可能被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
SPA单页应用的优缺点
`Single Page Web Application`是一种特殊的`Web`应用,其所有的活动局限于一个`Web`页面中,仅在该`Web`页面初始化时加载相应的`HTML`、`JavaScript`、`CSS`文件,一旦页面加载完成,`SPA`不会进行页面的重新加载或跳转,而是利用`JavaScript`动态的变换`HTML`,默认`Hash`模式是采用锚点实现路由以及元素组件的显示与隐藏实现交互,简单来说`SPA`应用只有一个页面,通常多页面应用会有多个页面不断跳转,而单页面应用始终在一个页面中,默认`Hash`模式是通过锚点实现路由以及控制组件的显示与隐藏来实现类似于页面跳转的交互。
优点
- 良好的交互体验,页面首次加载完成后内容的改变不需要重新加载整个页面,具有更快的响应速度,具有桌面应用的即时性、网站的可移植性和可访问性。
- 良好的前后端工作分离模式,单页应用可以和`RESTful`架构一起使用,通过`RESTAPI`提供接口数据,有助于分离客户端和服务器端工作与`API`通用化。
- 减轻服务端压力,服务端不需要处理页面模板的逻辑与拼接,除首次加载页面外只需要提供数据信息即可,把计算尽量放在客户端,单页应用能提高单位服务器的负载量。
- 可维护性高,通常采用组件化与模块化开发,代码复用程度高,相对来说可维护性高。
缺点
- 不利于`SEO`,由于是采用前端渲染的方式,搜索引擎不会去解析`Js`从而只能够抓取首页未渲染的模板,如果需要单页面应用有更好的`SEO`,那么通常需要使用`SSR`服务端渲染,搜索引擎爬虫抓取工具可以直接查看完全渲染的页面,但是由于是服务端进行渲染,那么会对服务器造成一定压力,`SSR`服务端渲染属`CPU`密集型,当然如果只是需要`SEO`少数几个页面,可以采用预渲染的方式。
- 首次加载速度慢,`SPA`单页应用通常首次加载页面时就会将相应的`HTML`、`JavaScript`、`CSS`文件全部加载,通常可以通过采取缓存措施以及懒加载即按需加载组件的方式来优化。
Vue生命周期
- Vue实例需要经过创建、初始化数据、编译模板、挂载`DOM`、渲染、更新、渲染、卸载等一系列过程,这个过程就是`Vue`的生命周期,在`Vue`的整个生命周期中提供很多钩子函数在生命周期的不同时刻调用,`Vue`中提供的钩子函数有`beforeCreate`、`created`、`beforeMount`、`mounted`、`beforeUpdate`、`updated`、`beforeDestroy`、`destroyed`。
示例
在实例化`Vue`过程中,会直接触发的生命周期有`beforeCreate`、`created`、`beforeMount`、`mounted`,在数据更新的过程中触发的生命周期有`beforeUpdate`、`updated`,在销毁组件的过程中触发的生命周期有`beforeDestroy`、`destroyed`。
beforeCreate
从`Vue`实例开始创建到`beforeCreate`钩子执行的过程中主要进行了一些初始化操作,例如组件的事件与生命周期钩子的初始化。在此生命周期钩子执行时组件并未挂载,`data`、`methods`等也并未绑定,此时主要可以用来加载一些与`Vue`数据无关的操作,例如展示一个`loading`等。
console.log("beforeCreate");
console.log(this.$el); //undefined
console.log(this.$data); //undefined
console.log(this.msg); // undefined
console.log("--------------------");
created
从beforeCreate
到created
的过程中主要完成了数据绑定的配置、计算属性与方法的挂载、watch/event
事件回调等。在此生命周期钩子执行时组件未挂载到到DOM
,属性$el
目前仍然为undefined
,但此时已经可以开始操作data
与methods
等,只是页面还未渲染,在此阶段通常用来发起一个XHR
请求。
console.log("created");
console.log(this.$el); //undefined
console.log(this.$data); //{__ob__: Observer}
console.log(this.msg); // Vue Lifecycle
console.log("--------------------");
beforeMount
从created
到beforeMount
的过程中主要完成了页面模板的解析,在内存中将页面的数据与指令等进行解析,当页面解析完成,页面模板就存在于内存中。在此生命周期钩子执行时$el
被创建,但是页面只是在内存中,并未作为DOM
渲染。
console.log("beforeMount");
console.log(this.$el); //<div id="app">...</div>
console.log(this.$data); // {__ob__: Observer}
console.log(this.msg); // Vue Lifecycle
console.log("--------------------");
mounted
从beforeMount
到mounted
的过程中执行的是将页面从内存中渲染到DOM
的操作。在此生命周期钩子执行时页面已经渲染完成,组件正式完成创建阶段的最后一个钩子,即将进入运行中阶段。此外关于渲染的页面模板的优先级,是render
函数 >
template
属性 >
外部HTML
。
console.log("mounted");
console.log(this.$el); //<div id="app">...</div>
console.log(this.$data); //{__ob__: Observer}
console.log(this.msg); // Vue Lifecycle
console.log("--------------------");
beforeUpdate
当数据发生更新时beforeUpdate
钩子便会被调用,此时Vue
实例中数据已经是最新的,但是在页面中的数据还是旧的,在此时可以进一步地更改状态,这不会触发附加的重渲染过程。在上述例子中加入了debugger
断点,可以观察到Vue
实例中数据已经是最新的,但是在页面中的数据还是旧的。
// this.msg = "Vue Update";
console.log("beforeUpdate");
console.log(this.$el); //<div id="app">...</div>
console.log(this.$data); //{__ob__: Observer}
console.log(this.msg); // Vue Update
debugger;
console.log("--------------------");
updated
当数据发生更新并在DOM
渲染完成后updated
钩子便会被调用,在此时组件的DOM
已经更新,可以执行依赖于DOM
的操作。
// this.msg = "Vue Update";
console.log("updated");
console.log(this.$el); //<div id="app">...</div>
console.log(this.$data); //{__ob__: Observer}
console.log(this.msg); // Vue Update
console.log("--------------------");
beforeDestroy
在Vue
实例被销毁之前beforeDestroy
钩子便会被调用,在此时实例仍然完全可用。
// this.$destroy();
console.log("beforeDestroy");
console.log(this.$el); //<div id="app">...</div>
console.log(this.$data); //{__ob__: Observer}
console.log(this.msg); // Vue Update
console.log("--------------------");
destroyed
在Vue
实例被销毁之后destroyed
钩子便会被调用,在此时Vue
实例绑定的所有东西都会解除绑定,所有的事件监听器会被移除,所有的子实例也会被销毁,组件无法使用,data
和methods
也都不可使用,即使更改了实例的属性,页面的DOM
也不会重新渲染。
// this.$destroy();
console.log("destroyed");
console.log(this.$el); //<div id="app">...</div>
console.log(this.$data); //{__ob__: Observer}
console.log(this.msg); // Vue Update
console.log("--------------------");
双向数据绑定的原理
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的setter,getter,在数 据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几 个步骤:
- 需要observe 的数据对象进行递归遍历,包括子属性对象的属性, 都加上setter和getter这样的话,给这个对象的某个值赋值,就会 触发setter,那么就能监听到了数据变化
- compile 解析模板指令,将模板中的变量替换成数据,然后初始化 渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数 据的订阅者,一旦数据有变动,收到通知,更新视图 Watcher 订阅者是Observer 和 Compile 之间通信的桥梁,主要做 的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ② 自身必须有一个update()方法 ③待属性变动dep.notice()通知时, 能调用自身的update()方法,并触发Compile中绑定的回调,则功 成身退。
- MVVM 作为数据绑定的入口,整合Observer、Compile和Watcher 三者,通过Observer来监听自己的model数据变化,通过Compile 来解析编译模板指令,最终利用Watcher搭起Observer和Compile 之间的通信桥梁,达到数据变化-> 视图更新;视图交互变化(input)-> 数据model变更的双向绑定效果
slot 是什么?有什么作用?原理是什么?
- slot 又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽slot是子组件的一个模板 标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决 定的。slot又分三类,默认插槽,具名插槽和作用域插槽。 默认插槽:又名匿名插槽,当slot没有指定name属性值的时候一个 默认显示插槽,一个组件内只有有一个匿名插槽。
- 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一 个组件可以出现多个具名插槽。
- 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也 可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可 以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过 来的数据决定如何渲染该插槽。
- 实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的 内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插 槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇 到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递 数据,若存在数据,则可称该插槽为作用域插槽
$nextTick 原理及作用
Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的 一种应用。
nextTick 的核心是利用了如 Promise 、MutationObserver、 setImmediate、setTimeout 的原生 JavaScript 方法来模拟对应的 微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任 务队列来实现 Vue 框架中自己的异步回调队列。
nextTick 不仅是 Vue 内部的异步队列的调用方法,同时也允许开发 者在实际项目中使用这个方法来满足实际应用中对 DOM 更新数据时 机的后续逻辑处理
nextTick 是典型的将底层 JavaScript 执行原理应用到具体案例中 的示例,引入异步更新队列机制的原因∶
如果是同步更新,则多次对一个或多个属性赋值,会频繁触发 UI/DOM 的渲染,可以减少一些无用渲染
同时由于 VirtualDOM 的引入,每一次状态发生变化后,状态变化的 信号会发送给组件,组件内部使用 VirtualDOM 进行计算得出需要更 新的具体的 DOM 节点,然后对 DOM 进行更新操作,每次更新状态后 的渲染过程需要更多的计算,而这种无用功也将浪费更多的性能,所 以异步渲染变得更加至关重要
Vue 采用了数据驱动视图的思想,但是在一些情况下,仍然需要操作 DOM。有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2 需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新, 这时就需要用到了nextTick了。
由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2 获取数据的操作写在$nextTick中。 所以,在以下情况下,会用到nextTick: 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变 化的DOM结构的时候,这个操作就需要方法在nextTick()的回调函 数中。
在vue生命周期中,如果在created()钩子进行DOM操作,也一定要 放在nextTick()的回调函数中。 因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办 法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在 nextTick()的回调函数中。