Open py-tofee opened 4 years ago
computed和watch都可以观察属性的变化从而作出响应;
不同的是,computed的结果会被缓存,且只有在computed所依赖的响应式属性发生变化,或者computed的返回值发生变化时,才会去通知computed的watcher和组件的watcher;
computed的watcher会将自己的dirty属性设置为true(标记为脏数据),并通知组件watcher,当下次读取计算属性时,才会重新计算;
组件的watcher得到通知后,执行render函数,这时会重新读取计算属性的值,此时computed的watcher的dirty已经是true,所以computed重新计算,用于此次渲染,然后再将dirty设置为false。
watch并不具备缓存性,监听器watch提供一个监听函数,当监听的属性发生变化时,会立即执行该函数。
当数据发生变化时,watch会在computed之前执行。
vue实现响应式,并不是数据变化之后,立即更新DOM;而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新;所以vue是异步执行DOM更新的。 微任务和宏任务(异步)
它有两个重要属性: include--字符串或正则表达式,只有匹配的组件才会被缓存; exclude--字符串或正则表达式,任何匹配的组件都不会被缓存; router-view是vue-router内置组件,如果直接被包在keep-alive中,那么所有路径匹配到的视图组件都会被缓存;
<keep-alive>
<!--插槽用法-->
<router-view></router-view>
</keep-alive>
activated和deactivated两个钩子函数
VNode是一个类,可以生成不同类型的vnode实例,文本节点,注释节点等,不同类型的vnode表示不同类型的真实DOM元素;由于vue对组件采用类虚拟DOM来更新视图,当数据发生变化时,整个组件都要进件重新渲染;但是组件内并不是所有的DOM节点都要更新,所以将vnode缓存,并将当前新生成的vnode和上次缓存的oldvnode进行对比,只对需要更新的部分进行DOM操作,可以提升很多性能; 虚拟DOM最核心的部分是patch,patch将vnode渲染成真实的DOM; 通过patch算法,在现有的DOM上,计算出真正需要更新的DOM节点,最大限度减少DOM操作;
生命周期钩子函数其实是一个回调函数,当vue实例执行到某个状态时,会触发对应的生命周期钩子函数;vue实例创建时,会将实例的生命周期函数与构造函数(父级 Vue.options = {} 定义默认组件选项,影响全局)上定义的生命周期函数合并成一个数组,父级上的生命周期函数会先执行; 与mixin类似,mixin按照某种合并规则与组件选项合并,但是mixin不是组件之间共享的,而是会在组件上定义对应的属性和方法。
总结:先执行父组件的before,再执行子组件的before,当子组件ed(完成操作)的时候,父组件的ed才执行。
<el-form-item prop="fields">
<el-form-item v-for="(item, index) in list"
:key="index"
:prop="'fields.' + index + '.value'">
<el-input v-model="item.value"></el-input>
</el-form-item>
</el-form-item>
vue中的模板template无法被浏览器解析渲染,因为这不属于浏览器的标准,不是正确的HTML语法,所以需要将template转换成一个render函数,执行render函数返回对对应的HTML结构;模板编译分三个阶段:
在构建的时候使用vue-template-compiler插件把组件中的template转换成render函数,vue运行时就可以省去将template->AST->优化AST,标记静态节点->生成render函数 这一步骤,提升性能,减少代码体积;
v-for="(item, key, index) in data"
v-for="(item, key, index) of data"
如果data是数组,那么key就是索引,index是undefined; 如果data是对象,那么key就是键值,index是索引; 遍历对象时,按照Object.keys(data)的结果顺序遍历,遍历结果在不同的JS引擎中可能不同;
作用:使事件处理器绑定的方法只处理数据逻辑,而不用处理DOM事件细节;
<!--@click.capture与@click是不同的事件处理器,可以同时存在-->
<div @click.capture="log(1)" @click="log(2)" >
<div @click.capture="log(3)" @click="log(4)" ></div>
</div>
<div @click="log(1)" >
<div @click.stop="log(2)" @click="log(3)" ></div>
</div>
<div class="box-out" @click="log(1)" >
<div class="box-in" @click="log(2)" >
<a @click.prevent="log(3)" href="/internal/black">A</a>
</div>
</div>
<div class="box-out" @click="log(1)" >
<div class="box-in" @click.self="log(2)" >
<a @click.prevent="log(3)" href="/internal/black">A</a>
</div>
</div>
<div class="box-out" @click="log(1)" >
<div class="box-in" @click.once="log(2)"></div>
</div>
补充:once修饰符除了用于原生DOM事件,还可以用于自定义的组件事件上,当once为true,表示当事件监听器被添加之后,会在其第一次被调用之后自动移除;
v-mybind:click.stop="handle"
<input v-model.lazy="msg" />
,elementUI的el-input不支持,因为el-input组件解析后input被包裹在<div class="el-input"></div>
中;vue3升级了v-model,去掉了.sync修饰符;
:propName.sync="value"
,然后子组件配合$emit('update:propName', value)
实现更新;v-model:propName="value"
,这样一个组件或者表单元素可以实现多个prop的双向绑定了
<my-component
v-model.capitalize:="value"
v-model:title.capitalize:="title"
></my-component>
my-component组件内部:
{
props: ['modelValue', 'modelValueModifiers', 'title', 'titleModifiers'], // Modifiers保存的是修饰符
emits: ['update:modelValue', 'update:title']
}
// this.modelValueModifiers = {capitalize: true}
// this.titleModifiers = {capitalize: true}
{
emits: {
click: null, // 没有验证
submit: ({name}) => {
return !!name
} // 验证submit事件,判断name是否有值
}
}
props: {
name: {
type: String/Number/Boolean/Array/Object/Date/Function/Symbol/自定义构造函数, // 类型
required: true, // 是否必传
default: 'a', // 默认值
validator(value) { // 自定义验证函数,校验prop是否满足条件
return value.includes('a')
}
}
}
v-bind="$attrs"
;如果一个组件有多个根元素,那么必须显示的将$attrs绑定。slot又名插槽,是Vue的内容分发机制;分三类,默认插槽,具名插槽和作用域插槽。 实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。(子组件数据传递给父组件)
<!--父组件-->
<template v-slot:header></template>
<!--子组件-->
<slot name="header"></slot>
<!--父组件-->
<template v-slot:header="slotProps">{{slotProps.title}}</template>
<!--父组件-解构插槽prop-->
<template v-slot:header="{ item, title }">{{title}}</template>
<!--父组件-解构插槽prop-重命名和设置默认值-->
<!--给index重命名为key,给title设置了默认值'hello'-->
<template v-slot:header="{ item, title="hello", index: key }">{{title}} {{key}}</template>
<!--子组件-->
<slot name="header" :item="item" :title="title"></slot>
<template>
元素上,但当只存在默认插槽时,v-slot可以用于组件标签上:
<todo-list v-slot="slotProps">{{slotProps.item}}</todo-list>
<!--子组件-只存在默认插槽-->
<slot :item="item" :index="index"></slot>
<template v-slot:[dynamicSlotName]></template>
<!--跟动态指令参数一样,如下-->
<a v-bind:[attributeName]="url"> ... </ a>
<a v-on:[eventName]="doSomething"> ... </ a>
v-slot:
缩写为#
,#
后面必须带有插槽名
<template v-slot:header="{item}">...</template> 缩写为 <template #header="{item}">...</template>
<template v-slot:default="{item}">...</template> 缩写为 <template #default="{item}">...</template>
<template v-slot="{item}">...</template> 缩写为 <template #default="{item}">...</template>
<template slot="todo" slot-scope="{ todo }"></template>
vue2.6.0后用 v-slot 代替 slot和slot-scope
<template v-slot:todo="{todo}"></template>
const app = Vue.createApp({
myOption: 'hello!'
})
// 为自定义的选项 'myOption' 注入一个处理器。
app.mixin({
created() {
const myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})
app.mount('#app') // => "hello!"
setup(props, context)
允许我们控制当前组件或者元素在指定的父节点下渲染,但不影响vue-devtools中的组件上下级关系
<teleport to="body"><modal>...</modal></teleport>
<teleport to="#box"><div>...</div></teleport>
<div id="box"></div>
const FunctionalComponent = (props, context) => {
// ...
}
第二个参数 context 包含三个 property:attrs、emit 和 slots。它们分别相当于实例的 $attrs、$emit 和 $slots 这几个 property。
const original = reactive({ count: 0 })
const copy = readonly(original)
修改copy将会失败并出现告警
this.$nextTick(callback)
,该函数返回一个promise,所以可以使用await/async简写
await this.$nextTick();
// todo something
target[key] = val
,不做其他操作target.__ob__.dep
中export function set (target: Array<any> | Object, key: any, val: any): any {
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
原理:组件实例的自定义事件都存在事件中心_events属性中,不管是添加、移除还是触发事件,都是操作_events。
vm.$once( event, callback )
监听一个自定义事件,在当前组件生命周期中,只执行一次,一旦触发之后,监听器会自动移除vm.$off( [event, callback] )
移除自定义事件监听器
在create的时候,还不能访问dom,在mounted的时候,dom可以访问,由于生命周期钩子函数都是同步执行的,ajax是异步执行,不会阻塞生命周期钩子函数,所以无论放在create还是mounted,都是在生命周期初始化执行完成之后,再执行ajax(事件循环:先执行同步,在执行异步); 通常为了保证统一,一般在客户端渲染中,将ajax放在mounted中;而服务端渲染不支持mounted,所以服务端渲染一般放在create中。
原生事件和组件事件
@click.native=""
因为JavaScript中,页面上的事件处理程序数量直接关系到页面额度性能,因为事件处理程序就是函数,函数就是对象,就会占用内存,而且每个事件处理程序,都会访问DOM,访问DOM次数过多,也会影响页面的交互时间。
// 下面这种情况,当list数据量大的时候,会使用addEventListener绑定很多事件监听器
<div v-for="item in list" @click="handleClick"></div>
// 使用事件代理优化(事件冒泡):将事件提升到父元素,这样只需要绑定一个事件处理器
<div @click="handleClick">
<div v-for="item in list" @click="handleClick"></div>
</div>
model: {value: '', event: ''}
;如果组件的功能很多,那么子组件可以使用异步加载的方式,主要是利用import函数实现;当组件需要被渲染的时候,才去加载这个组件。
components: {
addItem: (resolve) => import("../components/addItem")
}
Vue响应式数据原理
vue在初始化数据时,通过Object.defineProperty将数据对象转换成getter/setter的形式来追踪变化,读取数据的时候会触发getter,并在getter中进行依赖收集(收集当前组件的watcher);修改数据的时候会触发setter,setter去通知getter中收集的依赖(watcher);watcher接收到通知后,去触发视图更新或者触发某个回调函数。(发布订阅设计模式)
Array变化侦测:
Array也是在getter中收集依赖,但是是在拦截器中触发依赖; 因为数组是通过方法来改变内容的,所以vue通过创建拦截器(自定义的方法)去覆盖数组原型方法的方式来追踪数组的变化的; 覆盖原型,即覆盖数组的_proto_属性,
如果某些浏览器不支持_proto_属性,那么直接将拦截方法设置到被侦测的数组array上,也是有效的;因为当访问一个对象的方法时,只有其自身不存在这个方法时,才会去他的原型上查找这个方法。 数组的新元素赋值操作,数组设置length操作,不能被拦截到,因为不属于原型上的方法;
proxy的优势:
由于Object.defineProperty只能追踪一个数据是否被修改,无法追踪新增和删除属性的变化,只能通过vm.$set(确保新增的属性是响应式的)和vm.$delete(确保数据被删除之后能通知到watcher)解决。 使用proxy代理对象,直接拦截对象的所有操作,包括对象的新增和删除属性,数组的新元素赋值操作,数组设置length操作