关于Vue的深刻以及不深刻理解
Table of Contents
什么是MVVM #
MVVM是一种视图模型双向绑定
的设计模式,是Model-View-ViewModel
的缩写,是从MVC
模式演变过来的。
Model
层代表数据模型,View
代表UI组件,ViewModel
是前两者之间的桥梁,数据会绑定到ViewModel
层,并将数据渲染到页面上,视图变化时会通知ViewModel
更新数据。
以前是操作DOM结构更新视图,现在是数据驱动视图
。
MVVM的优点 #
低耦合
:在MVC
模式中View
强依赖Model
,导致View
无法进行组件化设计;现在View
可以独立于Model
变化和修改,一个Model
可以绑定到不同View
上可重用性
:我们可以吧一些视图逻辑放在一个Model
里,让很多View
重用这段视图逻辑独立开发
:开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员就专注页面设计可测试
:在MVC
中,View
需要UI环境,因此依赖View
的Controller
测试会变得很困难
Vue的底层逻辑 #
Vue是个典型的MVVM框架,它实现了数据与视图的双向绑定。
它是采用数据劫持结合发布订阅者模式来实现这一逻辑的。
Observer #
数据监听器,主要功能是对数据进行劫持,核心功能是通过Object.defineProperty()
方法实现的,每当获取数据时,会触发方法内部的getter
函数,将订阅者(Watcher)添加到订阅器(Dep)中;当数据发生变化的时候就触发setter
函数,通知订阅者进行视图更新。
Watcher #
订阅者,作为Compile
和Observer
之间通信的桥梁,主要的职责是:
- 在
Compile
运行时实例化自身,生成需要传递给update()
方法的回调函数 - 在
Observer
监听到获取数据时,往Dep
中添加自己的实例 - 自身实例都会有一个
update()
方法,数据发生变化的时候,Dep.notify()
给订阅者发布消息,触发update()
方法
Compile #
指令解析器,就是用来解析模板指令的,主要的工作是:
- 把模板中的变量替换成响应的数据,然后初始化渲染页面视图。
- 生成订阅者更新视图时需要调用的回调函数
- 给每个指令对应的节点绑定更新函数,实现数据的双向绑定。
Vue的生命周期 #
对于生命周期,官方文档是这么解释的:
每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
依然是官方给的生命周期图示:
- 创建阶段
beforeCreat
:创建前,data和methods中的数据都还没初始化created
:创建完,数据初始化完成,能够获取到data中的值
- 编译阶段
beforeMount
:进行模板编译mounted
:将数据挂载到视图上,可以获取和操作DOM
- 更新阶段
beforeUpdate
:监测到数据的变化,触发虚拟DOM的重新渲染updated
:视图更新完成
- 销毁阶段
beforeDestroy
:实例销毁前,可以手动销毁一些方法destroyed
:销毁后
组件生命周期 #
父组件beforeCreate –> 父组件created –> 父组件beforeMount –> 子组件beforeCreate –> 子组件created –> 子组件beforeMount –> 子组件 mounted –> 父组件mounted –>父组件beforeUpdate –>子组件beforeDestroy–> 子组件destroyed –> 父组件updated
规律就是,子组件在父组件数据挂载前才开始初始化,直到子组件挂载完毕父组件才挂载完毕;子组件的更新或者销毁都会触发父组件更新,直到子组件更新或销毁完毕,父组件才更新完毕;在销毁阶段同理,得在子组件销毁完毕后父组件才会触发destroyed
钩子函数。
computed和watch的区别 #
watch
:是一个对象,键是需要监听已在data中定义的属性,值是对应的回调函数。当监听的属性发生变化时,触发watch中的方法,通常需要执行异步或者开销较大的操作时使用
computed
:计算属性是用来声明式的描述一个值依赖了其它的值,computed中的函数必须用return
返回结果。只有当它依赖的属性发生变化时,才会调用函数重新计算,否则会直接从缓存中读取结果。
所以相比watch来说,computed更加高效,如果二者都能实现的功能,推荐优先使用computed。
如何实现一个computed #
computed的用法是这样的
data: () {
return {
a: 1,
b: 2,
name: 'helene'
}
},
computed: {
sum() {
return this.a + this.b
},
myName: this.name
}
computed里的值可能是个函数,也可能是个对象,computed函数就能这么写
// 将vue实例传进来
function initComputed(vm) {
// 获取到computed对象
const computed = vm.$options.computed
// 听过Object.keys()方法将computed对象的key转化成数组
Object.keys(computed).forEach(key => {
// 数据代理,获取方法时不用写成vm.$options[key],直接vm[key]就能获取了
Object.defineProperty(vm, key, {
/**
* 判断computed里的值是对象还是函数,
* 是函数就直接运行,读取依赖的值时会自动调用getter方法;
* 是对象就手动调用get方法
*/
get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
set() {}
})
})
}
组件中的data为什么是一个函数 #
一个组件可以复用多次,这样会创建出多个实例,但本质都是由一个构造函数创建的。如果data是一个对象的话,那么data就是一个引用类型,当我们修改一个实例的data属性时,其它实例的属性会跟着一起变化。为了防止数据污染,data必须是一个函数,这样里面的属性就有自己的作用域了。
vue的路由原理 #
首先,为什么要使用路由?
众所周知,vue生成的页面是单页面(SPA)。服务器上只有一个由前端生成的index.html
静态文件,用户在页面上点击操作不会触发http请求新的静态文件index.html
,但依然可以做到局部刷新数据,让用户体验到反馈,这就是路由的作用。
路由有两种模式,hash模式和history模式。
hash模式 #
hash模式URL长这样
- 首页:
yourdomain.xxx.com/index.html/#/
- 博文:
yourdomain.xxx.com/index.html/#/posts
#
号后面的内容就是hash部分,不同显示内容对应的hash不同,路由就是通过监听hash的变化进行相应的内容渲染的。 实现hash模式的核心就是浏览器暴露给开发者的hashchange
方法。
class VueRouter {
constructor(routes = []) {
// 获取路由映射
this.routes = routes
this.currentHash = ''
this.refresh = this.refresh.bind(this)
// 监听加载和路由变化事件,调用refresh方法
window.addEventListener('load', this.refresh, false)
window.addEventListener('hashchange', this.refresh, false)
}
// 获取hash值
getUrlPath(url) {
return url.indexOf('#') >= 0 ? url.slice(url.indexOf('#') + 1) : '/'
}
refresh(event) {
let newHash = ''
let oldHash = null
if (event.newURL) {
oldHash = this.getUrlPath(event.oldURL || '')
newHash = this.getUrlPath(event.newURL || '')
} else {
newHash = this.getUrlPath(window.location.hash)
}
// 记录当前路由的hash值
this.currentHash = newHash
// 通过hash值获取相应组件内容
this.matchComponent()
}
matchComponent() {
// 找到路由映射中与当前hash匹配的路由数据
let currentRoute = this.routes.find(route => route.path === this.currentHash)
if(!currentRoute) {
// 如果没有数据匹配就返回到首页(根据具体场景可能有所不同
currentRoute = this.routes.find(route => route.paht === '/')
}
// 获取到组件数据
const { component } = currentRoute
// 将数据挂载到DOM上
documnet.querySelector('#content').innerHTML = component
}
}
const router = new VueRouter([
{
path: '/',
name: 'home',
component: '<div>首页</div>'
}, {
path: '/',
name: 'posts',
component: '<div>博文</div>'
}
])
从代码可以看出,hash模式可以完全由前端实现。
history模式 #
history模式的核心是HTML5提供的一个history
全局对象,它记录了用户在访问页面时留下的历史会话信息,同时这个对象中的一些方法可以对浏览器历史会话进行操作。
window.history.go
:可以跳转到浏览器历史会话中指定的某一个记录页window.history.forward
:跳转到历史会话中的下一页window.history.back
:跳转到历史会话中的上一页window.history.pushState
:将数据压入历史回话栈中window.hisotry.replaceState
:将当前回话页信息替换成指定数据 其实不论hash模式还是history模式,我们都是通过监听url的变化来实现数据更新的。
通过上面对history方法的介绍可以看出来,如果我们能够监听到pushState
和replaceState
事件的触发,就能够更新页面数据了。但这两个方法只能改变URL却并不能触发页面的刷新,这是为什么?
我们还要了解window在处理历史会话发生变化时触发的事件popstate
,它只会在浏览器行为下触发,比如点击前进、后退键,history.replaceState()
和history.pushState
无法触发popstate
。
那我们只能手动将这两个方法的事件监听器注册到window上了。
let _wr = function(type) {
let orig = history[type]
return function() {
// 改变原始方法的this指向
let rv = orig.apply(this, arguments)
// 初始化事件
let e = new Event(type)
// 将原方法调用的参数传给新创建的事件对象
e.arguments = arguments
// 将事件派发给window,当事件调用时,会触发EventListener
window.dispatchEvent(e)
return rv
}
}
history.pushState = _wr('pushState')
history.replaceState = _wr('replaceState')
然后我们就可以在监听回调中处理页面渲染了,思路和hash的实现大同小异。
但history模式有个问题,一旦刷新,页面会报404。
因为没刷新,不论我们怎么通过pushState改变URL,都是访问默认路径yourdomain.xxx.com
下的index.html
文件,刷新后就变成访问yourdomain.xxx.com/posts
下的index.html
文件了,但服务器上并没有这个路径资源。这种时候就需要配置nginx代理服务器,告诉服务器如果路径资源不存在,默认指向静态资源index.html
。