一、router
1.1 History的问题
History模式需要服务器的支持
在单页面应用中, 访问http://www.test.com/login, 服务端不存在这样的地址会返回找不到该页面。
前端虽然借助了router实现了无刷新路由跳转,但是一旦进行刷新那么就会去请求服务器,就会返回404
在脚手架中不存在这个问题,因为它默认帮我们处理了这个问题。
hash模式不存在这个问题
演示
借助Node服务器,部署我们打包好的项目
const path = require('path')
const express = require('express')
// const history = require('connect-history-api-fallback') // 导入处理history模式的模块
const app = express()
// 注册处理sistory模式的中间件
// app.use(history())
// 处理静态资源
app.use(express.static(path.join(__dirname, './web')))
app.listen(3000, () => {
console.log('服务器在3000端口启动成功~')
})
访问,报错:Cannot GET /detail/14
而当启用了处理这history的中间件后,再刷新就不会报错了
解决方案:刷新时默认会去请求服务器,而如果服务器找不到对应url的资源,返回index.html。这样子路由切换就可以交回到前端处理
1.2 hash模式的原理
前端路由是如何做到URL和内容进行映射呢?
- URL中#后内容作为路径地址
- 监听hashchange事件
- 根据当前路由地址找到对应组件重新渲染
URL的hash也就是锚点(#),本质上是改变window.location的href属性;我们也可以通过直接赋值location.hash来改变href,但是页面不发生刷新
onHashchange事件可以监听到location.hash的变化,从而进行替换页面内容的DOM操作
<div id="app">
<a href="#/home">home</a>
<a href="#/about">about</a>
<div class="content">default</div>
<script>
const content = document.querySelector(".content")
window.addEventListener("hashchange",()=>{
switch(location.hash ) {
case "#/home":
content.innerHTML = "Home";
break;
case "#/about" :
content.innerHTML = "About"
break;
default:
content.innerHTML = "Default"
}
})
</script>
hash的优势就是兼容性更好,但是缺陷是有一个#,显得不像一个真实的路径。
1.3 history模式的原理
原理
- 通过
history.pushState()方法改变地址栏 - 监听popState事件(该事件可以监听浏览器历史操作的变化)
- 根据当前路由地址找到对应组件重新渲染
分析
1、vue.use(),如果传递一个函数,则调用函数;如果传递对象,则调用对象中的install方法
所以VueRouter中需要有一个install方法
2、VueRouter应该是一个类,或是一个构造函数
VueRouter类图
install方法
//VueRouter/index.js
let _Vue = null
export default class VueRouter {
static install(Vue) {
// 1.判断当前插件是否已经被安装
if(VueRouter.install.installed) return
VueRouter.install.installed = true
// 2.把Vue构造函数记录到全局变量
_Vue = Vue
// 3.把创建Vue实例时候传入的router对象注入到Vue示例上
_Vue.mixin({
beforeCreate() {
// 只需全局组件获得$router即可
if(this.$options.router) {
_Vue.prototype.$router = this.$options.router
}
}
})
}
}
constructor
需初始化三个对象:options、routeMap、data
options:记录传入的选项
routeMap:保存路径与组件的映射关系
data:是一个响应式对象,其中有一个current属性表示当前的路径地址
export default class VueRouter {
static install(Vue) { ... }
constructor(options) {
this.options = options
this.routeMaps = {}
this.data = _Vue.observable({
current: '/'
})
}
createRouteMap
遍历所有的路由规则,把路由规则解析成键值对的形式,存储到routeMap中
createRouteMap() {
this.options.routes.forEach(route => {
this.routeMaps[route.path] = route.component
})
}
initComponents
该方法用于创建router-link和router-view两个组件
let _Vue = null
export default class VueRouter {
static install(Vue) {
// 1.判断当前插件是否已经被安装
// 2.把Vue构造函数记录到全局变量
// 3.把创建Vue实例时候传入的router对象注入到Vue示例上
_Vue.mixin({
beforeCreate() {
// 只需全局组件获得$router即可
if(this.$options.router) {
_Vue.prototype.$router = this.$options.router
this.$options.router.init() //调用
}
}
})
}
constructor(options) {
//...
}
init() {
this.createRouteMap()
this.initComponents(_Vue)
}
createRouteMap() {
//....
}
initComponents(Vue) {
Vue.component('router-link', {
props: {
to: String
},
template: '<a :href="to"><slot></slot></a>'
})
}
}
测试
import Vue from "vue";
import VueRouter from "../VueRouter/index";
Vue.use(VueRouter)
const routes = [
//...
]
const router = new VueRouter({
mode: 'history',
routes
})
export default router
但是页面报错了
错误原因在于vue-cli创建的项目是运行时版本的vue,不支持template模板
Vue的构建版本
1、运行时版:不支持template模板,需要打包的时候提前编译
2、完整版:包含运行时和编译器,体积比运行时版大10k左右,程序运行的时候把模板转换成render函数
所以我们需要将版本切换为完整版的,只需在webpack中配置runtimeCompiler: true
当然,如果要使用运行时版本的Vue也可以的,只需要将template模板的内容写成render函数
initComponents(Vue) {
Vue.component('router-link', {
props: {
to: String
},
// template: '<a :href="to"><slot></slot></a>'
render(h) {
return h('a', {
attrs: {
href: this.to
}
}, [this.$slots.default])
}
})
}
创建router-view组件
Vue.component('router-view', {
render(h) {
const component = self.routeMap[self.data.current]
return h(component)
}
})
但是当我们单间切换路由时,页面发生了刷新。且没有切换到对应的数据上.所以我们需要a标签的默认行为,并为其绑定事件
initComponents(Vue) {
Vue.component('router-link', {
props: {
to: String
},
// template: '<a :href="to"><slot></slot></a>'
render(h) {
return h('a', {
attrs: {
href: this.to
},
on: {
click: this.clickHandler
}
}, [this.$slots.default])
},
methods: {
clickHandler (e) {
history.pushState({}, "", this.to)
this.$router.data.current = this.to
e.preventDefault()
}
}
})
const self = this
Vue.component('router-view', {
render(h) {
const component = self.routeMaps[self.data.current]
return h(component)
}
})
}
initEvent
initEvent() {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
完整代码
let _Vue = null
export default class VueRouter {
static install(Vue) {
// 1.判断当前插件是否已经被安装
if(VueRouter.install.installed) return
VueRouter.install.installed = true
// 2.把Vue构造函数记录到全局变量
_Vue = Vue
// 3.把创建Vue实例时候传入的router对象注入到Vue示例上
_Vue.mixin({
beforeCreate() {
// 只需全局组件获得$router即可
if(this.$options.router) {
_Vue.prototype.$router = this.$options.router
this.$options.router.init()
}
}
})
}
constructor(options) {
this.options = options
this.routeMaps = {}
this.data = _Vue.observable({
current: '/'
})
}
init() {
this.createRouteMap()
this.initComponents(_Vue)
this.initEvent()
}
createRouteMap() {
this.options.routes.forEach(route => {
this.routeMaps[route.path] = route.component
})
}
initComponents(Vue) {
Vue.component('router-link', {
props: {
to: String
},
// template: '<a :href="to"><slot></slot></a>'
render(h) {
return h('a', {
attrs: {
href: this.to
},
on: {
click: this.clickHandler
}
}, [this.$slots.default])
},
methods: {
clickHandler (e) {
history.pushState({}, "", this.to)
this.$router.data.current = this.to
e.preventDefault()
}
}
})
const self = this
Vue.component('router-view', {
render(h) {
const component = self.routeMaps[self.data.current]
return h(component)
}
})
}
initEvent() {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
}
二、vue响应式原理
vue.js 采⽤数据劫持的⽅式,结合发布者-订阅者模式,通过Object.defineProperty()来劫持各个属性
的setter,getter以监听属性的变动,在数据变动时发布消息给订阅者,触发相应的监听回调
概念梳理
- 数据响应式
数据模型仅仅是普通的js对象,当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率
- 双向绑定
数据改变,视图改变;视图改变,数据也随之改变
- 数据驱动
Vue最独特的特性之一,开发过程中仅需关注数据本身,不需要关心数据是如何渲染到视图的
2.1 模拟响应式核心原理
vue2.x使用的是Object.defineProperty
<div id="root"></div>
<script>
// 模拟Vue中的data选项
const data = {
name: 'linzm',
age: 25
}
// 模拟Vue实例
let vm = {}
// 数据劫持
proxyData(data)
function proxyData() {
// 遍历data对象的所有属性
Object.keys(data).forEach(key => {
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get() {
console.log('访问数据')
return data[key]
},
set(newValue) {
console.log('设置数据')
if(data[key] === newValue) return
data[key] = newValue
document.getElementById('root').textContent = data[key]
}
})
})
}
</script>
而在vue3中使用了proxy进行代理
<div id="root">ddddd</div>
<script>
// 模拟Vue中的data选项
const data = {
name: 'linzm',
age: 25
}
// 数据劫持
let vm = new Proxy(data, {
get(target, key) {
console.log('访问数据')
return target[key]
},
set(target, key, newValue) {
console.log('设置数据')
if(target[key] === newValue) return
target[key] = newValue
document.getElementById('root').textContent = target[key]
}
})
</script>
2.2 发布订阅模式和观察者模式
发布订阅者和观察者是不同的,主要区别是观察者没有信号中心
1、发布/订阅模式
- 订阅者(家长想要知道孩子的考试成绩)
- 发布者(老师将成绩发布给家长)
- 信号中心
我们假定,存在一个“信号中心",某个任务执行完成,就向信号中心”发布“一个信号,其他任务可以向信号中心”订阅“这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式“
事件触发器
class EventEmitter {
constructor() {
// 对象,用于存储事件与处理程序的映射关系
// {'click': [fn1, fn2], 'change': [fn]}
this.subs = Object.create(null)
}
// 注册事件(订阅消息)
$on(eventType, handler) {
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
}
// 触发事件(发布消息)
$emit(eventType) {
if(this.subs[eventType]) {
this.subs[eventType].forEach(handler => {
handler()
})
}
}
}
// 测试
const em = new EventEmitter()
em.$on('click', () => {
console.log('事件处理程序')
})
em.$emit('click')
2、观察者模式
观察者(订阅者)——watcher
- update():当事件发生时,具体要做的事情
目标(发布者)——Dep
- subs数组:存储所有的观察者
- addSub():添加观察者
- notify():当事件发生,调用所有观察者的update方法
没有事件中心
// 发布者-目标
class Dep {
constructor() {
// 记录所有的订阅者
this.subs = []
}
// 添加订阅者
addSub(sub) {
if(sub && sub.update) {
this.subs.push(sub)
}
}
// 发布通知
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 订阅者-观察者
class Watcher {
update() {
console.log('update')
}
}
const d = new Dep()
const w = new Watcher()
d.addSub(w)
d.notify()
总结
3.3 实现过程
功能:
- 负责接收初始化的参数(选项)
- 负责把data中的属性注入到Vue实例,转换成getter/setter
- 负责调用observer监听data中所有属性的变化
- 负责调用compiler解析指令/差值表达式
Vue的类图
class Vue {
constructor(options) {
// 1.通过属性保存选项的数据
// 2.把data中的成员转换成getter、setter,注入到vue实例中
// 3.调用observer对象,监听数据的变化
// 4.调用compiler对象,解析指令和差值表达式
}
_proxyData(data) {}
}
class Vue {
constructor(options) {
// 1.通过属性保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2.把data中的成员转换成getter、setter,注入到vue实例中
this._proxyData(this.$data)
// 3.调用observer对象,监听数据的变化
// 4.调用compiler对象,解析指令和差值表达式
}
_proxyData(data) {
// 遍历data中所有的属性
Object.keys(data).forEach(key => {
// this 指向vue实例,这里将向实例注入data的属性
Object.defineProperty(this, key, {
configurable: true,
enumerable: true,
get() {
return data[key]
},
set(newValue) {
if(newValue === Object[key]) return
Object[key] = newValue
}
})
})
}
}
测试
<div id="app"></div>
<script src="./js/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
count: 10,
msg: 'hello'
}
})
console.log(vm)
</script>
可看到Vue实例创建成功了,但是$data的属性却不是响应式的,我们需要调用observer将其进行转换
observer
observer负责把data中的数据转换成响应式
class Observer {
constructor(data) {
this.walk(data)
}
walk(data) {
// 1.判断data是否是对象
if(!data || typeof data !== 'object') return
// 2.遍历data对象的所有属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(obj, key, val) {
let that = this
this.walk(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return val
},
set(newValue) {
if(newValue === val) return
val = newValue
that.walk(val)
// 发送通知
}
})
}
}
//vue.js
class Vue {
constructor(options) {
// 3.调用observer对象,监听数据的变化
new Observer(this.$data)
}
}
compiler
负责编译模板,解析指令/差值表达式、负责页面的初次渲染,当数据变化后要重新渲染视图
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
}
// 编译模板,处理文本节点和元素节点
compile(el) {}
// 编译元素节点,处理指令
compileElement(node) {}
// 编译文本节点,处理差值表达式
compileText(node) {}
// 判断元素属性是否是指令
isDirective(attrName) {
return attrName.startsWith('v-')
}
// 判断节点是否是文本节点
isTextNode(node) {
return node.nodeType === 3
}
// 判断节点是否是元素节点
isElementNode(node) {
return node.nodeType === 1
}
}
处理文本节点
class Compiler {
constructor(vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 编译模板,处理文本节点和元素节点
compile(el) {
let childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
// 处理文本节点
if(this.isTextNode(node)) {
this.compileText(node)
} else if(this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node)
}
// 判断node节点,是否有子节点,如果有,要递归调用compile
if(node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点,处理指令
compileElement(node) {}
// 编译文本节点,处理差值表达式
compileText(node) {
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if(reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
}
}
}
在vue.js中调用一次
class Vue {
constructor(options) {
// 4.调用compiler对象,解析指令和差值表达式
new Compiler(this)
}
}
Dep
功能:收集依赖,添加观察者
class Dep {
constructor() {
// 存储所有的观察者
this.subs = []
}
// 添加观察者
addSub (sub) {
if(sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
watcher对象
功能:当数据变化触发依赖,dep通知所有的watcher实例更新视图、自身实例化的时候往dep对象中添加自己
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
// data中属性名称
this.key = key
// 回调函数负责更新视图
this.cb = cb
// 把wathcer对象记录到dep类的静态属性target
Dep.target = this
// 触发get方法,在get方法中调用addSub
this.oldValue = vm[key]
Dep.target = null
}
// 当数据发生变化的时候更新视图
update() {
let newValue = this.vm[this.key]
if(this.oldValue === newValue) return
this.cb(newValue)
}
}
//compiler.js
// 编译文本节点,处理差值表达式
compileText(node) {
// ....
// 创建watcher对象,当数据改变更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
总结
1、创建实例,将传递进来的options记录下来,调用_proxyData方法将options的data注入到Vue实例中
2、调用Observer方法,将data中的数据转换成getter和setter
3、当数据发生变化,调用setter去通知Dep,并调用Dep的notify方法
4、notify方法发送通知watcher(当我们创建watcher时,会将watcher添加到Dep的subs中,收集依赖)
5、watcher的update方法便会去更新视图(注:页面首次加载时调用的是compiler更新视图)
三、Vuex
3.1 基本用法
state
一般写法
<div id="app">
{{$store.state.name}}
{{this.$store.state.name}}
</div>
简洁的写法
<template>
<div id="app">
<div>简洁的写法</div>
<div>{{name}}</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
// name: state => state.name
...mapState(['name', 'count'])
// 防止命名冲突,可以传入对象
// ...mapState({myName: 'name'})
}
}
</script>
getters
<template>
<div id="app">
<!-- 一般写法 -->
{{$store.getters.reversename}}
<!-- 简洁写法 -->
{{reversename}}
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
...mapGetters(['reversename'])
}
}
</script>
mutations
<template>
<div id="app">
{{count}}
<!-- 一般写法 -->
<button @click="$store.commit('increment', 10)">++</button>
<!-- 简洁写法 -->
<button @click="increment(10)">++</button>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
export default {
computed: {
// name: state => state.name
...mapState(['name', 'count']),
},
methods: {
...mapMutations(['increment'])
}
}
</script>
actions
定义
actions: {
incrementAction(content, payload) {
setTimeout(() => {
content.commit('increment', payload)
}, 2000);
}
}
使用
<template>
<div id="app">
{{count}}
<!-- 一般写法 -->
<button @click="$store.dispatch('incrementAction', 10)">++</button>
<!-- 简洁写法 -->
<button @click="incrementAction(33)">++</button>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
computed: {
// name: state => state.name
...mapState(['name', 'count']),
},
methods: {
...mapActions(['incrementAction'])
}
}
</script>
module
// cart
export default {
state: {
number: 999
},
mutations: {
decrement(state, payload) {
state.number -= payload
}
},
actions: {},
}
使用
<template>
<div id="app">
<!-- 一般用法 -->
{{$store.state.cart.number}}
<button @click="$store.commit('decrement',99)">--</button>
</div>
</template>
如果我们希望使用map映射,使代码更简洁,可以添加命名空间
//cart
export default {
namespaced : true,
}
<template>
<div id="app">
<!-- 简洁用法 -->
{{number}}
<button @click="decrement(99)">--</button>
</div>
</template>
<script>
import { mapState, mapMutations } from 'vuex'
export default {
computed: {
...mapState('cart', ['number'])
},
methods: {
...mapMutations('cart', ['decrement'])
}
}
</script>
3.2 Vuex原理
new Vuex.Store({...})
可知:Vuex是一个对象,Store是一个类
Vue.use(Vuex)
Vuex中还需要一个install函数,当注册时会调用该函数
基本结构
let _Vue = null
class Store {
}
function install (Vue) {
_Vue = Vue
}
export default {
Store,
install
}
install
当Vue实例创建时,将传入的store对象,注入到Vue原型中$store
function install (Vue) {
_Vue = Vue
_Vue.minin({
beforeCreate() {
if(this.$options.store) {
_Vue.prototype.$store = this.$options.store
}
}
})
}
完整代码
let _Vue = null
class Store {
constructor(options) {
const {
state = {},
getters = {},
mutations = {},
actions = {}
} = options
this.state = _Vue.observable(state)
this.getters = Object.create(null)
Object.keys(getters).forEach(key => {
Object.defineProperty(this.getters, key, {
get: () => getters[key](state)
})
})
this._mutations = mutations
this._actions = actions
}
commit(type, payload) {
this._mutations[type](this.state, payload)
}
dispatch(type, payload) {
this._actions[type](this, payload)
}
}
function install (Vue) {
_Vue = Vue
_Vue.mixin({
beforeCreate() {
if(this.$options.store) {
_Vue.prototype.$store = this.$options.store
}
}
})
}
export default {
Store,
install
}
四、面试要点
1、v-model的实现原理
v-model其实是一个语法糖,它实际上是做了两步动作:
1、绑定数据value
2、触发输入事件input
<div id="app">
<input type="text" :value="message" @input="change">
<div>{{message}}</div>
</div>
<script>
const app = new Vue({
el:"#app",
data () {
return {
message:""
}
},
methods:{
change(event) {
this.message = event.target.value
}
}
})
</script>
2、生命周期函数
vue实例有一个完整的生命周期,包括了开始创建、初始化数据、编译模板、挂载DOM、渲染-更新-渲染、卸载。
这一整个流程称为Vue实例的生命周期,而钩子函数给了我们在某个阶段处理代码的机会。
| 生命周期钩子 | 详细内容 |
|---|---|
| beforeCreate | 实例初始化之后,数据观测(data observer)和事件配置(event/watcher)之前调用。注:此时组件的选项对象还未建立,el和data还未初始化,因此无法访问methods,data,computed等上的方法和数据 |
| created | 实例已经创建完成之后被调用。在这一步,实例已完成一下配置:数据观测(data observer),属性和方法的运行,watch/event事件回调。然而,挂载阶段还没开始,$el属性目前不可见 |
| beforeMount | 挂载开始之前被调用,相关的render函数首次被调用(虚拟DOM),实例已完成以下配置:编译模板、把data里面的数据和模板生成了html,完成了el和data初始化,但是此时还没有挂载到html页面上。 |
| mounted | 挂载完成。el被新创建的vm.$el替换,并挂载到实例上去后调用该钩子函数。mounted只会执行一次。可以做一些ajax操作 |
| beforeUpdate | 数据更新之前被调用,发生在虚拟DOM重新渲染和打补丁之前,可以在该钩子中进一步更改状态,这不会触发附加的重渲染过程 |
| updated | 由于数据更改导致的虚拟DOM重新渲染和打补丁,调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。应避免在此期更改状态,可能会导致更新无线循环。 |
| beforeDestroy | 实例销毁之前调用,在这一步,实例仍然完全可用。这一步还可以使用this来获取实例,可以做以下重置操作,比如清除掉组件中的定时器和监听的dom事件 |
| destroyed | Vue实例销毁后调用。调用后,实例指示的所有东西都会解除绑定,所有事件监听器都会移除,所有子实例也会被销毁 |
(除了beforeCreate和created钩子之外,其他钩子均在服务器端渲染期间不被调用)
补充
1、created是一个比较常用的生命周期函数,可以调用methods方法,改变data数据,获取methods中的计算属性等等,通常可以在这个函数中对实例进行预处理。
ajax请求最好放在created里面,因为此时已经可以访问this了,请求到数据就可以直接放在data中
2、在mouted钩子函数中,对挂载的dom进行操作。
关于DOM的操作要放在mouted中,在mouted前面访问DOM都是undefined
3、data为什么是一个函数
vue实例的data必须是一个函数,这是js本身的特性带来的,跟vue本身设计无关。
理解:
组件时可复用的vue实例,一个组件被创建好之后,可能被用在各个地方。而组件管被复用了多少次,其内部的data数据都应该是互相隔离,互不影响的。
如果data是个对象
Object是引用数据类型,如果不用function返回,组件中的data保存着一个内存地址,当存在多个组件时,这些组件的data都指向同一个内存地址,改变其中一个,其他的也会被改变。
如果data是一个函数
JavaScript只有函数构成作用域(只有函数{}构成作用域,对象{}以及if(){}都不构成作用域),data是一个函数时,每个组件实例都有自己的作用域,每个实例相互独立,不会相互影响。
4、虚拟DOM
分析虚拟DOM之前,先来看看真实DOM的渲染
<html>
<head>
<title>Document</title>
</head>
<body>
<h1>hello world</h1>
</body>
</html>
以上代码经过浏览器解析,会形成一颗DOM树
虚拟DOM的优势
目前的框架都引入了虚拟DOM来对真实DOM进行抽象,这样做有很多的好处:
1、首先可以对真实的元素节点进行抽象,抽象成VNode(虚拟节点),这样方便后续对其进行各种操作
2、其次是方便实现跨平台,可以将VNode节点渲染成任意想要的节点,vue允许开发者开发自己的渲染器(renderer),在其他的平台上渲染
3、在传统开发的模式中,假设需要更新10个DOM节点,浏览器收到第一个DOM请求后马上执行流程,最终执行10次,真实DOM增删改会引起浏览器的回流和重绘,效率很低;
虚拟DOM是用js对象模拟出来的一个DOM,将页面中内容的改变先应用到DOM上,对虚拟DOM修改时不会引起回流和重绘,然后通过diff算法比较虚拟DOM和真实DOM的不同,找出最小变更,再把这些变更写入真实DOM中,减少了实际DOM操作次数,性能会有较大的提升
5、computed与watch
当一个值A,是同时受到B和C影响的,这个时候应该使用computed;
当一个值A发生变化后,要执行一系列操作,这个时候应该使用watch
计算属性computed
1、支持缓存,只有依赖的数据发生改变时,才会重新进行计算
2、不支持异步,当computed内有异步操作时无效,无法监听数据的变化
3、在computed中,属性都有一个get和set方法,当数据变化时,调用set方法
侦听属性watch
1、不支持缓存,可以监听某一个数据,然后执行相应的操作
2、支持异步操作
3、监听的函数接收两个参数,参数一newValue,参数二oldValue
4、监听的数据必须是data中声明过或者父组件传递过来的props中的数据。监听对象时,默认是监听 其引用,当内部的值发生变化时,无法监听到(使用deep深度监听解决)
6、v-for的key的作用
在使用v-for进行列表渲染时,我们通常会给元素或者组件绑定一个key属性
这个key有什么作用呢?官方的解释:
1、key属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识vNodes
2、不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法
3、使用key,它会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素
案例分析
在数组letters中间添加一个新的元素f
<body>
<div id="app"></div>
<!-- 模板 -->
<template id="my-app">
<ul>
<li v-for="item in letters">{{item}}</li>
</ul>
<button @click="insertF">添加</button>
</template>
<script>
const App = {
template:'#my-app',
data() {
return {
letters: ['a', 'b', 'c', 'd']
}
},
methods:{
insertF() {
this.letters.splice(2,0,'f')
}
}
}
Vue.createApp(App).mount('#app')
</script>
</body>
对于这次更新ul和button是不需要进行更新的,需要更新的是li列表
要完成这一步操作,有三种方法
1、直接生成一个新的vnode,整个替换掉旧的vnode
2、将c、d位置内容改成f、c,并在末尾添加一个元素d
3、直接往中间添加f元素,其他元素依次往后移动一个位置
那么Vue中对于这种情况的更新是如何操作的呢?
Vue会根据有没有key,调用两个不同的方法
1、有key,那么就使用patchKeyedChildren方法
2、没有key,那么就使用patchUnkeyedChildren方法
patchUnkeyedChildren
没有key的情况下,采用该方法,具体步骤可以参考mini-vue的patch函数的实现
const patch = (n1, n2) => {
if (n1.tag !== n2.tag) { //类型不同
const n1ElParent = n1.el.parentElement
//直接删除掉n1,并将n2挂载到原n1位置
n1ElParent.removeChild(n1.el)
mount(n2, n1ElParent)
} else { //类型是相同的
// 1.取出element对象,并且在n2中进行保存
const el = n2.el = n1.el
// 2.处理props
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 2.1 获取所有的newProps,并添加进el中
for (const key in newProps) {
const newValue = newProps[key]
const oldValue = oldProps[key] //如果oldValue有值,说明新旧n1、n2存在相同的属性
if (newValue !== oldValue) {
//将不相同的属性进行添加操作
if (key.startsWith("on")) {
el.addEventListener(key.slice(2).toLowerCase(), newValue)
} else {
el.setAttribute(key, newValue)
}
}
}
// 2.2 删除旧的props
for (const key in oldProps) {
//如果存在旧n1的属性在n2中,进行删除操作
if(key.startsWith('on')) {
const value = oldProps[key]
el.removeEventListener(key.slice(2).toLowerCase(), value)
}
if(!(key in newProps)) {
el.removeAttribute(key)
}
}
// 3.处理children
const newChildren = n2.children || []
const oldChildren = n1.children || []
// 3.1 新节点是一个文本
if (typeof newChildren === 'string') {
// 替换掉旧el的整个innerHTML
if (typeof oldChildren === 'string') {
if (newChildren !== oldChildren) {
el.textContent = newChildren
} } else {
el.innerHTML = newChildren
}
} else {//3.2 新节点是一个数组
//3.2.1 旧节点是一个文本
if (typeof oldChildren === 'string') {
el.innerHTML ="" //清空
newChildren.forEach(item => {
mount(item, el) // 递归 :将每一个item挂载到el上
})
} else {
//3.2.2旧节点是一个数组
//情况一
//oldChildren: [v1, v2, v3]
//newChildren: [v1, v5, v6]
const commonLength = Math.min(oldChildren.length, newChildren.length)
for (let i = 0; i < commonLength; i++) {
// 有相同节点的元素进行patch操作
patch(oldChildren[i], newChildren[i])
}
////情况二:newChildren.length > oldChildren.length
//oldChildren: [v1, v2, v3]
//newChildren: [v1, v5, v6, v7, v8]
if (newChildren.length > oldChildren.length) {
//newChildren: [v7, v8] 添加至el上
newChildren.slice(oldChildren.length).forEach(item => {
mount(item, el)
})
}
// 情况三:newChildren.length < oldChildren.length
//oldChildren: [v1, v2, v3, v7, v8]
//newChildren: [v1, v5, v6]
if (newChildren.length < oldChildren.length) {
//oldChildren: [v7, v8]进行移除
oldChildren.slice(newChildren.length).forEach(item => {
el.removeChild(item.el)
})
}
}
}
}
}
总的来说,如有没有key,就对children进行三种情况的对比
情况一:新旧节点个数相同,递归调用patch
情况二:旧节点大于新节点,移除多余的节点
情况三:新节点大于旧节点,挂载新节点
patchKeyedChildren
如果有key的情况下,每次进行patch,不仅要判断新旧node的类型,还要(&&)对比两者的key
大致分成五步:
第一,从头到尾开始两两对比,如果不同(类型不同或者key不同),跳出while循环
第二,从末尾到头部开始对比,同理遇见不同,跳出循环
第三,如果旧的节点,遍历完,还有新的节点,那么新增多的新节点
第四,如果新的节点,遍历完,还有旧的节点,那么移除多的旧节点
第五,考虑到中间可能是乱序的,vue会尽可能在新旧node中,找到相同的,然后对不同的进行patch
所以,vue在进行diff算法的时候,会尽量利用key进行优化操作,没有key的时候效率是非常低的。
在进行插入或者重置顺序的时候,保持相同的key可以让diff算法更加高效
7、nexttick
官方解释:将回调推迟到下一个DOM更新周期之后执行。在更改了一些数据以及等待DOM更新后立即使用它。
案例分析
假设宽度固定的div中,有一个空字符串。当第一次为文本增加了内容后,div有了高度offsetHeight。(不断增加文本后,div会换行,高度会不断增加)
<template>
<div class="box" ref="box">{{ message }}</div>
<button @click="addMsg">增加</button>
</template>
<script>
export default {
data() {
return {
message: "",
};
},
methods: {
addMsg() {
this.message += "哈哈哈";
// 获取高度
console.log(this.$refs.box.offsetHeight); // 第一次,输出:0
},
},
updated() {
console.log(this.$refs.box.offsetHeight); //第一次,输出:41
}
}
</script>
说明了当div有了高度,第一次在methods获取到的是DOM数据更新之前的高度
当div有了高度,第一次在updated获取到的是DOM数据更新之后的高度
显然,我们经常需要获取数据更新之后的数据(即updated中的数据),但是我们不能经常在updated回调函数中获取,因为只要页面数据发生变化,都会触发updated,在updated中定义代码A后,有时候其他无关的数据B变化,updated会执行一次。显然这个时候并不需要执行我们的A代码,但它还是运行了
nexttick可以解决上方的问题
//正确的形式
addMsg() {
this.message += "哈哈哈"; //数据发生变化
//更新DOM
console.log(this.$refs.box.offsetHeight); //最终获取到正确的结果
},
//使用$nexttick
addMsg() {
this.message += "哈哈哈";
this.$nextTick(() => {
console.log(this.$refs.box.offsetHeight); //输出41
});
},
nexttick原理
理解nexttick需要从事件循环的角度理解
addMsg() {
this.message += "哈哈哈"; // 更新数据
console.log(this.$refs.box.offsetHeight);
},
当我们更新数据时,Vue会往微任务队列里边添加三个微任务
// 1. watch(回调函数) preQueue
// 2. 组件的更新 update jobQueue
// 3.声明周期回调 postQueue
认真想一下,当以上三个微任务尚未执行,console.log(this.$refs.box.offsetHeight); 已经放入调用栈执行,结果肯定是0
而nexttick做的事情简单,它将执行的时机延迟到这三个微任务执行之后(包裹一个Promise,因为Pomise的then回调默认会放进微任务队列)
所以,nexttick的回调函数的最终执行时机
//1....
//2... 更新DOM
//3...
//4.nexttick的回调,此时已经能够获取到正确的数据
8、vue的优点与不足
优点
1、轻量级的框架,只关注视图层,是一个构建数据的视图结合,大小只有几十KB
2、简单易学,国人开发,中文文档,不存在语言障碍,便于理解和学习
3、双向数据绑定,在数据操作中较为简单
4、组件化,用可复用、解耦的组件来构建页面
5、数据驱动:自动计算属性和追踪依赖的模板表达式
6、虚拟DOM:DOM操作十分消耗性能,vue不再使用原生的dom操作节点,极大地解放了dom操作,但具体操作还是dom,只不过换了一种方式
缺点
1、大量地进行封装,不利于SEO优化
2、报错不明显,适合单人开发,适合中小型项目
9、vue源码解析
vue的源码包括三大核心:
1、Compiler模块:编译模板系统
2、Runtime模块:也可以称之为Renderer模块,真正渲染的模块
3、Reactivity模块:响应式系统
10、diff算法
1、为什么要用Diff算法
由于在浏览器中操作DOM的代价是非常“昂贵”的,十分消耗性能(开销十分大),所以Vue才引入了虚拟DOM(Virtual DOM),xuniDOM是对真实DOM的一种抽象描述
但是即使使用了虚拟DOM来进行真是DOM的渲染,在页面更新的时候,而不能全量地将整颗虚拟DOM进行渲染,而是去渲染改变的部分,这个时候就需要计算xuniDOM树改变部分的算法了,这就个算法就是Diff算法
diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁
11、导航守卫
通俗的讲,导航守卫就是路由跳转过程中的一些钩子函数。
有的时候,我们需要通过路由来进行一些操作,比如最常见的登录权限验证,当用户满足条件时,才让其进入导航,否则就取消跳转,并跳到登录页面让其登录
导航守卫分为:全局的、单个路由独享的、组件内的三种
全局守卫(三个)
1、router.beforeEach全局前置守卫:进入路由前
2、router.beforeResolve全局解析守卫:在beforeRouteEnter调用之后调用
3、router.afterEach全局后置路由:进入路由后
//全局前置守卫
router.beforeEach((to, from, next) => {
console.log("进入路由前");
next()
})
//全局解析守卫
router.beforeResolve((to, from, next) => {
console.log("beforeResolve");
next()
})
//全局后置路由(没有next)
router.afterEach((to, from) => {
console.log("路由跳转后");
})
to、from、next参数说明
1、to和form是将要进入和将要离开的路由对象(可以通过this.$route获取)
2、next函数,如果有这个参数必须调用,否则不能进入路由(页面空白)
- next()进入该路由
- next(false):取消进入路由,url地址重置为from路由地址(也就是将要离开的路由地址)
- next跳转新路由,当前的导航被中断,重新开始一个新的导航
//或者可以这样子跳转
next("path地址")、next({ path: '' }) 、 next({ name: '' })
路由独享守卫
指的是单个路由配置的时候可以设置钩子函数。目前只有beforeEnter这一个
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) =>{
//代码逻辑
}
}
]
})
路由组件内的守卫
指的是组件内执行的钩子函数,类似于组件内的生命周期
1、beforeRouteEnter:进入路由前
2、beforeRouteUpdate:路由复用同一个组件时
3、beforeRouteLeave:离开当前路由时
<script>
export default {
beforeRouteEnter (to, from, next) {
//不能获取组件实例 `this`
//因为当函数执行前,实例还未被创建
},
beforeRouteUpdate (to, from, next) {
//在当前路由改变,但是该组件被复用是调用,可以访问组件实例`this
},
beforeRouteLeave (to, from, next) {
//导航离开该组件对应路由时调用,可以访问组件实例`this`
}
};
</script>
跳转死循环,页面永远空白
router.beforeEach((to, from, next) => {
if(登录) {
next()
} else {
next({ name:'login' })
}
})
逻辑貌似是对的,但是跳转到login之后,因为此时还是未登录状态,所以会一直跳转到login,然后死循环,页面一直空白,所以要把判断条件稍微改一下
router.beforeEach((to, from, next) => {
if(登录 || to.name === 'login') {
next()
} else {
next({ name:'login' })
}
})
完整的路由导航解析流程(不包含其他声明周期)
1、触发进入其他路由
2、调用离开路由的组件守卫beforeRouteLeave
3、调用全局前置守卫beforeEach
4、在重用的组件里调用beforeRouteUpdate
5、调用路由独享守卫beforeEnter
6、解析异步路由组件
7、在将要进入的路由组件中调用beforeRouteEnter
8、调用全局解析守卫beforeResolvle
9、导航被确认
10、调用全局后置钩子的afterEach钩子
11、触发DOM更新mouted
12、执行beforeRouteEnter守卫中传给next的回调函数
12、keep-alive
在开发Vue项目的时候,大部分组件是没必要多次渲染的,所以Vue提供了一个内置组件keep-alive来缓存组件内部状态,避免重新渲染
1、基本使用
A、缓存动态组件(不推荐)
包裹动态组件时,会缓存不活动的组件实例,而不是销毁他们。不过这种方式意义不大
//示例
<keep-alive>
<home v-if="active" />
<cart v-else />
</keep-alive>
B、缓存路由组件
使用keep-alive可以将所有路径匹配到的路由组件都缓存起来,包括路由组件里面的组件。keep-alive大多数场景就是这种
<keep-alive>
<router-view></router-view>
</keep-alive>
2、生命周期钩子
在keep-alive包含的组件/路由中,会多出两个生命周期的钩子:activated和deactivated
activated
1、activated在组件第一次渲染时会被调用,之后在每次缓存组件被激活时调用
2、activated的调用时机:在moutend之后,beforeRouteEnter之前
3、因为组件被缓存了,再次进入缓存路由/组件时,不会触发这些钩子
//beforeCreate created beforeMount mounted 都不会触发
理解:当把Home路由、Cart路由放在keep-alive中,会在这两个路由第一次创建时触发以上的声明周期函数,之后被缓存下来,来回切换这两个路由就不会再次触发以上钩子函数了
keep-alive的属性
有时候,我们只想缓存部分路由。(例如商城项目中的详情页,因为商品个数十分多,详情页也十分多,缓存下来显然浪费了内存)
include: 匹配的路由/组件 会被缓存exclude:匹配的路由/组件 不会被缓存
注:include、exclude支持三种方式来缓存有条件的路由:采用逗号分隔的字符串形式、正则形式、数组形式
<!--逗号分隔 -->
<keep-alive exclude="a,b">
<router-view></router-view>
</keep-alive>
<!--数组 -->
<keep-alive :include="['a', 'b']"></keep-alive>
注:keep-alive会优先匹配组件的name选项。(开发中尽量给组件补充个name属性)
13、v-if和v-show
Vue里面v-if、v-show的区别
两者都可用来控制DOM元素的显示和隐藏,区别在于: v-if是真正的条件渲染,它会在切换过程中被适当地销毁和重建;v-if也是惰性的,就是初始条件为假的话什么也不做,直到条件第一次变为真时才开始渲染条件块。
相比之下,v-show就简单得多,无论初始条件是什么都会进行渲染,只是简单地基于CSS进行切换,隐藏的话就加入display:none的属性值。
所以v-if具有较高的切换开销,适用于运行时条件很少改变的场景,而v-show具有较高的初始化渲染开销,适用于需要频繁切换的场景。
14、computed和methods
computed:
- 编写时要求有返回值
- 系统刚运行时会调用一次,在数值发生变化的时候会一直执行,而不是调用的时候执行。数值一直变化的时候很消耗性能
- 调用时可视为一个变量值使用,无需进行函数调用
- 计算结果不变时,该函数仅会调用一次。computed具有缓存功能
methods:
- 页面刚加载时调用一次,后期只有调用的时候才执行
- 需要进行函数式调用(加括号)
- methods中的返回值可写可不写
15、scoped及其原理
默认情况下,CSS总是全局生效的,即使是在模块化编程下也是如此。为了避免CSS样式之间的污染,vue引入了scoped这个概念
1、scoped属性
<style scoped></style>
当一个style标签拥有了scoped属性时,它的CSS样式就只能作用于当前的组件。
通过设置该属性,使得组件之间的样式不相互污染,如果一个项目中的style标签全部加上了scoped,相当于实现了样式的模块化
2、原理
vue中的scoped通过在DOM结构以及css样式上加唯一不重复的标记data-v-hash的方式,以保证唯一,而这个工作是由PostCSS转译实现的,达到样式私有化模块化的目的
渲染规则:
1、给HTML的DOM节点加一个不重复data属性(形如:data-v-19fca230)来表示他的唯一性
2、在每句css选择器的末尾(编译后的生成的css语句),加上一个当前组件的data属性选择器如[data-v-19fca230]来私有化样式
3、如果组件内部包含有其他组件,只会给其他组件最外层标签加上当前组件的data属性
3、例子
<div class="box">
<button class="btn">确定</button>
</div>
<style scoped>
.box{
background-color:white;
}
.btn{
width:200px;
}
</style>
经过PostCSS转译后
<div class="box" data-v-2311c06a>
<button class="btn" data-v-2311c06a>确定</button>
</div>
<style scoped>
.box[data-v-2311c06a]{
background-color:white;
}
.btn[data-v-2311c06a]{
width:200px;
}
</style>
PostCSS会给一个组件中所有DOM添加一个独一无二的动态属性data-v-xxxx,然后CSS选择器额外添加一个对应的属性选择器来选择该组件中的DOM,这中做法使得样式只作用于含该属性的DOM——组件内部DOM,从而达到了样式模块化的效果
4、穿透scoped
在项目中,我们有时会引用第三方组件,但又想局部改变第三方组件中的样式,该怎么办呢?
这个时候就可以使用穿透scoped>>>
<template>
<div class="box">
<dialog></dialog>
</div>
</template>
<style scoped>
.box >>> input {
width:2000px;
text-align: c
}
</style>
16、router的两种模式
前端路由的核心,就在于——改变视图的同时不会向后端发出请求
为了达到这一目的,浏览器当前提供了以下两种支持:
1、hash——即地址栏URL中的#符号,例如:http://www.abc.com/#/hello
本质:hash模式本质上是改变window.location的href属性,可以直接赋值location.hash来改变href,但页面不发生刷新
//演示一
setTimeout(() => {
location.hash = 'home'
}, 2000)
//输出url: ..../#/home
//演示二
<a href="#/home">change</a>
<!-- URL: .../#/home-->
特点:hash虽然出现在URL中,但不会被包括在HTTP请求中,对后端完全没有影响,因此改变hash不会重新加载页面
监听路由的改变
window.addEventListener("hashchange", () => {
console.log(location.hash)
//可以在这里做出路由改变后的操作,比如url改变后,页面怎么变化。(这就是前端路由的原理)
})
2、history——利用了HTML5 History Interface中新增的pushState()和replaceState()方法
//pushState
history.pushState({}, '', 'home')
//replaceState
histoyr.replaceState({}, '', 'category')
这两个方法应用于浏览器的历史记录栈,在当前已有的back、forword、go的基础上,他们提供了对历史记录进行修改的功能
只是当它们执行修改时,虽然改变了当前的URL,但浏览器不会立即向后端发送请求
因此可以说,history模式和hash模式都属于浏览器自身的特性,Vue-Router只是利用二这两个特性来实现前端路由
