Vue是完全的MVVM模型吗?
严格的MVVVM要求View不能和Model直接通信,而Vue提供了$refs这个属性,让Model可以直接操作View,违反了这一规定,所以是Vue没有完全遵循MVVM。
Vue的单项数据流
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
Vue的父子组件生命周期钩子函数执行顺序
-
加载渲染过程
-
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
-
子组件更新过程
-
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
-
父组件更新过程
-
父beforeUpdate -> 父updated
-
销毁过程
-
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
MVC和MVVM区别
MVC
MVC是model view controller
MVC的思想:Controller负责将Model的数据用View显示出来,换句话说就是在Controller里面把Model的数据赋值给View。
MVC的特点:实现关注点分离,即应用程序中的数据模型与业务和展示逻辑解耦。就是将模型和视图之间实现代码分离,松散耦合,使之成为一个更容易开发、维护和测试的客户端应用程序。
一般用于中大型项目开发。
MVVM
MVVM是Model-View-ViewModel的简写,即模型-视图-视图模型。
它有两个方向:
- 一是将模型转化成视图,即将后端传递的数据转化成所看到的页面。实现的方式是:数据绑定。
- 二是将视图转化成模型,即将所看到的页面转化成后端的数据。实现的方式是:DOM 事件监听。
这两个方向都实现的,就是数据的双向绑定。
diff算法
双向绑定
什么是双向绑定
单项绑定就是通过Model去改变view。
当实现用户更新view,model的数据也自动被更新就实现了双向绑定。
例如当用户填写表单时候改变了view,Model的数据也被自动更新了,这就实现了双向绑定。
双向绑定原理

