目录
为什么data必须是一个函数
一、实例和组件定义data的区别
一、实例和组件定义data的区别
vue常用的指令
v-if与v-show的区别
Vue生命周期的执行过程
Vue的双向数据绑定原理
浏览器的回流与重绘
Vue中的虚拟dom
vue中的diff算法
Vue组件通信(父传子、子传父、兄弟通信)
一.父组件传到子组件
二.子组件向父组件传值
3. 非父子传参 (事件总线)
事件总线推荐下面写法:
vue监听和深度监听watch
Deep handler immediate
computed,methods,watch的区别
mvvm和mvc
三者的关系:
vue中的修饰符
一、事件修饰符
二、 v-model的修饰符
三、键盘事件的修饰符
四、element的修饰符
slot插槽
keep-alive
v-for为什么要使用key
$nextTick()的作用
一、实例和组件定义data的区别
vue实例中的data属性既可以是一个对象,也可以是一个函数
const app = new Vue({
el:"#app",
// 对象格式
data:{
foo:"foo"
},
// 函数格式
data(){
return {
foo:"foo"
}
}
组件中定义data属性,只能是一个函数
如果为组件data直接定义为一个对象
Vue.component('component1',{
template:`组件`,
data:{
foo:"foo"
}
})
则会得到警告信息
警告说明:返回的data应该是一个函数在每一个组件实例中
上面讲到组件data必须是一个函数,不知道大家有没有思考过这是为什么呢?
在我们定义好一个组件的时候,vue最终都会通过Vue.extend()构成组件实例
这里我们模仿组件构造函数,定义data属性,采用对象的形式
function Component(){
}
Component.prototype.data = {
count : 0
}
创建两个组件实例
const componentA = new Component()
const componentB = new Component()
修改componentA组件data属性的值,componentB中的值也发生了改变
console.log(componentB.data.count) // 0
componentA.data.count = 1
console.log(componentB.data.count) // 1
产生这样的原因这是两者共用了同一个内存地址,componentA修改的内容,同样对componentB产生了影响
如果我们采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同)
function Component(){
this.data = this.data()
}
Component.prototype.data = function (){
return {
count : 0
}
}
修改componentA组件data属性的值,componentB中的值不受影响
console.log(componentB.data.count) // 0
componentA.data.count = 1
console.log(componentB.data.count) // 0
vue组件可能会有很多个实例,采用函数返回一个全新data形式,使每个实例对象的数据不会受到其他实例对象数据的污染
三、总结
根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象
{{message}}
15.v-pre 把标签内部的元素原位输出
一、相同点
都可以动态控制着dom元素的显示隐藏
一、区别
v-if: 控制DOM元素的显示隐藏是将DOM元素整个添加或删除;
v-show: 控制DOM 的显示隐藏是为DOM元素添加css的样式display,设置none或者是block,DOM元素是还存在的
二、性能对比
v-if有更高的切换消耗;
v-show有更高的初始渲染消耗
三、使用场景
v-if适合运营条件不大可能改变的场景下;
v-show适合频繁切换;
例如:
v-if:
在请求后台接口,用接口返回数据渲染生成多个元素的时候,由于网速太慢,导致数据刚开始没有值,此时我们页面也会进行渲染空数据就会报错。
我们可以把一个元素作为包装元素,并使用v-if 进行条件判断,如果有值,才会显示内部元素。最终的渲染不会包含这个元素,v-show是不支持这么做的。
{{seller.supports.length}}个
export default {
data() {
return {
seller:null
}
},
//生命周期 - 创建完成(访问当前this实例)
created() {
this.$axios.get('http://www.test.com').then(res=>{
this.seller=res.data
})
},
}
组件刚创建,渲染完成后,请求未必能完成。所以一开始的seller.supports只是一个空值,必须在之前加上v-if,确保有值以后再开始计算length
{{seller.supports.length}}个
v-show:
商品分类选项卡,频繁切换;
答: 总共分为8个阶段。创建前/后,载入前/后,更新前/后,销毁前/后。
创建前/后: 在beforeCreated阶段,vue实例的挂载元素$el和数据对象 data 都为undefined,还未初始化。在 created阶段,vue实例的数据对象data有了,$el还没有。
载入前/后: 在beforeMount阶段,vue实例的$el和data都初始化了,但还是挂载之前为虚拟的dom节点,data.message还未替换。在mounted阶段,vue实例挂载完成,data.message成功渲染。
更新前/后: 当data变化时,会触发beforeUpdate和updated方法。
销毁前/后: 在destroy阶段,对data的改变不会再触发周期函数,说明此时vue实例已经解除了事件监听以及和dom的绑定,但是dom结构依然存在。destroyed阶段,组件销毁
答:
vue.js是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调来渲染视图。
具体步骤:
第一步: 需要observer(观察者)对数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
第二步: compile(模板解析器)解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
第三步: Watcher(订阅者)是Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
第四步: MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
回答以上内容即可,下方内容,可以帮助大家理解
名词解释:
1. 什么是setter、getter
答:首先,别误以为他们就是一会要说的get、set
对象有两种属性:
数据属性: 就是我们经常使用的属性
访问器属性: 也称存取器属性(存取器属性就是一组获取和设置值的函数)
再看一行代码:
log打印出来的如下:
数据属性就是a和b;
get和set就是关键字 它们后面各自对应一个函数,这个函数就是上面红字部分所讲的,存储器属性。
get对应的方法称为getter,负责获取值,它不带任何参数。set对应的方法为setter,负责设置值,在它的函数体中,一切的return都是无效的。
2. 什么是Object.defineProperty() ?
答:我们先看一句定义:
对象是由多个名/值对组成的无序的集合。对象中每个属性对应任意类型的值。
除了以上添加属性的方式,当然还可以使用Object.defineProperty定义新属性或修改原有的属性;
语法:
Object.defineProperty(obj, prop, descriptor)
参数:
obj: 必需。目标对象;
prop: 必需。需定义或修改的属性的名字;
descriptor: 必需。目标属性所拥有的特性;
返回值:
传入函数的对象,即第一个参数obj;
OK,定义介绍完了,我们现在说一下一会关于双向绑定我们要用到的知识点:存取器描述;(诶?你是不是发现和上面好像有点关系)
对头,它是这样使用的:
现在无论是你获取还是设置我们都可以接到通知,是不是有一点双向数据绑定的影子了,别急下面还有;
OK,我终于叨叨完没用的了,现在开始说正题,如何理解Vue的双向数据绑定,哈哈,先来一个定义:
Vue是采用数据劫持结合发布/订阅模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
是不是有点明白了呢,当然这也不是全部,我们刚刚说的大概就是下面Observer的部分,对每个vue中的data中定义的属性循环用Object.defineProperty()实现数据劫持,以便利用其中的setter和getter,然后通知订阅者,订阅者会触发它的update方法,对视图进行更新。
别急,下面也很简单:
Dep,它就像一个依赖管理一样,小伙伴又问啥是依赖管理倪? 上图!
我用一个例子来解释一下上面这张图 下面高能预警:
在vue中v-model,v-name,{{}}等都可以对数据进行展示,也就是说假如一个属性都通过这三个指令了,那么每当这个属性改变的时候,相应的这个三个指令的html视图也必须改变;
于是vue中就是每当有这样的可能用到双向绑定的指令,就在一个Dep中增加一个订阅者(addSub),其订阅者只是更新自己的指令对应的数据,也就是v-model='name'和{{name}}有两个对应的订阅者,各自管理自己的地方;
每当属性的set方法触发,就循环更新Dep中的订阅者(notify);
OK,Dep是不是很明白了呢
集合上面的那张图来看,就是Observer一旦有了set触发,就会通知到Dep,那Dep接到通知之后呢?从图上来看,下面所讲的就应该是Compile了,也很简单:
首先,先要知道它负责干什么?
compile主要做的事情是解析模板指令,将模板中的变量替换成数据
1)初始化,init的时候 初始化渲染页面视图;
2)将每个指令对应的节点绑定更新函数,添加监听数据的订阅者;
Dep负责维护依赖,而订阅者则来自于compile,一旦有数据变动,则会绑定更新函数,此时也就是产生了订阅者,这个时候Dep内就增加了一个订阅者,而一旦数据变动,则会收到通知,更新视图;
好了,你是不是觉得上面这行说不通,或是读不通,当然,因为上面的这个流程了缺少了,我们最后要说的Watcher,我把上面这句话补全,就是Watcher的工作了;
Dep负责维护依赖,而订阅者则来自于compile,一旦有数据变动,则会通过Watcher绑定更新函数,此时Watcher也向Dep中添加了订阅者,一旦Dep接到Observer的通知,它就会再去通知Watcher,Watcher则会调用自身的update()方法,并触发Compile中绑定的回调,更新视图;
最后敲黑板:
首先我们为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;
然后在编译的时候在该属性的数组dep中添加订阅者,v-model会添加一个订阅者,{undefined{}}也会,v-bind也会,只要用到该属性的指令理论上都会;
接着为input会添加监听事件,修改值就等于为该属性赋值,则会触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。
下面让我们来看代码演示:
先简单的实现一个js的双向数据绑定来熟悉一下Object.defineProperty()方法
输入的值为:
这样我们就能实现js的双向数据绑定,随着文本框输入文字的变化,span中会同步显示相同的文字内容;这样就实现了 model => view 以及 view => model 的双向绑定。
通过给in输入框添加事件监听input来触发obj对象的set方法,而set再修改了访问器属性的同时,也修改了dom样式,改变了span标签内的文本。
回流:当页面元素的尺寸,样式,布局,dom增删发生改变时,浏览器会重新构造页面,这就是回流
重绘:当页面中的元素属性发生改变,例如颜色,背景色,而这些属性只影响元素的外观风格,不影响布局时,浏览器就会进行重绘,修改这些更改的属性
回流与重绘的区别
回流一定会引起重绘,但是重绘不一定要回流,只有在页面的布局和几何属性发生改变时,才会进行回流,例如:删除元素后引发元素位置的改变,属性导致大小,边框,内外边距改变等
如何避免重绘
1 集中改变样式,通过同样的类名进行改变
2 将频繁操作变为一次性操作,例如进行for循环添加dom结构或者修改数据
一、什么是虚拟DOM
通过js创建一个Object对象来模拟真实DOM结构,这个对象包含标签名 (tag)、属性 (attrs) 和子元素对象 (children) 三个属性,通过vue中的render()函数把虚拟dom编译成真实dom,在通过appendChild()添加到页面中。
是不是一下子秒懂 没懂再来一张
创建虚拟DOM就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟DOM对象的节点与真实DOM的属性一一照应
在vue中如何应用虚拟DOM的
定义真实DOM
节点内容
{{ foo }}
实例化vue
const app = new Vue({
el:"#app",
data:{
foo:"foo"
}
})
const app = new Vue({
el:"#app",
data:{
foo:"foo"
}
})
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
functionalContext: Component | void; // only for functional component root nodes
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions
) {
/*当前节点的标签名*/
this.tag = tag
/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.data = data
/*当前节点的子节点,是一个数组*/
this.children = children
/*当前节点的文本*/
this.text = text
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm
/*当前节点的名字空间*/
this.ns = undefined
/*编译作用域*/
this.context = context
/*函数化组件作用域*/
this.functionalContext = undefined
/*节点的key属性,被当作节点的标志,用以优化*/
this.key = data && data.key
/*组件的option选项*/
this.componentOptions = componentOptions
/*当前节点对应的组件的实例*/
this.componentInstance = undefined
/*当前节点的父节点*/
this.parent = undefined
/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false
/*静态节点标志*/
this.isStatic = false
/*是否作为跟节点插入*/
this.isRootInsert = true
/*是否为注释节点*/
this.isComment = false
/*是否为克隆节点*/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next https://github.com/answershuto/learnVue*/
get child (): Component | void {
return this.componentInstance
}
}
这里对VNode进行稍微的说明:
源码create-element.js
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
上面可以看到createElement 方法实际上是对 _createElement 方法的封装,对参数的传入进行了判断
export function _createElement(
context: Component,
tag?: string | Class | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array {
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context`
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
...
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if ( === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
// 创建VNode
...
}
可以看到_createElement接收5个参数:
context 表示 VNode 的上下文环境,是 Component 类型
tag 表示标签,它可以是一个字符串,也可以是一个 Component
data 表示 VNode 的数据,它是一个 VNodeData 类型
children 表示当前 VNode的子节点,它是任意类型的
normalizationType 表示子节点规范的类型,类型不同规范的方法也就不一样,主要是参考 render 函数是编译生成的还是用户手写的
根据normalizationType 的类型,children会有不同的定义
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if ( === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
simpleNormalizeChildren方法调用场景是 render 函数是编译生成的
normalizeChildren方法调用场景分为下面两种:
render 函数是用户手写的
编译 slot、v-for 的时候会产生嵌套数组
无论是simpleNormalizeChildren还是normalizeChildren都是对children进行规范(使children 变成了一个类型为 VNode 的 Array),这里就不展开说了
规范化children的源码位置在:src/core/vdom/helpers/normalzie-children.js
在规范化children后,就去创建VNode
let vnode, ns
// 对tag进行判断
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// 如果是内置的节点,则直接创建一个普通VNode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
// 如果是component类型,则会通过createComponent创建VNode节点
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}
createComponent同样是创建VNode
源码位置:src/core/vdom/create-component.js
export function createComponent (
Ctor: Class | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array,
tag?: string
): VNode | Array | void {
if (isUndef(Ctor)) {
return
}
// 构建子类构造函数
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}
data = data || {}
// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}
// 安装组件钩子函数,把钩子函数合并到data.hook中
installComponentHooks(data)
//实例化一个VNode返回。组件的VNode是没有children的
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}
return vnode
}
稍微提下createComponent生成VNode的三个关键流程:
构造子类构造函数Ctor
installComponentHooks安装组件钩子函数
实例化 vnode
小结
createElement 创建 VNode 的过程,每个 VNode 有 children,children
每个元素也是一个VNode,这样就形成了一个虚拟树结构,用于描述真实的DOM树结构
diff算法就是进行虚拟节点对比,并返回一个patch对象,用来存储两个节点不同的地方,最后用patch记录的消息去局部更新Dom。
父组件是通过props属性给子组件通信的
数据是单向流动 父—>子 (子组件中修改props数据,是无效的,会有一个红色警告)
1 . 父组件parent.vue
代码如下:
{{ msg }}
2 . 子组件son代码如下:
{{ sonMsg }}
子组件接收到内容:{{ faMsg }}
子组件通过props来接受数据
第一种方法
props: ['childCom']
第二种方法
props: {
childCom: String //这里指定了字符串类型,如果类型不一致会警告的哦
}
第三种方法
props: {
childCom: {
type: String,
default: 'sichaoyun'
}
}
通过绑定事件然后及$emit传值
vue2.0只允许单向数据传递,我们通过出发事件来改变组件的数据
1.父组件parent代码如下:
父组件通过绑定自定义事件,接受子组件传递过来的参数
{{ msg }}
父组件接手到的内容:{{ username }}
2.子组件son代码如下:
子组件通过
$emit
触发父组件上的自定义事件,发送参数
{{ sonMsg }}
子组件接收到内容:{{ psMsg }}
假设你有两个Vue组件需要通信: A 和 B ,A组件按钮上面绑定了点击事件,发送一则消息,B组件接收。
1. 初始化,全局创建$bus
直接在项目中的 main.js 初始化 $bus :
// main.js
window.$bus=new Vue();
注意,这种方式初始化一个 全局的事件总线
。
2. 发送事件$bus.$emit("aMsg", '来自A页面的消息');
接下来,我们需要在 B页面 中接收这则消息。
4. 接收事件$bus.$on("事件名",callback)
{{msg}}
<<<<<<<<<<<<<<下方是拓展,面试不必说>>>>>>>>>>>
集中式的事件中间件就是 Bus。我习惯将bus定义到全局:
app.js
var eventBus = {
install(Vue,options) {
Vue.prototype.$bus = vue
}
};
Vue.use(eventBus);
然后在组件中,可以使用$emit, $on, $off
分别来分发、监听、取消监听事件
:
分发事件的组件
// ...
methods: {
todo: function () {
this.$bus.$emit('todoSth', params); //params是传递的参数
//...
}
}
监听的组件
// ...
created() {
this.$bus.$on('todoSth', (params) => { //获取传递的参数并进行操作
//todo something
})
},
// 最好在组件销毁前
// 清除事件监听
beforeDestroy () {
this.$bus.$off('todoSth');
}
如果需要监听多个组件,只需要更改 bus 的 eventName:
// ...
created() {
this.$bus.$on('firstTodo', this.firstTodo);
this.$bus.$on('secondTodo', this.secondTodo);
},
// 清除事件监听
beforeDestroy () {
this.$bus.$off('firstTodo', this.firstTodo);
this.$bus.$off('secondTodo', this.secondTodo);
}
watch可以让我们监控一个值的变化,从而做出相应的反应。
用户名:
你的名字是:{{name}}
通过watch属性可以监控data属性中name值的变化,定义监控时,name这个值对应的是一个监控处理函数name()。
将name属性和对话框绑定,并在对应页面绑定name值,然后在浏览器中测试,页面上数据会随着对话框中输入值的变化而变化。
其中监控处理函数name有两个参数:
v1表示当前监控的值。
v2表示上一次监控的值。
如果监控的是一个对象,需要进行深度监控,才能监控到对象中属性的变化:
用户名:
你的名字是:{{person.name}}
你的年龄是:{{person.age}}
现在定义监控时,person是一个对象,并且要指定两个属性:
deep:代表深度监控,不仅监控person变化,也监控person中属性变化。
handler:就是以前的监控处理函数。
通过v.name和v.age获取对象中具体的值。
与computed比较:
computed是计算属性,对应数据变化,自动计算,需要return返出最终的值,拥有缓存,数据不变时,调用缓存
methods是vue中的事件与方法,需要手动调用才能触发
watch是数据监听,不需要return返回值,数据变化自动监听,可判别变化前后的差异
mvvm
概念:MVVM是 Model-View-ViewModel 的缩写,分别对应着:数据,视图,视图模型。Model是我们应用中的数据模型,View是我们的UI视图层,通过ViewModle,可以把我们Modle中的数据映射到View视图上,同时,在View层修改了一些数据,也会反应更新我们的Modle。简单理解就是双向数据绑定,即当数据发生变化的时候,视图也就发生变化,当视图发生变化的时候,数据也会跟着同步变化。
(1)MVC:是后台的框架模式
分为M:(model模型)、V(view试图)、C(controller控制器)
(2)MVVM是为了实现MVC中的V
MVVM分为:M(model数据)、V(view试图)、VM(viewModel控制数据的改变和控制试图)
以vue为例:
view 对应 template,vm 对应 new Vue({…}),model 对应 data
1 . view 视图层
Hello, {{ name }}!
--View层 --VUE中的单向绑定
--View层 --VUE中的双向绑定
2 . model数据层
data: {
name: '' --Model层
}
3 .VM层
VUE框架已封装好
new Vue({…})
view 可以通过事件绑定(或v-model指令)的方式影响 model,model 可以通过数据绑定的形式影响到view,viewModel是把 model 和 view 连起来的连接器
MVC模式概要(典型的框架有angular.js)
1 . 即Model、View、Controller即数据模型、视图、控制器。
View:视图层;
Model:业务数据层;
Controller: 控制器。接收View层传递过来的指令,选取Model层对应的数据,进行相应操作。
2.举一个现实中的类似的例子,MVC如同一家商铺的运作模式,View层相当于是这家商铺的店面,Model层相当于这家商铺的仓库,Controller层相当于是这家商铺的执行部门。
特点:MVC有如下两种模式,不管哪种模式,MVC的通信都是单向的,由图也可以看出,View层会从Model层拿数据,因此MVC中的View层和Model层还是存在耦合的。
.stop 阻止事件继续传播
.prevent 阻止标签默认行为
.capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
.self 只当在 event.target 是当前元素自身时触发处理函数
.once 事件将只会触发一次
.passive 告诉浏览器你不想阻止事件的默认行为
...
...
...
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用v-on:click.prevent.self
会阻止所有的点击,而 v-on:click.self.prevent
只会阻止对元素自身的点击。
<1> .lazy
默认情况下,v-model同步输入框的值和数据。可以通过这个修饰符,转变为在change事件再同步。
<2> .number
自动将用户的输入值转化为数值类型
<3> .trim
自动过滤用户输入的首尾空格
在我们的项目经常需要监听一些键盘事件来触发程序的执行,而Vue中允许在监听的时候添加关键修饰符:
对于一些常用键,还提供了按键别名:
全部的按键别名:
.enter
.tab
.delete (捕获“删除”和“退格”键)
.esc
.space
.up
.down
.left
.right
修饰键:
.ctrl
.alt
.shift
.meta
Do something
与按键别名不同的是,修饰键和 keyup 事件一起用时,事件引发时必须按下正常的按键。换一种说法:如果要引发 keyup.ctrl,必须按下 ctrl 时释放其他的按键;单单释放 ctrl 不会引发事件。
对于elementUI的input,我们需要在后面加上.native, 因为elementUI对input进行了封装,原生的事件不起作用。
插槽分为匿名插槽,具名插槽,作用域插槽
匿名插槽
子组件内部,设置
匿名插槽其实也是有名字的,为 default ,通常都是不写的
具名插槽
通过给插槽命名,以此来区分多个插槽
命名:v-slot:name 可简写为 #name
V2的使用
或
V3的使用
作用域插槽
通过自定义属性,来预备插槽所使用的数据
子组件中
子组件标签中
//解构作用域插槽的数据
{{say1}}
keep-alive 是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。keep-alive 是一个抽象组件:它自身不会渲染成一个 DOM 元素,也不会出现在父组件链中。
在组件切换过程中 把切换出去的组件保留在内存中,防止重复渲染DOM,减少加载时间及性能消耗,提高用户体验性
被包含在 keep-alive 中创建的组件,会多出两个生命周期的钩子: activated(组件激活时使用) 与 deactivated(组价离开时调用)
如果需要缓存整个项目,直接在app.vue中用keep-alive包裹router-view即可。要缓存部分页面,需要在路由地址配置中,在meta属性中添加一个状态,在app.vue中判断一下包裹的router-view即可
key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点
$nextTick()是在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
使用场景
你在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中,因为在此时还拿不到dom节点
如果是一个input框,在让他显示之后,立即让他自动获取焦点,是无法实现的,因为vue的render()函数将虚拟dom转换真实dom的时候,是异步的,无法在当时直接获取到dom,因此,可以使用this.$nextTick(),再里面书写获取焦点的代码,就可以实现这个功能了
前端路由原理
路由就是用来解析URL以及调用对应的控制器,在不重新请求页面的情况下,更新页面的视图
路由模式
路由分为hash模式与history模式
hash与history的切换
在router的index.js文件中,有一个new Router(),里面的mode默认为hash模式,可以在这里更改为history模式
const router = new VueRouter({
routes,
mode:'hash' //可在这里更改为history模式
})
hash模式
hash模式:在浏览器中符号的“#”,以及#后面的字符称之为hash
http://localhost:8080/#/login
特点
1 hash不会被包含在http请求中,因此,改变hash不会重新加载页面
2 hashchange 用来监听hash改变的事件
3 每一次改变,都会在浏览器中添加一个历史记录
方法
HashHistory.push 跳转路由,添加历史记录栈
HashHistory.replace() 跳转路由,直接替换当前页面,不会添加历史
history模式
http://localhost:8080/home
特点
1 history模式的URL要与后端的URL一样,后端如果没有对应路由,则会返回404错误
2 popState(window.onpopstate)用来监听history改变的事件
方法
history.pushState() 跳转路由,添加历史记录栈
history.replaceState() 跳转路由,直接替换当前页面,不会添加历史
路由传参
query,params,动态路由传参
(1) params只能使用name query可以使用name和path
(2) 使用params传参刷新后丢失,而query传参刷新后不会丢失
(3) Params在地址栏中不会显示,query会显示
(4) Params可以和动态路由一起使用,query不可以
导航式路由
编程式路由
router.push( ) | 将有记录存放在历史栈中,点击后退,将返回上一级的记录 router-link to 就是调用的push |
router.replace() | 不会存放历史栈,而是替换了当前的历史 可以在router-link中添加replace或router.push对象中添加replace:true 激活当前功能 |
router.go(-3) | ()里的参数为整数,指的是在历史栈中前进或后退的步数 -1为返回上一历史,0为刷新页面,1为返回下一历史 |
路由守卫分为全局守卫,路由独享守卫,组件内守卫
每个守卫接收三个参数
to 即将要进入的目标路由对象
from 当前导航正要离开的路由
next 钩子函数,里面定义参数,确认下一步路由要做什么
全局守卫
router.beforeEach | 全局前置守卫 最合适用在判断进入的页面是否需要登录 |
router.beforeResolve | 全局解析守卫 在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用 |
router.afterEach | 全局后置钩子 一般用来设置动态的请求头 再这里next()是多余的 |
组件内守卫
beforeRouteEnter | 进组组件前的守卫 注意:在这里是拿不到vue的this的,但是可以通过传一个回调给 next来访问组件实例 next ( vm=> { console.log(vm) } ) vm就是vue的组件实例 通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消 |
beforeRouteUpdate | 路由更新时的守卫 |
路由更新时的守卫 | 离开组件时的守卫 |
完整的导航解析流程
1 在失活的组件里调用 beforeRouteLeave 组件离开守卫。
2 调用全局的 router.beforeEach 全局前置守卫。
3 在重用的组件里调用 beforeRouteUpdate 组件更新守卫 。
4 在路由配置里调用 beforeEnter 路由独享守卫
5 解析异步路由组件。
6 在被激活的组件里调用 beforeRouteEnter 组件进入守卫
7 调用全局的 router.beforeResolve 全局解析守卫 。
8 导航被确认。
9 调用全局的 router.afterEach 全局后置钩子。
10 触发 DOM 更新。
11 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
重写route的方法看这里
在2.x版本里。不管数据多大,都会在一开始就为其创建观察者,在数据很大时,就会造成性能的问题。在3.x中,只会对渲染出来的数据创建观察者,而且3.x的观察者更高效。
Vue2--------------vue3
beforeCreate -> setup() 开始创建组件之前,创建的是data和method
created -> setup()
beforeMount -> onBeforeMount 组件挂载到节点上之前执行的函数。
mounted -> onMounted 组件挂载完成后执行的函数
beforeUpdate -> onBeforeUpdate 组件更新之前执行的函数。
updated -> onUpdated 组件更新完成之后执行的函数。
beforeDestroy -> onBeforeUnmount 组件挂载到节点上之前执行的函数。
destroyed -> onUnmounted 组件卸载之前执行的函数。
activated -> onActivated 组件卸载完成后执行的函数
deactivated -> onDeactivated