一 基础知识
1.vue 的生命周期
1-1 钩子函数
1-1-1.创建
beforecreate
第一个钩子,这个阶段的data,methods,computed以及watch的数据和方法不能被访问
create
是实例创建完成后发生的。这个阶段完成数据观测,可以使用数据,更改数据,无法与Dom进行交互,想要的话可以通过nextTick来访问
1-1-2 挂载
beforeMount
发生在页面渲染之前,当前阶段虚拟Dom已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated。
mounted
在挂载完成后发生,在当前阶段,真实的Dom挂载完毕,数据完成双向绑定,可以访问到Dom节点,使用$refs属性对Dom进行操作。
补:第一次页面加载就会触发 创建和挂载钩子
1-1-3.更新
beforeUpdate
发生在更新之前,也就是响应式数据发生更新,虚拟dom重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。
updated
发生在更新完成之后,当前阶段组件Dom已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。
1-1-4.销毁
beforeDestroy
发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器
destroyed
发生在实例销毁之后,这个时候只剩下了dom空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。
1-2 钩子函数的使用方法
1.beforecreate: data 和 $el 都没有初始化 全部为 undefined,可以在加个loading事件,在加载实例时触发 ,

2.created: 初始化完成时的事件写在这里,如在这结束loading事件,异步请求也适宜在这里调用
3.mounted: 挂载元素,获取到dom节点
4.beforeUpdate 可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
5.updated: 可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,
因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。
6.beforeDestroy : 可以做一个确认停止事件的确认框
7.destroyed 可以执行一些优化操作,清空定时器,解除绑定事件
2 v-show 与 v-if v-for
2-1.v-show 与 v-if 的区别
共同:
根据真假切换元素的显示状态
不同:
v-show :修改元素的display,实现显示隐藏
(适合频繁切换,在第一次渲染的时候已经消耗了性能)
v-if : 通过操控DOM值来 实现 显示隐藏
(不适合频繁切换. 数据多不建议用if.每一次切换则重新消耗性能.乱)
如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
2-2 v-if 和v-for的优先级
当v-if 与 v-for 一起使用时,v-for 比v-if 优先级高,如果连用的话会把 v-if 给每个元素都添加一下,会造成性能问题,所以不推荐v-if和v-for在同一个标签中同时使用。
解决方法:
1、ul和li搭配使用,或者是渲染父级标签下的子标签。
<ul v-if = "state">
<li v-for = "(item,id) in list" :key="id"></li>
</ul>
2、使用过滤器将v-if中的判断转移到computed的计算属性中。
<ul>
<li v-for"(item,id) in formList" :key = "id"></li>
</ul>
computed: {
formList: function(){
return this.list.filter(function(item){
return item.state;
})
}
}
3.组件通信有哪些方式?
3-1.父传子
思路:
1.在父组件调用子组件的位置 绑定属性
(属性名:需要发送的变量名)
2.子组件通过 props接收
(props: [属性名])
代码展示:
3-2.子传父
思路:
1.子组件 在调用子组件的位置 添加自定义事件 使用$emit 发送数据
2.父组件 通过create 接收数据
3-3.兄弟传值
思路:
1.定义中央事件总线let bus = new Vue();
2.触发自定义事件 并发送 bus.emit (要出发的事件名,要发送的数据)
3.触发create 并接收bus.on(要出发的事件名,要发送的数据)
3-4.公共传递
思路:
1.在父组件内部添加 provide:{命名:‘内容’} 并赋明需要具体传递的值,
2.在子或者子子 内部 inject:[“命名”] 接收
3.在相对应页面中可直接获取
补:组件传值代码参考:
https://blog.csdn.net/weixin_55042716/article/details/114434354
3.solt 插槽
组件的最大特性就是 重用 ,而用好插槽能大大提高组件的可重用能力
插槽的作用:父组件向子组件传递内容。
通俗的来讲,插槽无非就是在 子组件 中挖个坑,坑里面放什么东西由 父组件 决定
3-1 匿名插槽
匿名插槽一般就是使用单个插槽
//子组件内部
<div class='banner'>
<img v-for='item in imgs' :src='item.src' :key='item.bannerId'>
//占了个位,
<solt>
//插槽内部设置了默认值。如果将来插槽没有被插入组件或者元素,就用自己内部的元素来渲染
<button>组件内部</button>
</solt>
</div>
//父组件
<banner>
//如果这里没有设置值,则显示插槽中的内容,
//设置例了值则显示此处的值
<button> < </button>
<banner>
3-2 具名插槽
slot 元素可以用一个特殊的特性 name 来进一步配置如何分发内容。多个插槽可以有不同的名
字,具名插槽将匹配内容片段中有对应 slot 特性的元素。
具名插槽存在的意义就是为了解决在单个页面中同时使用多个插槽。
如果没有起名的话,默认为default
//子组件
<div class='banner'>
<img v-for='item in imgs' :src='item.src'>
<solt name="but">
<button>组件内部</button>
</solt>
</div>
//父组件
<banner>
<template v-solt:but>
//简写 <template #but>
<button> < </button>
</template>
<banner>
3-3作用域插槽
应用场景:父组件对子组件的内容进行加工处理
作用域插槽是一种特殊类型的插槽,作用域插槽会绑定了一套数据,父组件可以拿这些数据来用,
于是,情况就变成了这样:样式父组件说了算,但父组件中内容可以显示子组件插槽绑定的数据。
//子组件
<div>
<slot text="我是子组件中的内容"></slot>
</div>
//父组件
<banner>
<div slot-scope="props">
<div>父组件</div>
<h3>{
{ props.text }}</h3>
</div>
</banner>
作用域插槽 相对于匿名插槽 加上 匿名插槽里面携带的信息
4 computed和watch ,有什么区别
4-1.computed 计算属性
- 支持缓存,只在依赖的数据发生变化时,才会重新计算,否则当多次调用computed属性时,调用的其实是缓存;
- 不支持异步,当computed内有异步操作时无效,无法监听数据的变化
- 如果一个数据需要经过复杂计算就用 computed
4-2.watch 侦听器
- 不支持缓存,每调用一次就计算一次;
- watch支持异步;
- 如果一个数据需要被监听并且对数据做一些操作就用 watch
4-3.methods 方法
- 不支持缓存, 只要使用该方法便进行调用。
5 Vue中的 data 为什么必须是函数
vue组件中data值不能为对象,因为对象是引用类型,所以vue实例所用的是在一个作用域里面,没有分开,如果修改一个对象里面的数据,其他的也会被影响到,
data 如果是一个函数,每一个函数都有自己的局部作用域,他改变的话不会影响到其他的数据。
6 Vue中的 key 有什么作用?
列表渲染时的唯一性
6-1.添加减少值
例:
[1,2,3,4,5,]
添加一个a
[1,2,a,3,4,5,]
不加key 则 3,4,5 会重新渲染一遍 a成3 ,3成4, 4成5
加key 则只单独渲染新插入的值,
减少
[1,2,4,5,]
加key,如果有一项移出,后面的不会错位,用于删除操作
6-2 条件渲染中的key
不加key:
vue无法判断每个组件的不同,则会重复使用相同的组件,会拿到上一个组件的信息,
加key:
对于vue来说,每个组件都是不同的,切换时,都会重新渲染,
(如果不重新渲染的话,不会触发生命周期函数,没法请求新的数据)
6-3 总结:
无:key属性时,状态默认绑定的是位置;
有:key属性时,状态根据key的属性值绑定到了相应的数组元素。
6-4 建议:
不使用index作为key的值,使用唯一id作为标识.(后台提供或自己创建)
7 vue 路由
7-1 路由的理解
通过互联的网络把信息从 (源地址) 传输到 (目的地址) 的活动
映射表:地址和具体某一台电脑之间的关系,ky
SPA(Single Page Application)单页面应用程序,基于前端路由而起:整个网站只有一个页面,通过
监听地址栏中的变化事件,来通过Ajax局部更新内容信息显示、同时支持浏览器地址栏的前进和后退操
作。
7-2 vue-router 路由模式有几种?
1.hash路由
使用的是前端锚点链接,在地址栏会有一个#号,不会将路由信息发往后端,不需要后端支持
并且每次hash值发生改变的时候,会触发hashchange事件。因此我们可以通过监听该事件,来知道hash值发生了哪些变化。。
2.history路由
地址栏没有其他特殊符号,需要后端支持。会被原生的history对象记录
其中最主要的 API 有以下两个:history.pushState() 和 history.repalceState()。
这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。直接替换当前的历史记录,
7-3 路由跳转
7-3-1导航模式
<router-link to="path">xxx</router-link>
to 要跳转到的路由规则 string|object
to="users"
:to="{path:'path'}"
tag 可以指定渲染成什么组件
replace 不会留下history记录,浏览器返回前进按钮不能用
7-3-2 导航模式
可以跟点击事件一起用
this.$router.push("/login");
this.$router.push({ path:"/login" });
this.$router.push({ path:"/login",query:{username:"jack"} });
this.$router.push({ name:'user' , params: {id:123} });
this.$router.go( n );//n为数字 负数为回退
7-3-3 路由重定向
概念:用户在访问地址 A 的时候,强制用户跳转到地址 C ,从而展示特定的组件页面
实现: 通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向
7-3-4 嵌套路由
在路由下 添加子路由
在父路由组件的模板内容中添加子路由链接和子路由填充位(占坑),同时在路由规则处为父路由配置children属性指定子路由规则:
routes: [
{
path: "/user",
component: User, //这个不能丢
// 通过children属性为/user添加子路由规则
children:[
{ path: "index", component: Index },
{ path: "add", component: Add },
]
}
]
需要在 User组件中定义一个router-view 用于嵌套路由的渲染显示
<router-view></router-view>
7-4 路由守卫
导航守卫 就是路由跳转过程中的一些 钩子函数 ,这个过程中触发的这些函数能让你操作一些其它事情时,可以进行过滤操作,这就是导航守卫。
to: 目标路由
from: 当前路由
next() 跳转 一定要调用
next(false);//不让走
next(true);//继续前行
next(’/login’)//走哪
next({path:’/login’,params:{},query:{}})//带点货
全局前置守卫 beforEach
router.beforeEach((to, from, next) => {
document.title = to.meta.title || '卖座电影';
if (to.meta.needLogin && !$store.state.isLogin) {
next({
path: '/login'
})
} else {
next()
}
})
路由独享的守卫 beforeEnter
使用方法与全局守卫相同 不同的是:全局守卫可以作用于全局,路由独享守卫只作用于被设置守卫的路由
//登录模块
path: '/login',
component: () => import('@/views/login'),
beforeEnter: (to, from, next) => {
if (to.meta.needLogin && !$store.state.isLogin) {
next({
path: '/login'
})
} else {
next()
}
}
组件路由守卫
//组件内部钩子
beforeRouteEnter (to, from, next) {//前置
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {//后置
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
注意: 路由独享守卫,守的是path 组件内部守卫,守的是component
7-5 router和route的区别
1 $router
用来操作路由 只写对象
2 $route
用来获取路由的信息 只读对象
//操作 路由跳转
this.$router.push({
name:'hello',
params:{
name:'word',
age:'11'
}
})
//读取 路由参数接收
this.name = this.$route.params.name;
this.age = this.$route.params.age;
7-6 params传参 跟query传参
7-6-1 params :
this.$route.params.xxx
params 只能用name来引入路由
params 是路由的一部分,必须要在路由后面添加参数名。
params 相当于post请求,参数不会再地址栏中显示。
7-6-2 query :
this.$route.query.xxx
query 传参使用path来引入路由。
query是拼接在url后面的参数,没有在路由后面添加参数名也没关系。
query相当于get请求,页面跳转的时候,可以在地址栏看到请求参数,
7-6-3 props
在路由对象内,通过props属性 开启传参功能
在组件对象内 通过props接收
原因: 为了降低路由和组件的耦合度,
在子detial中设置
{
path:'/detial:id',
name:'detail',
components:{
default:()=> import(/* webpackChunkName: "pro" */'@/views/detail/detail.vue')
},
props:{
default:true
}
}
路由页面,设置 props:true
页面 props:[‘id’]
8 路由懒加载
8-1.为什么需要懒加载?
像vue这种单页面应用,如果没有应用懒加载,运用webpack打包后的文件将会异常的大,造成进入首页时,需要加载的内容过多,时间过长,会出啊先长时间的白屏,即使做了loading也是不利于用户体验,
而运用懒加载则可以将页面进行划分,需要的时候加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时
8-2.如何实现懒加载
方法一 resolve
这一种方法较常见。它主要是使用了resolve的异步机制,用require代替了import,实现按需加载,下面是代码示例
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
// {
// path: '/',
// name: 'HelloWorld',
// component: HelloWorld
// }
// 更改
{
path: '/',
name: 'HelloWorld',
component: resolve => require(['@/components/HelloWorld'], resolve)
}
]
})
方法二.import按需加载(官方写法)
能够被webpack自动代码分割允许将不同的组件打包到一个异步块中,使用命名chunk(特殊注释语法)。
Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
const comA = () => import('url')
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
方法三 webpack提供的require.ensure()
vue-router配置路由,使用webpack的require.ensure技术,也可以实现按需加载。
这种情况下,多个路由指定相同的chunkName,会合并打包成一个js文件。
{
path: '/home', name: 'home',
component: r =>require.ensure([], () => r(require('@/components/home')), 'demo')
}
9 vue的内置组件 keep-live -优化
缓存组件的状态 ,可能会被重复显然的动态组件处,缓存动态组件的状态。避免重复渲染。节约性能
使用:将要被缓存的组件或可能会被重复渲染的组件,使用 keep-alive包裹即可
渲染场景 例:
首页下拉刷新,加载过的数据,切换其他页面后,再返回首页,没有记录,则数据重新请求,添加 keep-live包裹后,记录数据,不会再重复加载。 切换另一个页面,上一个页面依旧保持活跃,数据继续保持
应用 :
利用路由的元数据meta 添加
APP.js页面
<keep-alive>
<!-- 添加meta的则添加 keep-alive -->
<router-view v-if="$route.meta.keep"></router-view>
</keep-alive>
<!-- 不添加 -->
<router-view v-if="!$route.meta.keep"></router-view>
路由页面给需要的添加meta
{
path: '/',
name: 'home',
alias: '/home',
components:{ home },
meta:{
keep:true
}
},
10 vue 的混入 mixin -优化
公共配置
多个组件中都需要的数据或功能,混入对象内部的选项等同于组件对象的选项,但是混入对象并不是真正的组件对象,必须添加到组件对象内部后才能执行,
重复方法或重复数据
— 对象类合并
— 函数类 将多个函数放在数组中,都执行
— 重复的数据 以组件内部的为准,
语法:
—定义mixin 对象
— 引入 mixin
— 使用
<body>
<div id="app">
<button @click='hello'>测试</button>
</div>
</body>
<script>
var mymixin ={
methods:{
hello:function(){
console.log('进入了mymixin了')
}
}
}
//进入 vue编程中
const app = new Vue({
el:'app',
mixins:[mymixin],
data:{}
})
</script>
11. nextTick
11-1 理解
就是当页面出来的时候,可能静态资源还没配置完成,这个时候要用nexttick延迟回调,更新数据后立即操作dom
(事件循环机制: 当宏任务执行后,在渲染页面前,将所有的微任务执行完,再进行页面的渲染。)
11-2 应用场景
在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中
在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,
所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted()钩子函数,因为该钩子函数执行时所有的DOM挂载和渲染都已完成,此时在该钩子函数中进行任何DOM操作都不会有问题 。
总结:在下一次dom更新循环结束之后执行延迟回调,在修改数据之后使用这个方法,立即更新dom.
11-3 nextTick 函数的实现原理
1.先定义了一个callbacks 存放所有的nextTick里的回调函数
2.然后判断一下当前的浏览器内核是否支持 Promise,如果支持,就用Promise来触发回调函数
3.如果不支持Promise再看看是否支持MutationObserver,是一个可以监听DOM结构变化的接口,观察文本节点发生变化时,触发执行所有回调函数。
4.如果以上都不支持就只能用setTimeout来完成异步执行了。
12.对vuex的理解
集中式的存储管理 应用程序中所有组件的状态
需要多个组件共享的变量全部存储在一个对象里面
然后,将这个对象放在顶层的vue实例中,让其他组件可以使用
12-1 为什么使用它?
-
vue单向数据流 父级 prop 的更新会向下流动到子组件中,如果组件嵌套过深 通过props层层往下传比较繁琐,在传的过程中容易出现对数据的更改,导致数据紊乱
-
如果在一个项目很庞大各个,组件会共用一个状态,所以用到vuex,即可在整个Vue项目的组件中使用。
12-2.五大核心属性
state 是状态管理器的状态中心,里面是初始化的数据
export default new Vuex.store({
state:{
count:0
}
})
//使用页面:
<h1>{
{ this.$store.state.count }}</h1>
mutation 是唯一改变数据的方式 不在这里面写 异步代码
export default new Vuex.store({
state:{
count:0
},
mutations:{
add(state,step){
state.count =state.count+step
}
}
})
//使用页面
<button @click='add1'> mutations测试</button>
methods:{
add1(){
this.$store.commit('add',1)
}
}
// 每次点击 增加 1
actions 就是处理异步操作,让motation中的方法能在异步操作中起作用
export default new Vuex.store({
state:{
count:0
},
mutations:{
add(state,step){
state.count=state.count+step
}
},
actions:{
act(context,step){
setTimeout(() =>{
context.commit('add',step)
},3000)
}
}
})
//使用
<button @click='add2'> actions测试</button>
methods:{
add2(){
this.$store.dispatch('act',1)
}
}
// 每次点击 增加1 三秒后执行
getter 是状态管理器中的计算属性
export default new Vuex.store({
state:{
count:0
},
getters:{
powerCounter(state){
return state.count * state.count
}
}
})
使用:
<h>{
{ $store.getters.powerCounter}}</h>
module 具体去分割模块
export default new Vuex.store({
modules :{
a:{
state:{
name:'zs'
},
mutations:{},
actions:{},
getters:{}
},
b:{
state:{},
mutations:{},
actions:{},
getters:{}
}
}
})
<h1>{
{$store.state.a.name}}</h1>
概念:
state 是状态管理器的状态中心,里面是初始化的数据
mutation 是唯一改变数据的方式 不在这里面写异步代码
action 就是处理异步操作,让motation中的方法能在异步操作中起作用
getter 是状态管理器中的计算属性
module 将 store 分割成模块,每个模块都具有state,mutation、action、getter、甚至是嵌套子模块
12-3.哪些会用到?
登录状态,用户名称,头像,购物车的数量,收藏信息,地理位置。所有需要用户信息的地方验证登陆状态。
vuex登录注册使用场景
点击登录按钮的时候,发送axios请求,从后端调用的借口获取数据,返回成功的状态码时,调用vuex里面的mutation方法,动态改变用户和密码的内容,把用户名渲染在用户页面上
13 vue中import和require的用法
1,require是CommonJS规范的模块化语法,import是ECMAScript 6规范的模块化语法;
2,require是运行时加载,import是编译时加载;
3,require可以写在代码的任意位置,import只能写在文件的最顶端且不可在条件语句或函数作用域中使用;
4,require通过module.exports导出的值就不能再变化,import通过export导出的值可以改变;
5;require通过module.exports导出的是exports对象,import通过export导出是指定输出的代码;
6,require运行时才引入模块的属性所以性能相对较低,import编译时引入模块的属性所所以性能稍高。
14 父组件可以监听到子组件的生命周期吗?
比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit("mounted");
}
以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。
15 vue项目如何刷新当前页面
15-1.场景
在处理列表时,常常有删除一条数据或者新增数据之后需要重新刷新当前页面的需求。
15-2.遇到的问题
1.用vue-router重新路由到当前页面 页面是不进行刷新的
2.采用window.reload(),或者router.go(0)刷新时,整个浏览器进行了重新加载,闪烁,体验不好
15-3.推荐好用的方法
15-3-1 思路:
1、在router-view中加上条件渲染 v-if 默认为true。让它显示出来
2、写一个reload方法,在页面刷新只有,点击某个查询条件的时候调用这个重载的方法
这时条件渲染变化了为false
在修改数据之后使用 $nextTick,
条件渲染变化了为true
则可以在回调中获取更新后的 DOM
3.在需要刷新的页面 注入这个函数
在需要用到这个函数的地方去引用
15-3-2 代码展示:
App.vue 页面
<template>
<div id="app">
<div class="wrap">
//条件渲染 v-if 默认为true
<router-view v-if="isRouterAlive"></router-view>
</div>
</div>
</template>
<script>
export default {
name: 'App',
provide () {
return {
reload: this.reload
}
},
data () {
return {
isRouterAlive: true
}
},
methods: {
//reload方法,在页面刷新只有,点击某个查询条件的时候调用这个重载的方法
reload () {
this.isRouterAlive = false
//修改数据之后
this.$nextTick(function() {
this.isRouterAlive = true
})
}
}
}
</script>
需要被刷新的页面
<template>
<button @click="refresh"></button>
</template>
<script>
export default{
name: 'refresh',
//注入这个函数
inject: ['reload'],
methods: {
refresh () {
//引用
this.reload()
}
}
}
</script>
二.原理部分
1 MVVM?
M:(model)数据模型
V:(view)前端展示页面 视图
VM:(ViewModel)桥梁 用于双向绑定数据与页面,对于我们的课程来说,就是vue的实例
MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。这种模式下,页面输入改变数据,数据改变影响页面数据展示与渲染
vue使用MVVM响应式编程模型,避免直接操作DOM , 降低DOM操作的复杂性。
优点
低耦合。
视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的View上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。
可重用性。
你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。
可测试。
界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。
2.vue响应式原理
响应式原理意思就是 在改变数据的时候,视图会跟着更新
Object.defineProperty() 此方法会直接在一个对象上定义一个新的属性,或者修改一个对象的现有属性,并返回此对象
当把一个普通的JavaScript对象传给Vue实例的data选项,Vue将遍历此对象所有的属性,使用Object.defineProperty把这些属性全部转为getter/setter(数据劫持/数据映射),检测对象属性变化。
在属性被访问和修改时通知变化。每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
Object.defineProperty(obj, prop, descriptor)
**obj** 要定义属性的对象。
**prop** 要定义或修改的属性的名称 。
**descriptor** 要定义选项。
<body>
<div id="app">
<div id="msg"></div>
<input type="text" name="" id="" oninput="changeVal(this)"/>
</div>
</body>
<script src="./js/vue.js"></script>
<script type="text/javascript">
// 1. 定义对象
var userInfo = {
name: "这个信息虽然用户看不到,但是Vue可以追踪到",
};
// 2. 数据劫持
var obj = {};
Object.defineProperty(obj, "name", {
get() {
return userInfo.name;
},
set(data) {
userInfo.name = data;
document.getElementById("msg").innerHTML = data;
return true;
},
});
// 3. 实时渲染
document.getElementById("msg").innerHTML = obj.name;
// 4. 发布订阅
function changeVal(eleObj) {
let value = eleObj.value;
obj.name = value;
return true;
}
</script>
3说说你对proxy的理解
vue的数据劫持有两个缺点:
1、无法监听通过索引修改数组的值的变化
2、无法监听object也就是对象的值的变化
所以vue2.x中才会有$set属性的存在
proxy是es6中推出的新api,可以弥补以上两个缺点,所以vue3.x版本用proxy替换object.defineproperty
4.直接给一个数组项赋值,Vue 能检测到变化吗?-未完
由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。
4-1 对于对象
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用
Vue.set(object, propertyName, value)
this.$set(this.someObject,'b',2)
赋值多个
Object.assign(this.someObject, { a: 1, b: 2 })
4-2 对于数组:
Vue.set(vm.items, indexOfItem, newValue) vm.items.splice(indexOfItem, 1, newValue)
或者
vm.$set(vm.items, indexOfItem, newValue)
4-3 数组方法
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
5.虚拟DOM 原理
5-1 虚拟DOM原理
在渲染页面之前(真实的dom树挂载之前) vue会自己根据真实的dom构建一个虚拟dom,当里面某个节点发生变动时,与旧节点对比一下,发现不一样的地方直接修改在真实对的dom上,就是在虚拟dom上比较新旧节点的同时直接在真实dom上修补
真实DOM
<div class="red">
<span></span>
<span></span>
</div>
虚拟DOM
const app = {
tag: "div", // 标签名或组件名
data: {
class: "red", // 标签上的属性
on: {
click: () => {} // 事件
}
},
children: [ // 子元素节点
{......}
],
}
如何创建虚拟 DOM
h('div', {
class: 'red',
on: {
click: () => {}
},
}, [h('span', {}, 'span1'), h('span', {}, 'span2')])
5-2 优点:
在某些情况下,虚拟 DOM 比真实 DOM 快,因为虚拟 DOM 能减少不必要的 DOM 操作
虚拟 DOM 可以减少 DOM 操作:将多次操作合并一次操作,比如在页面里添加1000个div,DOM可能要操作1000次,但是虚拟DOM只需要操作一次(把数据添加到一个数组里然后操作);
还可以减少DOM操作的范围:虚拟DOM借助DOM diff可以把多余的操作省掉,比如添加1000个div,通过对比区分出哪些是新增的、哪些是重复的,如果只有10个是新增的就只渲染这10个。
虚拟DOM可以跨平台渲染
虚拟DOM不仅可以变成DOM,还可以变成小程序、IOS或者安卓应用,虚拟DOM本质上 只是一个JS对象
5-3 缺点:
需要额外的创建函数来创建虚拟DOM,但是可以通过JSX语法来简化成XML写法,但是这也有缺点,就是严重依赖打包工具,要添加额外的构建过程
当数据规模不大(比如有几千个节点的时候),虚拟DOM的确更快,但是如果规模达到一定程度(十万+)的话,虚拟DOM的稳定性不如原生DOM。不过一般也达不到这个规模,所以多数情况下不必考虑。
6 DOM diff
DOM diff是一个函数,称之为 patch
patches = patch(oldVNode, newVNode)
patches 就是要运行的DOM操作
6-1 DOM diff 的优点
DOM diff算法可以排除多余的DOM操作。
DOM diff会对比前后两次DOM树的区别,然后只更新有变化的DOM节点,优化操作。
6-2 DOM diff 的问题(key)
同级比较会出现bug。
diff算法是从左往右进行同层级对比的,如果发现元素相同但是内容不相同,会直接修改内容。这会导致有时删除了一个节点,结果却是另外一个节点被删除了。
这个问题可以通过给每一个列表指定key值来解决,注意这里的key值不能指定为index,否则无效。
7vue渲染过程
7-1 渲染过程
Vue 推荐在绝大多数情况下使用 template 来创建你的 HTML。但是模板毕竟是模板,不是真实的dom节点。从模板到真实dom节点还需要经过一些步骤
1 把模板编译为render函数(或在开发环境已完成,vue-loader)
2 实例进行挂载, 根据根节点render函数的调用,递归的生成虚拟dom
3 对比虚拟dom,渲染到真实dom
4 组件内部data发生变化,组件和子组件引用data作为props重新调用render函数,生成虚拟dom, 返回到步骤3
7-1-1 第一步 模板到render
在我们使用Vue的组件化进行开发应用的时候, 如果仔细的查看我们要引入的组件, 例子如下
// App.vue
<template>
<div>
hello word
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
在我们的主入口main.js
import Vue from 'vue'
import App from './App'
console.log(App)
new Vue({
render: h => h(App)
}).$mount('#app')
我们引入的App这个模块,里面是一个对象,对象里面存在一个方法叫做render。。原先,每一次加载一个组件,然后对模板进行解析,解析完后,生成Dom,挂载到页面上。这样会导致效率很低效。 而使用Vue-cli进行组件化开发,在我们引入组件的后,其实会有一个解析器(vue-loader)对此模板进行了解析,生成了render函数。这样,能保证组件每次调用的都是render函数,使用render函数生成VNode。
7-1-2 第二步:虚拟节点VNode
我们把Vue的实例挂载到#app, 会调用实例里面的render方法,生成虚拟DOM。来看看什么是虚拟节点,把例子修改一下。
new Vue({
render: h => {
let root = h(App)
console.log('root:', root)
return root
}
}).$mount('#app')
上面生成的VNode就是虚拟节点,虚拟节点里面有一个属性elm, 这个属性指向真实的DOM节点。因为VNode指向了真实的DOM节点,那么虚拟节点经过对比后,生成的DOM节点就可以直接进行替换。
7-1-3 这样有什么好处呢?
一个组件对象,如果内部的data发生变化,触发了render函数,重新生成了VNode虚拟节点。那么就可以直接找到所对应的节点,然后直接替换。那么这个过程只会在本组件内发生,不会影响其他的组件。于是组件与组件是隔离的。
7-2 更新过程
1,修改data,触发setter(此前在getter中已被监听)
2,重新执行render函数,生成newVnode
3,patch(vnode,newVnode)
8 vue2.x中如何监测数组变化 --未完
使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组api时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。
9 Vue2.x和Vue3.x渲染器的diff算法分别说一下
简单来说,diff算法有以下过程
同级比较, 再比较子节点,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除),
比较都有子节点的情况(核心diff)递归比较子节点
正常·Diff两个树·的时间复杂度是O(n3),但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。
Vue3.x借鉴了ivi算法和 inferno算法 在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。该算法中还运用了动态规划的思想求解最长递归子序列。
10.预编译器less 和 sass
CSS预处理器,可嵌套class、减少很多重复的选择器,提高样式代码的可维护性。大大提高了我们的开发效率。
Less 是在客户端处理的。
Sass 是在服务器端处理的。
相同之处
Less和Sass在语法上有些共性,比如下面这些:
1、混入(Mixins)——class中的class;
2、参数混入——可以传递参数的class,就像函数一样;
3、嵌套规则——Class中嵌套class,从而减少重复的代码;
4、运算——CSS中用上数学;
5、颜色功能——可以编辑颜色;
6、名字空间(namespace)——分组样式,从而可以被调用;
7、作用域——局部修改样式;
8、JavaScript 赋值——在CSS中使用JavaScript表达式赋值。
三 项目优化
(1)代码层面的优化
v-if 和 v-show 区分使用场景
computed 和 watch 区分使用场景
v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
长列表性能优化
事件的销毁
图片资源懒加载
路由懒加载
第三方插件的按需引入
优化无限列表性能
服务端渲染 SSR or 预渲染
(2)Webpack 层面的优化
Webpack 对图片进行压缩-------先引入npm install image-webpack-loader --save-dev,然后在 webpack.config.js 中配置
减少 ES6 转为 ES5 的冗余代码
提取公共代码
模板预编译
提取组件的 CSS
优化 SourceMap
构建结果输出分析
Vue 项目的编译优化
(3)基础的 Web 技术的优化
开启 gzip 压缩
浏览器缓存
CDN 的使用
使用 Chrome Performance 查找性能瓶颈