vue采用的是 数据劫持结合 发布-订阅模式
通过ES5的 Object.defineProperty()
的get
和set
来进行数据的劫持。
1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
需要自己动手实现代码
现在复述下实现过程
observe 作为一个收集者,将watch全收集起来,当发生改变的时候,会通过属性的set去notify。watch主要是对这个vue实例模板节点进行订阅后存进observe。存的包括这个节点、节点类型、节点value。每次初始化watch就会将将碰到的这个模板节点进行存放,调用这个属性的Obejct.defineProperty的get
方法,然后进行DeP的存放。每次这个值就行更改就会调用set方法,然后再把所有watch进行执行。
vuex
vuex是为专门开发的状态管理模式。采用集中式存储管理所有组件的状态。
出现原因
vue一般是单项数据流,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
多个视图依赖于同一状态、来自不同视图的行为需要变更同一状态。
作用:多个组件共享数据或者是跨组件传递数据
使用
// store.js
1.引入Vuex插件;
Vue.use(Vuex);
2.将Vuex.Store这个类实例化,并传入一些配置
const store = new Vuex.Store({
state:{
count:0
},
mutations:{
increment(state){
state.count++;
},
del(state){
state.count--;
},
},
actions:{
asyncAdd({commit}){
setTimeout(() => {
commit("increment");
}, 2000);
}
}
})
3.将store的实例配置给Vue
// main.js
new Vue({
store,
render: h => h(App),
}).$mount('#app')
4.组件中使用时
// App.vue
add(){
this.$store.commit('increment');
},
asyncAdd(){
this.$store.dispatch('asyncAdd');
}
</script>
实现
let Vue
class Store{
constructor(options){
let { state, getters, actions, mutations } = options
}
}
const install=function(_Vue){
Vue=_Vue
//使用Vue的混入方法
Vue.mixin({
beforeCreat(){
//根实例有store属性
if(this.$options && this.$options.store)
{
this.$store=this.$options.store
}
else{
//根实例没有store实例,往父节点找
new Vue({store})
this.$store=this.$parent && this.$parent.$store
}
}
})
}
export default install
在beforeCreate
中,第一次根组件通过store
属性挂载$store
,后面子组件调用beforeCreate
挂载的$store
都会向上找到父级的$store
,这样子通过层层向上寻找,让每个组件都挂上了一个$store
属性,而这个属性的值就是我们的new Store({...})
的实例。如下图
设置state响应数据
通过创建vue实例来实现双向绑定
class Store{
constructor(options){
let {state,getters,mutations,actions}=options
this.getters = {}
this.mutations = {}
this.actions = {}
}
// vuex的核心就是借用vue的实例,因为vuex的数据更改回更新视图
this._vm=new Vue({
data:{
state
}
})
}
// 访问state对象时候,就直接返回响应式的数据
get state() { // Object.defineProperty get 同理
return this._vm.state
}
}
组件中的通信
vue bus
通过注册一个Vue对象,其他组件通过这个中间商传递信息
import Vue from 'vue'
const bus = new Vue()
export default bus
组件发送消息
import bus from '@/utils/bus'
bus.$emit('message', 'hello');
组件接收消息
import bus from '@/utils/bus'
bus.$on('message', (e) => {
console.log(e)
})
Vuex
通过组件共享的store来实现全局共享
this.$store.state.xxx
props
和emit
父
<div>
<child :message="message" @changemessage="handlechild"></child>
</div>
<script>
expore default{
data(){
message:'fanfan',
},
methods:{
handlechild(){
...
}
}
}
</script>
孩子
<div>
{{message}}
</div>
<script>
expore default{
props:{
message:String
},
data(){
message:'fanfan',
},
methods:{
somemethods(){
this.$emit('changemessage',this.messgae)
}
}
}
</script>
或者使用更加简洁的写法
<child :message.sync="message">
this.$emit('update:message',this.message)
listeners
经常用于多层嵌套
attrs
:包含的是父域中未被props
识别的特性属性。通过v-bind="$attrs"
传入下一个子组件
listeners
: 包含了父作用域中的 (不含 .native
修饰器的) v-on
事件监听器。它可以通过 v-on="$listeners"
传入内部组件存储的父实例传入的方法(存放的是父组件中绑定的非原生事件)
provide和inject
向子孙后代注入一个依赖。
祖先通过provide
提供变量,然后在子孙组件通过inject
来注入变量。
// 父级组件提供 'foo'
var Provider = {
provide: {
foo: 'bar'
},
// ...
}
// 子组件注入 'foo'
var Child = {
inject: ['foo'],
created () {
console.log(this.foo) // => "bar"
}
// ...
}
//`inject`也可以有默认项
const Child = {
inject: {
foo: { default: 'foo' }
}
}
但需要注意的是 provide和inject的绑定并不是响应式的。如果传入的是可监听的对象,那么其对象还是可响应的。
var privider={
provide:{
name:'fan'
}
}
var child={
inject:['name'],
created(){
console.log(this.name)
}
}
如果这个父组件的name改变了,子组件的name还是不会变的。
当然我们可以直接提供祖先实例,这样对子孙组件对祖先的修改也会直接响应。
var provider={
provide(){
return{
myfather:this //注入祖先实例
}
}
}
var child={
inject:{
myfather:{
default:()=>({})
}
},
}
这样带来的不便就是将多余的方法等不需要的东西也挂载了。
当然也可以用Vue.observable
这个API来实现,它是让一个对象可响应,用于组件状态的共享。
var provider={
data(){
return {
myobj:{}
}
},
provide(){
this.myobj=Vue.Observable({
name:'fan',
age:'20'
})
return this.myobj
}
}
var child={
inject:{
myobj:{
default:()=>({})
}
},
}
children和$ref
<My-Comp ref="myref"></My-Comp>
//直接获得子组件的实例
this.$ref.myref.xxx
不建议频繁使用$parent、$children
vue-router
更新视图而不更新页面,是路由实现的核心功能
HashHistory
、HTML5History
、AbstratHistory
主要是靠以下两种方法实现
-
HashHistory
hash 虽然出现在 url 中,但不会被包括在 http 请求中,它是用来指导浏览器动作的,对服务器端完全无用,因此,改变 hash 不会重新加载页面。
给hash添加监听事件
window.addEventListener("hashchange",function(){},false)
HashHistory.push()
、HashHistory.replace()
第一个是将新路由推入浏览器访问历史的栈顶
第二个只替换当前路由,而不记录
-
HashHistory
通过
back()
,forward()
,go()
等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。从
HTML5
开始,History interface
提供了2个新的方法:pushState()
,replaceState()
使得我们可以对浏览器历史记录栈进行修改:这2
个方法有个共同的特点:当调用他们修改浏览器历史栈后,虽然当前url
改变了,但浏览器不会立即发送请求该url
,这就为单页应用前端路由,更新视图但不重新请求页面提供了基础。在
HTML5History
中添加对修改浏览器地址栏URL
的监听**popstate
**是直接在构造函数中执行的:
<body>
<div id="app">
<a href="#foo">foo</a>
<a href="#bar">bar</a>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const foo={template:`<div> foo</div>`}
const bar={template:`<div> bar</div>`}
const mydefault={template:`<div>mydefault</div>`}
const routetable={
'foo':foo,
'bar':bar,
'':mydefault,
}
const app=new Vue({
el:'#app',
data:{
url:window.location.hash.slice(1)
},
render(h) {
return h('div',[
h(routetable[this.url]),
h('a',{attrs:{href:'#foo'}},'foo'),
' | ',
h('a',{attrs:{href:'#bar'}},'bar')
])
},
})
window.addEventListener('hashchange',()=>{
app.url=window.location.hash.slice(1)
})
</script>
父子组件生命周期顺序
->父beforeCreate -> 父created -> 父beforeMount
**->子beforeCreate -> 子created -> 子beforeMount -> 子mounted
-> 父mounted
子组件更新过程
->父beforeUpdate
-> 子beforeUpdate -> 子updated
-> 父updated
父组件更新过程
父beforeUpdate -> 父updated
销毁过程
-> 父 beforeDestory
-> 子 beforeDestory -> 子 destoryed
-> 父 destoryed