vue双向数据绑定原理
Vue实现双向数据绑定是采用数据劫持和发布者-订阅者模式。
数据劫持是利用ES5的Object.defineProperty(obj,key,val)方法来劫持每个属性的getter和setter,在数据变动时发布消息给订阅者,从而触发相应的回调来更新视图。
Vue是如何实现数据双向绑定的?
Vue数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。
Vue主要通过以下4个步骤来实现数据双向绑定的:
1.实现一个监听器 Observer:
对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty() 对属性都加上 setter 和 getter。这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化。
2.实现一个解析器 Compile:
解析 Vue 模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新。
3.实现一个订阅者 Watcher:
Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
4.实现一个订阅器 Dep:
订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher,对监听器 Observer 和 订阅者 Watcher 进行统一管理。
自定义指令
钩子函数:
bind:只调用一次,指令第一次绑定到元素时调用
instead:被绑定元素插入父节点时调用
update:所在组件VNode更新时调用,但可能发生在其子VNode更新之前
componentUpdated:指令所在组件的VNode及其子VNode全部更新后调用
unbind:只调用一次,指令与元素解绑时调用
钩子函数的参数:
el:指令所绑定的元素,可以用来直接操作Dom
binding:一个对象,包含以下属性
name:指令名,不包含v-前缀
value:指令绑定的值
oldValue:指令绑定的前一个值(仅在update和compoundUpdated钩子中可用,无论值是否改变都可用)
expression:字符串形式的指令表达式
arg:传给指令的参数
modifiers:一个包含修饰符的对象
vnode:Vue 编译生成的虚拟节点。
oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。
vue-router 路由懒加载
路由懒加载指的是打包部署时将资源按照对应的页面进行划分,需要的时候加载对应的页面资源,而不是把所有的页面资源打包部署到一块。避免不必要资源加载。
三种方式可以实现vue项目路由跳转时资源的按需加载:
//非懒加载
import Home from '@/components/home'
{
path:'/home',
name:'home',
component:Home
}
1. vue异步组件
vue异步组件
这种情况下每一个组件就会生成一个js文件,不能分类指定chunkName
{
path:'/home',
name:'home',
component:resolve=>require(['@/components/home'],resolve)
}
2.使用import
写法一:
const 组件名= ()=>import('组件路径')
这种写法不能指定webpackChunkName,每个组件打包成一个js文件
const Home = ()=>import('@/components/home')
写法二:
const 组件名= ()=>import( /* webpackChunkName:'xxx' */ '组件路径')
这种写法把组件按组分块,指定了相同的webpackChunkName,会合并打包成一个js文件
const Home = ()=>import(/* webpackChunkName:'ImportHome' */ '@/components/home')
const Index = ()=>import(/* webpackChunkName:'ImportHome' */ '@/components/index')
3.webpack提供的require.ensure()
该方法也可指定相同的chunkName,合并打包成一个js文件。
最后一个参数使用‘’,则每个component会生成一个单独的js文件
{
path:'/home',
name:'home',
component:r=>require.ensure([],()=>r(require('@/components/home')),'home')
},
{
path:'/home',
name:'home',
component:r=>require.ensure([],()=>r(require('@/components/home')),'')
}
SPA单页面有什么优缺点?
优点:
1.体验好,不刷新,减少请求,数据ajax异步获取
2.前后端分离
3.减轻服务端压力
4.共用一套后端程序代码,适配多端
缺点:
1.首屏加载过慢
2.SEO不利于搜索引擎抓取
前端登录流程
前端权限管理怎么实现
vue换肤
vue hook使用
1.监听实例的某个生命周期,并执行相应的操作
this.$on/$once('hook:生命周期',callback)
2.在父组件监听子组件的生命周期方法
//父组件
<rl-child
:value="40"
@hook:mounted="childMountedHandle"
/>
method () {
childMountedHandle() {
// do something...
}
}
路由传参的两种方式
query可以使用name也可以使用path,都可以正常传参
params只能使用name传参,用path传递不过去,用$route也接收不到
1.params传参
url中显示参数:
在定义路由路径时定义参数名和格式(如 path: "/one/login/:num" )
子路由通过 this.$route.params.num 的形式来获取父路由向子路由传递过来的参数
注意:传递的值存在url中存在安全风险,为防止用户修改,另一种方式不在url中显示传递的值
url中不显示参数:
定义路由时添加name属性给映射的路径取一个别名
使用<router-link :to="{name:'home',params:{id:001}}>形式传递参数
子路由通过 this.$route.params.num 的形式来获取父路由向子路由传递过来的参数
注意:上述这种利用params不显示url传参的方式会导致在刷新页面的时候,传递的值会丢失
2.query传参
解决刷新页面参数丢失问题
created() {
if (localStorage.getItem('queryParams')) {
this.queryParams = JSON.parse(localStorage.getItem('queryParams'))
}
this.queryParams.id = this.$route.query.id || this.queryParams.id
localStorage.setItem('queryParams', JSON.stringify(this.queryParams))
}
路由守卫
1.全局路由守卫
router.beforeEach()
router.afterEach()
2.独享路由守卫
beforeEnter()
3.组件内路由守卫
beforeRouterEnter()
beforeRouterLeave()
一、全局路由守卫
全局路由守卫有个两个:一个是全局前置守卫,一个是全局后置守卫
router.beforeEach((to, from, next) => {
console.log(to) => // 到哪个页面去?
console.log(from) => // 从哪个页面来?
next() => // 一个回调函数
}
router.afterEach(to,from) = {}
二、组件路由守卫
// 跟methods: {}等同级别书写,组件路由守卫是写在每个单独的vue文件里面的路由守卫
beforeRouteEnter (to, from, next) {
// 注意,在路由进入之前,组件实例还未渲染,所以无法获取this实例,只能通过vm来访问组件实例
next(vm => {})
}
beforeRouteUpdate (to, from, next) {
// 同一页面,刷新不同数据时调用,
}
beforeRouteLeave (to, from, next) {
// 离开当前路由页面时调用
}
三、路由独享守卫
路由独享守卫是在路由配置页面单独给路由配置的一个守卫
export default new VueRouter({
routes: [
{
path: '/',
name: 'home',
component: 'Home',
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
history模式与hash模式
vue-router
路由本质:建立起url和页面之间的映射关系。
1、路由原理
SPA:单一页面应用程序,只有一个完整的页面;它在加载页面时,不会加载整个页面,而是只更新某个指定的容器中内容。
核心之一:更新视图而不重新请求页面
模式:Hash模式和History模式;根据mode参数来决定采用哪一种方式。
hash模式的原理:是 onhashchange 事件(监测hash值变化),可以在 window 对象上监听这个事件。
history 模式:充分利用了html5 history interface 中新增的 pushState() 和 replaceState() 方法。
2、使用路由模块来实现页面跳转的方式
方式1:直接修改地址栏
方式2:this.$router.push(‘路由地址’)
方式3:<router-link to="路由地址"></router-link>
3、路由的嵌套
4、vue-router如何做用户登录权限等
5、$route 和 $router 的区别
1.$route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。
① $route.path
字符串,对应当前路由的路径,总是解析为绝对路径,如 "/order"。
② $route.params
一个 key/value 对象,包含了 动态片段 和 全匹配片段,
如果没有路由参数,就是一个空对象。
③ $route.query
一个 key/value 对象,表示 URL 查询参数。
例如,对于路径 /foo?user=1,则有 $route.query.user为1,
如果没有查询参数,则是个空对象。
④ $route.hash
当前路由的 hash 值 (不带 #) ,如果没有 hash 值,则为空字符串。
⑤ $route.fullPath
完成解析后的 URL,包含查询参数和 hash 的完整路径。
⑥ $route.matched
数组,包含当前匹配的路径中所包含的所有片段所对应的配置参数对象。
⑦ $route.name 当前路径名字
$router 是“路由实例”对象,即使用 new VueRouter创建的实例,包括了路由的跳转方法,钩子函数等。
2.$router常见跳转方法:
this.$router.go(-1)//跳转到上一次浏览的页面
this.$router.replace('/menu')//指定跳转的地址
this.$router.replace({name:'menuLink'})//指定跳转路由的名字下
this.$router.push('/menu')//通过push进行跳转
this.$router.push({name:'menuLink'})//通过push进行跳转路由的名字下
3.$router.push和$router.replace的区别
虚拟 dom 的实现原理
由于在浏览器中操作DOM是很昂贵的。频繁的操作DOM,会产生一定的性能问题。这就是虚拟Dom的产生原因。 Virtual DOM本质就是用一个原生的JS对象去描述一个DOM节点。是对真实DOM的一层抽象。
vuex
1、Getter
类似于 Vue 中的 计算属性(可以认为是 store 的计算属性),getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
1.1通过属性访问
getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。
2.2通过方法访问
getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
2、为何使用展开运算符
Proxy Proxy 相比于 defineProperty 的优势
1、Object.defineProperty缺点
1.1Object.defineProperty无法监控到数组下标的变化
无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,经过vue内部处理后可以使用以下几种方法来监听数组
push、pop、shift、unshift、splice、sort、reverse
2.2Object.defineProperty只能劫持对象的属性
Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
2、Proxy有以下两个优点
2.1可以劫持整个对象,并返回一个新对象
2.2有13种劫持操作
3、Proxy缺点
Proxy是es6提供的新特性,兼容性不好,最主要的是这个属性无法用polyfill来兼容
组件间通信
1、父子组件通信
父->子props,子->父 $on、$emit
获取父子组件实例 $parent、$children
Ref获取实例的方式调用组件的属性或者方法
2、兄弟组件通信
Event Bus实现跨组件通信 Vue.prototype.$bus = new Vue
Vuex
v-show、v-if 的实现原理
一、应用场景
v-if:当组件中某块内容只会显示或者只会隐藏,不会因为页面上的操作再次改变显示状态时。
v-show:当组件中某块内容显示隐藏是可以变化时。
二、两者实现显示隐藏的方式
v-if:动态的向dom树中添加或者删除dom元素
v-show:通过设置dom元素的display属性
三、编译条件
v-if:惰性的,如果初始条件为假,则什么也不做,只有在条件第一次变为真时,才开始编译
v-show:在任何条件下都会被编译
$nextTick
this.$nextTick()将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。
它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。
假设我们更改了某个dom元素内部的文本,而这时候我们想直接打印出这个被改变后的文本是需要dom更新之后才会实现的,
也就好比我们将打印输出的代码放在setTimeout(fn, 0)中;
computed/watch 原理
1、区别
computed 计算属性 :
依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。
watch 侦听器 :
更多的是「观察」的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
2、原理
2.1computed
computed 内部实现了一个惰性的 watcher,也就是 _computedWatchers,_computedWatchers 不会立刻求值,同时持有一个 dep 实例。
其内部通过 this.dirty 属性标记计算属性是否需要重新求值
变更流程如下:
当 computed 的依赖的状态发生改变时,就会通知这个惰性的 watcher。
computed watcher 通过 this.dep.subs.length 判断有没有订阅者,
有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)
没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
2.2watch
Vue 和 React 有什么不同
2、性能:vue性能内部已经做过处理了 React的性能优化主要在shouldcomponentupdate
3、语法 vue template react jsx
4、灵活 vue不太灵活 react比较灵活
1、监听数据变化的实现原理不同
Vue通过 getter/setter以及一些函数的劫持,能精确知道数据变化。
React默认是通过比较引用的方式(diff)进行的。
2、数据流的不同
React一直不支持双向绑定,提倡的是单向数据流,称之为onChange/setState()模式。
3、模板渲染方式的不同
在表层上,React是通过JSX渲染模板。而Vue是通过一种拓展的HTML语法进行渲染。
在深层上,模板的原理不同,这才是他们的本质区别:React是在组件JS代码中,通过原生JS实现模板中的常见语法,比如插值,条件,循环等,都是通过JS语法实现的,更加纯粹更加原生。而Vue是在和组件JS代码分离的单独的模板中,通过指令来实现的,比如条件语句就需要 v-if 来实现对这一点。
4、染过程不同
Vue可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
React在应用的状态被改变时,全部子组件都会重新渲染。通过shouldComponentUpdate这个生命周期方法可以进行控制,但Vue将此视为默认的优化。
Vue 组件中的 data 为什么要写成函数形式
一个组件被复用多次的话,也就会创建多个实例。
本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。
所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。
Vue 中 key 值的作用
key的作用主要是为了高效的更新虚拟DOM。
key 的作用就是更新组件时判断两个节点是否相同。相同就复用,不相同就删除旧的创建新的。
vue 和 react 都是采用 diff 算法来对比新旧虚拟节点,从而更新节点。
Vue 中常见的修饰符
一、v-model 修饰符
1、.lazy:
输入框改变,这个数据就会改变,lazy 这个修饰符会在光标离开 input 框才会更新数据
2、.trim:
输入框过滤首尾的空格
3、.number:
先输入数字就会限制输入只能是数字,先字符串就相当于没有加 number,注意,不是输入框不能输入字符串,是这个数据是数字
二、事件修饰符
4、.stop:
阻止事件冒泡,相当于调用了 event.stopPropagation()方法
5、.prevent:
阻止默认行为,相当于调用了 event.preventDefault()方法,比如表单的提交、a 标签的跳转就是默认事件
6、.self:
只有元素本身触发时才触发方法,就是只有点击元素本身才会触发。比如一个 div 里面有个按钮,div 和按钮都有事件,我们点击按钮,div 绑定的方法也会触发,如果 div 的 click 加上 self,只有点击到 div 的时候才会触发,变相的算是阻止冒泡
7、.once:
事件只能用一次,无论点击几次,执行一次之后都不会再执行
8、.capture:
事件的完整机制是捕获-目标-冒泡,事件触发是目标往外冒泡
9、.sync
对 prop 进行双向绑定
10、.keyCode:
监听按键的指令,具体可以查看 vue 的键码对应表
Vue 中常用的指令
keep-alive 组件的作用
keep-alive 可以实现组件缓存,当组件切换时不会对当前组件进行卸载。
常用的两个属性 include/exclude,允许组件有条件的进行缓存。
两个生命周期 activated/deactivated,用来得知当前组件是否处于活跃状态。
Vue 中的 slot 与 slot-scope
1、单个插槽 | 默认插槽 | 匿名插槽
单个插槽是 vue 的官方叫法,但是其实也可以叫它默认插槽,或者与具名插槽相对,我们可以叫它匿名插槽。因为它不用设置 name 属性。
单个插槽可以放置在组件的任意位置,但是就像它的名字一样,一个组件中只能有一个该类插槽。相对应的,具名插槽就可以有很多个,只要名字(name 属性)不同就可以了。
2、具名插槽
具名插槽可以在一个组件中出现 N 次。出现在不同的位置。
父组件通过 html 模板上的 slot 属性关联具名插槽。没有 slot 属性的 html 模板默认关联匿名插槽。
3、作用域插槽 | 带数据的插槽
作用域插槽要求,在 slot 上面绑定数据。
作用域插槽和单个插槽和具名插槽的区别,因为单个插槽和具名插槽不绑定数据,所以父组件是提供的模板要既包括样式由包括内容的;而作用域插槽,父组件只需要提供一套样式(在确实用作用域插槽绑定的数据的前提下)。
* 插槽分为单个插槽、具名插槽、作用域插槽。
* 两个特点:
组件不知道自己要挂载的内容是什么
组件可能有自己的模板
* 具名插槽:子组件中的多个位置都需要父组件塞进不同的内容时,需要使用具名插槽。
* 作用域插槽:作用域插槽给了子组件将数据返给父组件的能力,子组件一样可以复用,同时父组件也可以重新组织内容和样式
mixins 混入
分发 Vue 组件中可复用功能
1、生命周期属性,会优先执行 mixins 当中的,后执行组件当中的
组件中引入两个 mixins 对象,先调用那个 mixins 对象,就先执行哪个(从右向左)。//mixins:[mixinsTest2,mixinsTest]
el-input 的 input change blur 事件
1、input 输入框 change 事件和 blur 事件
输入框的 change 和 blur 事件绝大多数情况下表现是一致的,输入结束后离开输入框会先后触发 change 和 blur。
区别:
当文本框获得焦点后,没有输入任何内容,或者最终文本框的值没有改变时,是不会触发 change 事件的,而 blur 事件始终会触发。如果希望文本框的值一发生改变就立马执行某些操作,而不是等到离开再执行,那么可以使用 keyup 事件。
2、@keyup.enter
该事件与 v-on:input 事件的区别在于:input 事件是实时监控的,每次输入都会调用,而@keyup.enter 事件则是在 pc 上需要点击回车键触发,而在手机上则是需要点击输入键盘上的确定键才可触发。
3、@input(或者是 v-on:input)
适用于实时查询,每输入一个字符都会触发该事件。
自定义指令
1、注册
注册自定义指令分为全局注册与局部注册两种
1.1 全局注册:
Vue.directive('xxx', {
inserted: function (el) {
//指令属性
}
});
1.2 局部注册:
var app = new Vue({
el: '#app',
directives: {
xxx: {
//指令属性
}
}
});
# 28.过滤器
1、插值表达式
<p>{{msg | msgFormat}}</p>
管道符前面的 msg:要过滤的内容
管道符后面的 msgFormat:是过滤器通过 msgFormat 来过滤
filters: {
msgFormat: function (myMsg) { //function 的第一个参数指的是管道符前面的 msg
return myMsg.replace(/我/g, '你')
}
}
2、注意
1. 当有局部和全局两个名称相同的过滤器时候,会以就近原则进行调用,即:局部过滤器优先于全局过滤器被调用! 2. 一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右
3.vue 中的过滤器不能替代 computed、methods、watch,因为过滤器不能改变真正的 data,而只是改变渲染结果
CSS样式穿透的三种方法
/deep/ 和 ::v-deep 属于深度选择器
1、使用 ::v-deep:
该方法对所有样式语法通用,即适用于 css、stylus、sass、scss 和 less
::v-deep .el-input__inner {
width: 160px;
}
2、使用 /deep/:
该方法适用的样式语法:sass、scss、less
.container /deep/ .el-input__inner {
width: 160px;
}
3、使用 >>>符号:
该方法适用的样式语法:css、stylus
.container >>> .el-input__inner {
width: 160px;
}
webpack
一、webpack的打包原理
识别入口文件
通过逐层识别模块依赖(Commonjs、amd或者es6的import,webpack都会对其进行分析,来获取代码的依赖)
webpack做的就是分析代码,转换代码,编译代码,输出代码
最终形成打包后的代码
二、什么是loader
webpack自身只支持js和json这两种格式的文件,对于其他文件需要通过loader将其转换为commonJS规范的文件后,webpack才能解析到
loader,它是一个转换器,将A文件进行编译成B文件,比如:将A.less转换为A.css,单纯的文件转换过程。
例如:css-loader、style-loader、postcss-loader、sass-loader
三、什么是plugin
四、loader和plugin的区别