一、MVC 和 MVVC
MVC模式:
- 模型(Model) - 数据管理
- 视图(View) - 将数据模型转化为UI
- 控制器(Controller)- 通知模型更新数据,通知试图及时渲染(控制器责任太大,导致臃肿)
MVVM模式:
- 模型(Model)- 数据管理
- 视图(View)- 将数据模型转化为UI
- 视图模型(ViewModel)- 同步模型和视图数据(只关心业务处理,不关心逻辑)
二、基础知识
(一)生命周期钩子函数
- beforeCreate:无法获取 props 或 data
- created:可以访问 props 和 data,组件还没被挂载
- beforeMount :开始创建 VDOM
- mounted:并将 VDOM 渲染为真实DOM并且渲染数据。
- beforeUpdate:数据更新前
- updated:数据更新后
- beforeDestroy:组件销毁前,适合移除事件、定时器等等
- destroyed:组件销毁后
(二)组件间通信
- 父子组件之间
// 父组件 to 子组件
<!--父组件中-->
<son :toSon="msg" @toParent="print"></son>
<script>
data() {
return {
msg: 'parent2son'
}
},
methods:{
print(val){
this.msg = val
}
}
</script>
<!--子组件中-->
props: ['toSon'],
mounted(){
this.$emit('toParent','son2parent')
}
</script>
<!--等价于-->
// sync 语法糖:当一个子组件需要改变了一个 prop 的值时,会通知其父组件进行同步的修改
<!--父组件中-->
<son :change.sync="msg"></son>
<script>
data() {
return {
msg: 'parent2son'
}
}
</script>
<!--子组件中-->
props: ['change'],
mounted(){
this.$emit('update:change','son2parent')
}
- 任意组件之间
// 无论相隔多少层都能取到 eventBus
<!--父组件中-->
<son></son>
<script>
provide() {
return {
eventBus: this.eventBus
}
},
data() {
return {
eventBus: new Vue(),
msg: 'parent2son'
}
},
mounted(){
this.eventBus.$on('change',(val)=>{
this.msg = val
})
}
</script>
<!--子组件中-->
inject: ['eventBus'],
data() {
return {
msg: 'son2parent'
}
},
mounted(){
this.eventBus.$emit('change', this.msg)
}
(三)extend
<div id="mount-point"></div>
<script>
// 创建构造器
var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function () {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount('#mount-point')
</script>
(四)mixin(混入) 和 组件引用
组件在引用之后相当于在父组件内开辟了一块单独的空间,来根据父组件props过来的值进行相应的操作,单本质上两者还是泾渭分明,相对独立。
而mixins则是在引入组件之后,则是将组件内部的内容如data等方法、method等属性与父组件相应内容进行合并。相当于在引入后,父组件的各种属性方法都被扩充了。
单纯组件引用: 父组件 + 子组件 >>> 父组件 + 子组件
mixins: 父组件 + 子组件 >>> new父组件
Vue.mixin({
created: function () {
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
new Vue({
myOption: 'hello!'
})
// => "hello!"
(五)computed 和 watch 区别
- computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容。
- watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。
(六)keep-alive
如果你需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。
声明周期钩子函数
- activated: 被 keep-alive 缓存的组件激活时调用
- deactivated: 被 keep-alive 缓存的组件停用时调用
(七)v-show 与 v-if 区别
- v-show: 只是在 display: none 和 display: block 之间切换。无论初始条件是什么都会被渲染出来。
- v-if: 当属性初始为 false 时,组件就不会被渲染,直到条件为 true,并且切换条件时会触发销毁/挂载组件。
三、变化侦测
-
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。
-
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把 getter 过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
(一) 相关 API
1. ES5 的 object.defineProperty
语法
Object.defineProperty(obj, key, descriptor)
参数
obj: 要在其上定义属性的对象。
key: obj 的 key。
descriptor: key 的描述符。
返回值
修改后的 obj
属性描述符
configurable:当且仅当 configurable 为 true 时,该属性描述符(descriptor)才能够被改变,同时该属性(key)也能被删除。默认为 false。
enumerable:当且仅当 enumerable 为 true 时,该属性(key)才能够出现在对象的枚举属性中。默认为 false。
value:该 key 对应的 value。默认为 undefined。
writable:当且仅当 writable 为 true 时,value 才能被赋值运算符改变。默认为 false。
get:一个给属性提供getter的方法。
set:一个给属性提供 setter 的方法。
var a = { a: 111 }
Object.defineProperty(a, 'a', {
get: function() {
console.log('get!')
},
set: function (val) {
console.log('set ' + val + '!')
}
})
a.a // get!
a.a = 222 // set 222!
2. ES6 的 Proxy
语法
let p = new Proxy(target, handler)
参数
target:用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler:定义 target 的拦截行为
Proxy 支持的拦截操作:
- get(target, propKey, receiver):get方法用于拦截某个属性的读取操作。
- set(target, propKey, value, receiver):set方法用来拦截某个属性的赋值操作。
- apply方法拦截函数的调用、call和apply操作。
- has(target, propKey):判断对象是否具有某个属性时,这个方法会生效,返回一个布尔值。
- construct(target, args):construct方法用于拦截new命令。
- deleteProperty(target, propKey):拦截delete操作。
- defineProperty(target, propKey, propDesc):defineProperty方法拦截了Object.defineProperty操作,返回一个布尔值。
- getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor()。
- getPrototypeOf(target):拦截获取对象原型。
- isExtensible(target):拦截Object.isExtensible(proxy)。
- ownKeys(target):拦截对象自身属性的读取操作。
- preventExtensions(target):拦截Object.preventExtensions()。
- setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto)。
let handler = {
get: function(target, name){
console.log('getter!' + name)
},
set: function(target, name){
console.log('setter!' + name)
}
}
let p = new Proxy({ 'a': 1 }, handler);
p.a // getter!a
p.a = 2 // setter!a
(二) Object.defineProperty 的缺陷
如果通过下标方式修改数组数据或者给对象新增属性并不会触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。
1.Object 解决方案
1.vm.$set( target, propertyName/index, value )
在object上设置一个属性,如果object是响应式的,则该属性被创建后也是响应式的。
var vm = new Vue({
data:{
obj: {
a: 1
}
}
}) // `vm.obj.a` 是响应式的
vm.obj.b = 2 // `vm.obj.b` 是非响应式的
this.$set(this.obj,'c',3) // `vm.obj.c` 是响应式的
2.vm.$watch( expOrFn, callback, [options] )
- callback:参数为新值和旧值。
- options - deep:为了发现对象内部值的变化,可以在选项参数中指定 deep: true 。注意监听数组的变动不需要这么做。
- options - immediate:在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调
vm.$watch('a.b.c', function (newVal, oldVal) {
// 做点什么
})
3.vm.$delete( target, propertyName/index )
删除数据中的某个属性,并且此时vue可以侦测到该数据发生了变化。
2.Array 解决方案
改变自身数组内容的方法有7个,分别是push、pop、shift、unshift、splice、sort、reserve,针对需要侦测变化的数据,使覆盖原型方法。
四、虚拟 DOM
Vue.js 中通过模板来描述状态与视图之间的映射关系,先将模板编译成渲染函数,然后执行渲染函数生成的虚拟节点,最后使用虚拟节点更新视图。
虚拟DOM在Vue.js中所做的事是提供虚拟节点vnode和对新旧两个vnode进行对比,并根据对比结果进行DOM操作来更新视图。
class VNode {
constructor(tag, children, text) {
this.tag = tag
this.text = text
this.children = children
}
render() {
if(this.tag === '#text') {
return document.createTextNode(this.text)
}
let el = document.createElement(this.tag)
this.children.forEach(vChild => {
el.appendChild(vChild.render())
})
return el
}
}
function v(tag, children, text) {
if(typeof children === 'string') {
text = children
children = []
}
return new VNode(tag, children, text)
}
(一) Vnode
- 注释节点
- 文本节点
- 克隆节点
- 元素节点
- tag:节点名称,如p、ul、li 和 div等
- data:节点上的数据,如attrs、class、style等
- children:当前节点的子节点列表
- context:当前组件的Vue.js实例
- 组件节点 和元素节点类似,有以下两个独有属性
- componentOptions:组件节点的选项参数,propsData、tag、children等
- componentOptions:组件的实例
- 函数式节点 和组件节点类似,有以下两个独有属性 functionalContext 和 functionalOptions
(二) patch
1.判断新旧节点的 tagName 是否相同,如果不相同的话就代表节点被替换了。如果没有更改 tagName 的话,就需要判断是否有子元素。
2.第二步算法中,我们需要判断原本的列表中是否有节点被移除,在新的列表中需要判断是否有新的节点加入,还需要判断节点是否有移动。那么在实际的算法中,我们如何去识别改动的是哪个节点呢?这就引入了 key 这个属性,想必大家在 Vue 或者 React 的列表中都用过这个属性。这个属性是用来给每一个节点打标志的,用于判断是否是同一个节点。当然在判断以上差异的过程中,我们还需要判断节点的属性是否有变化等等。
3.当我们判断出以上的差异后,就可以把这些差异记录下来。当对比完两棵树以后,就可以通过差异去局部更新 DOM,实现性能的最优化。
(三) 优点
- 通过 Virtual DOM 我们可以渲染到其他的平台,比如实现 SSR、同构渲染等等。 实现组件的高度抽象化
- 提高性能
五、模板编译
直接把模板丢到浏览器中肯定是不能运行的,模板只是为了方便开发者进行开发。Vue 会通过编译器将模板通过几个阶段最终编译为 render 函数,然后通过执行 render 函数生成 Virtual DOM 最终映射为真实 DOM。
1.将模板解析为抽象语法树(AST)
2.优化 AST
3.将 AST 转换为 render 函数
六、nextTick 原理
- vue用异步队列的方式来控制DOM更新和nextTick回调先后执行。
- microtask因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕。
- 因为兼容性问题,vue不得不做了microtask向macrotask的降级方案。
七、前端路由原理
前端路由:监听 URL 的变化,然后匹配路由规则,显示相应的页面。
1. Hash 模式
www.test.com/#/ 就是 Hash URL,当 # 后面的哈希值发生变化时,可以通过 hashchange 事件来监听到 URL 的变化,从而进行跳转页面,并且无论哈希值如何变化,服务端接收到的 URL 请求永远是 www.test.com。
2. History 模式
主要使用 history.pushState 和 history.replaceState 改变 URL。通过 History 模式改变 URL 同样不会引起页面的刷新,只会更新浏览器的历史记录。
3. 区别
1.URL
- Hash 模式只可以更改 # 后面的内容
- History 模式可以通过 API 设置任意的同源 URL History 模式可以通过 API 添加任意类型的数据到历史记录中,Hash 模式只能更改哈希值
2.兼容性
- Hash 模式无需后端配置,并且兼容性好
- History 模式在用户手动输入地址或者刷新页面的时候会发起 URL 请求,后端需要配置 index.html 页面用于匹配不到静态资源的时候
4. 路由守卫
全局守卫:
- 全局前置守卫 beforeEach (to, from, next)
- 全局解析守卫 beforeResolve
- 全局后置钩子 afterEach(to, from)
路由独享的守卫
- beforeEnter(to, from, next)
组件内守卫:
- 到达这个组件 beforeRouteEnter:(to,from,next)=>{}
- 在当前路由改变,但该组件被复用 beforeRouteUpdate (to, from, next) {}
- 离开这个组件时,beforeRouteLeave:(to,from,next)=>{}
5. 路由传参
path:'/:name'
this.$route.params.name
八、vuex
- vuex是一个专为vue.js应用程序开发的状态管理模式
- 五大核心属性
- state:存储数据,存储状态
this.$store.state
- getter:可以认为是 store 的计算属性,它的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
- mutation:更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
this.$store.commit('state', XXX)
- action:包含任意异步操作,通过提交 mutation 间接更变状态。
this.$store.dispatch(XXX)
- module:将 store 分割成模块,每个模块都具有state、mutation、action、getter、甚至是嵌套子模块。
九、双向绑定

<input type="text" :value="name" @input="name = $event.target.value">