vue系列-vue入门
Vue官网:https://cn.vuejs.org/v2/guide/installation.html
Vue-Cli 官网:https://cli.vuejs.org/zh/guide/
当前文档为2.x版本
理论
常见概念
双向绑定:在表单控件或者组件上使用 v-model 创建双向绑定。数据修改视图会进行修改,视图修改数据也会跟着修改。即vue实例中的data与其渲染的DOM元素的内容保持一致,无论谁被改变,另一方会相应的更新为相同的数据。这是通过设置属性访问器实现的。
双向绑定 = 单向绑定 + UI事件监听
vue是什么
Vue 是一套用于构建用户界面的渐进式框架。
与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。
Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。
另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
Vue2.0 中,“渐进式框架”和“自底向上增量开发的设计”这两个概念是什么?
框架做分层设计,每层都可选,不同层可以灵活接入其他方案。
Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。
没有完全遵循 MVVM 模型,但是核心思想相同。MVVM与其他架构模型有什么不同
版本
Runtime + Compiler
包含编译代码的,可以把编译过程放在运行时做。引用的 vue
版本文件vue.js
// 需要编译器
new Vue({
template: '<div>{{ hi }}</div>'
})
编译器会将 template
进行parse AST -> optimiz -> codegen
后生成使用 with(this){...}
包裹的函数,然后在运行的时候进行使用。
Runtime Only
推荐使用。不包含编译代码的,需要借助 webpack 的 vue-loader
事先把模板编译成 render
函数。引用的 vue
版本文件vue.runtime.js
当使用
vue-loader
的时候,*.vue
文件内部的模板会在构建时预编译成 JavaScript。
// 不需要编译器
new Vue({
render (h) {
return h('div', this.hi)
}
})
响应式系统
当一个 Vue 实例被创建时,它将 data
对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。当这些数据改变时,视图会进行重渲染。值得注意的是只有当实例被创建时就已经存在于 data
中的 property 才是响应式的。
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data
选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty
把这些 property 全部转为 getter/setter。
在 setter 中设置监听,在 getter 中响应监听事件
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖(进行依赖收集)。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
Object.defineProperty
是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
检测变化的注意事项
由于 js 的限制,vue不能检测数组和对象的变化。
由于 Vue 不允许动态添加根级响应式 property,所以你必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值。
对于对象
Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化时对对象进行 getter / setter
的转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式 property。
还可以使用
vm.$set
实例方法,这也是全局Vue.set
方法的别名
var vm = new Vue({
data:{
a:1
}
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
// 添加响应式属性
Vue.set(vm.someObject, 'b', 2)
// 或
this.$set(this.someObject,'b',2)
有时需要为已有对象赋值多个新 property,比如使用 Object.assign()
或 _.extend()
。但是,这样添加到对象上的新 property 不会触发更新。在这种情况下,应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。
// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
对于数组
Vue 不能检测一下的数组变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
使用 Vue.set 的方式触发数组的响应式状态更新。
或者使用 push、pop、shift、unshift、splice、sort、reverse进行操作原数组,Vue 实现了这些方法的重写。
import { def } from '../util/index'
// 重写原型链上修改数组的方法 利用原型链继承的方式重新设置数组类型
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
// 修改数组的方法绑定了响应式 进行修改数组api操作就会通知数据修改
ob.dep.notify()
return result
})
})
异步更新队列
Vue 在更新 DOM 时是异步执行的。
只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
然后,在下一个的事件循环“tick”中(微任务),Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)
。
如何进行的依赖收集
时机:在初始化数据(props / data)的时候就进行依赖收集。
data初始化时做了是对定义 data
函数返回对象的遍历,通过 proxy
把每一个值 vm._data.xxx
都代理到 vm.xxx
上;同时调用 observe
方法观测整个 data
的变化,把 data
也变成响应式,可以通过 vm._data.xxx
访问到定义 data
返回函数中对应的属性。
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data) // 获取到data的属性
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) { // 遍历每个属性,进行判断是否和props、methods的属性名是否重合 如果重合则抛出错误
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) { // Object.prototype.hasOwnProperty.call(obj, key)
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) { // Check if a string starts with $ or _ 其实就是检查这个属性是不是自定义属性 和vm实例上的vue属性区分开
// 将data的值代理到vm实例上
proxy(vm, `_data`, key)
}
}
// observe data 响应式处理data
observe(data, true /* asRootData */)
}
自定义的 proxy 方法实现通过 Object.defineProperty
把 target[sourceKey][key]
的读写变成了对 target[key]
的读写。将 data 中的数据挂在到实例上。
observe
的功能就是用来监测数据的变化。observe
方法的作用就是给非 VNode 的对象类型数据添加一个 Observer
,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer
对象实例。
Observer
的构造函数,接下来会对 value
做判断,对于数组会调用 observeArray
方法,否则对纯对象调用 walk
方法。
defineReactive
函数最开始初始化 Dep
对象的实例,接着拿到 obj
的属性描述符,然后对子对象递归调用 observe
方法,这样就保证了无论 obj
的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj
中一个嵌套较深的属性,也能触发 getter 和 setter。最后利用 Object.defineProperty
去给 obj
的属性 key
添加 getter 和 setter。
观察者模式
生命周期钩子
每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
生命周期函数就是在初始化及数据更新过程各个阶段执行不同的钩子函数
生命周期钩子的 this
上下文指向调用它的 Vue 实例。
所以不能使用箭头函数来定义一个生命周期方法。箭头函数绑定了父级上下文,this 无法指向预期的组件实例。
在beforeCreate
时,data/prop
还没有注入到vm
实例中,所以是无法获取到data
数据的。
// vue2.6x源码部分 4999-5008
// expose real self
vm._self = vm
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件
initRender(vm) // 初始化渲染函数
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化状态 初始化顺序 props methods data computed watch
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
所以在created
时才能获取到data/prop
数据并进行交互。
子组件和父组件的生命周期调用
子组件的其他生命周期在patch
时传入,父组件先进行beforeMount
然后进行对子组件的递归处理。所以父子组件创建-挂载的生命周期执行顺序是:parent beforeCreate -> parent created -> parent beforeMount -> son beforeCreate -> son created -> son beforeMount -> son mounted -> parent mounted
父组件销毁时,父子组件销毁的生命周期执行顺序时:parent beforeDestroy -> son beforeDestroy -> son destroyed -> parent destroyed
所以:
- 在created钩子函数中可以访问到数据
- 在mounted钩子函数中可以访问到DOM
- 在destroyed钩子函数中可以做一些定时器销毁工作
除了常规的组件生命周期,还有
activated:被 keep-alive 缓存的组件激活时调用。该钩子在服务器渲染期间不被调用。
deactivated:被 keep-alive 缓存的组件失活时调用。该钩子在服务器渲染期间不被调用。
errorCaptured:在捕获一个来自后代组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回
false
以阻止该错误继续向上传播。你可以在此钩子中修改组件的状态。因此在捕获错误时,在模板或渲染函数中有一个条件判断来绕过其它内容就很重要;不然该组件可能会进入一个无限的渲染循环。
错误传播规则
- 默认情况下,如果全局的
config.errorHandler
被定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报。 - 如果一个组件的 inheritance chain (继承链)或 parent chain (父链)中存在多个
errorCaptured
钩子,则它们将会被相同的错误逐个唤起。 - 如果此
errorCaptured
钩子自身抛出了一个错误,则这个新错误和原本被捕获的错误都会发送给全局的config.errorHandler
。 - 一个
errorCaptured
钩子能够返回false
以阻止错误继续向上传播。本质上是说“这个错误已经被搞定了且应该被忽略”。它会阻止其它任何会被这个错误唤起的errorCaptured
钩子和全局的config.errorHandler
。
- 默认情况下,如果全局的
服务端的生命周期
SSR 服务器渲染的生命周期和web渲染的生命周期不同,部分生命周期在服务器渲染期间不被调用。只会调用 beforeCreated、created 生命周期函数。
总结
Vue.js 的生命周期函数就是在初始化及数据更新过程各个阶段执行不同的钩子函数。
在 created 钩子函数中可以访问到数据,在 mounted 钩子函数中可以访问到 DOM,在 destroyed 钩子函数中可以做一些定时器销毁工作。
虚拟DOM(Virtual DOM)
Virtual DOM
就是用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。在 Vue.js 中,Virtual DOM 是用 VNode
这么一个 Class 去描述。
其实 VNode 是对真实 DOM 的一种抽象描述,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。
组件化
所谓组件化,就是把页面拆分成多个组件 (component),每个组件依赖的 CSS、JavaScript、模板、图片等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。
就可以类似搭积木的方式去拼装页面。
实现业务和功能解耦。降低了页面的维护成本。
组件注册的两种方式:全局注册(Vue.component('app', App)
)和局部注册(components: {app: App}
)。
全局注册的方式会在Vue
原型链上进行扩展,一次引入任意地方使用,而局部注册的方式只是在父组件上添加,只有引入才能使用。全局注册的组件可以在任意地方使用,而局部注册的组件只能在当前组件内使用。
通常组件库中的基础组件会使用全局注册,而业务相关的组件会进行局部注册。
组件引入优化,组件库组件进行按需注册,全部注册也会造成资源的浪费。如使用Antd
组件库进行开发。
Diff算法
推荐阅读:15张图,20分钟吃透Diff算法核心原理,我说的!!!
patch组件的情况有:
- 首次渲染创建新节点;
- 仅旧节点存在新节点的不存在表示移除旧节点;
- 仅新节点存在旧节点不存在表示新增新节点;
- 挂载真实节点,新旧节点不一致则表示销毁旧节点挂载新节点,新旧节点一致进行内容比对。
主要讨论新旧节点一致(且新节点的父节点不为真实节点)的情况。
这里的一致表示在key值相等的前提下,tag(tagName)、同为或同不为注释节点、定义了data、同为或同不为input元素的条件都满足或同为异步组件的条件下,才判定为是相同的节点。(data中包含了id、class、style的属性值。)
function sameVnode (a, b) {
// key值相等是前提 然后再进行比对(tag、注释节点、data、都是Input节点) 或 (异步节点 异步包装函数相同 b节点的异步包装函数error未定义)
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
// 比较两个input节点是否相同 或都不是input节点
function sameInputType (a, b) {
if (a.tag !== 'input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB)
}
进行新旧节点比对(patchVnode):
- 如果旧节点等于新节点则直接退出比对。
- 如果是非异步组件,判断新节点的
prepatch
方法是否存在,存在则执行prepatch(oldVnode, vnode)
方法。 - 如果新节点定义了data且为可挂载节点(有真实节点实例),循环执行
update hook
。 - 判断是否为文本节点(文本节点已经为组件的最内层的节点了):
- 如果新节点不是文本节点,则进一步判断新旧节点是否都存在子节点。
- 如果都存在子节点且不相等就更新子节点(updateChildren)。
- 如果仅新节点存在子节点则进一步判断旧节点是否为文本节点是就置空,添加新节点。
- 如果仅旧节点存在子节点则删除旧节点的子节点。
- 如果都不存在子节点,且旧节点为文本节点,则置空旧节点的文本。
- 如果新旧节点都是文本节点且值不相等,则直接替换旧节点的文本节点。
- 如果新节点不是文本节点,则进一步判断新旧节点是否都存在子节点。
- 如果新节点的
postpatch
方法存在,则执行postpatch(oldVnode, vnode)
方法。
子节点比对更新(updateChildren),采用头尾指针的方式进行,先设置新旧子节点的头尾index值、node值,循环遍历新旧节点的子节点差异,直到旧节点的子节点(oldCh)头尾指针交叉或新节点的子节点(newCh)头尾指针交叉为止:
oldCh 头指针指向的node节点无效,oldCh继续向后查找
++oldStartIdx
。oldCh 尾指针指向的node节点无效,oldCh继续向前查找
--oldEndIdx
。oldCh 和 newCh 头指针指向的node节点一致,进行
patchVnode
递归,比对后出栈,oldCh 和 newCh 都继续向后查找。oldCh 和 newCh 尾指针指向的node节点一致,进行
patchVnode
递归,比对后出栈,oldCh 和 newCh 都继续向前查找。oldCh 头指针和 newCh 尾指针指向的node节点一致,进行
patchVnode
递归,比对后出栈,如果节点可移动(transition-group组件不可移动),则将比对更新后的 oldCh 头指针节点元素插入到当前oldCh尾指针的后面,oldCh 向后查找,newCh 向前查找。oldCh 尾指针和 newCh 头指针指向的node节点一致,进行
patchVnode
递归,比对后出栈,如果节点可移动,则将比对更新后的 oldCh 尾指针节点元素插入到当前 oldCh 头指针的前面,oldCh 向前查找,newCh 向后查找。如果oldCh 和 newCh 当前头尾指针指向的节点都没有关系,即完全没有一致的情况。先遍历获取 oldCh 的 key 值并以
key: index
键值对的方式存到对象oldKeyToIdx
中。如果当前newCh的头指针节点有key值,则在oldKeyToIdx
中查找,否则就在oldCh中进行遍历比对如果找到相等的node则返回所在oldCh中的索引,找不到则默认返回undefined。- 如果在oldCh中没有找到和newCh头指针一致的节点,就克隆newCh头指针的节点并插入到 oldCh 的头指针前。
- 如果在oldCh中找到了key值相同的节点:
- 如果两个节点一致,则进行
patchVnode
递归,比对后出栈,如果当前节点可以可移动,则将更新后的 oldCh 子节点元素插入到 oldCh 头指针前,并置空 oldCh 中index对应的节点,避免重复处理。 - 如果两个节点不一致,即key值相同,但其实不同的节点,那就作为新的节点对待,克隆newCh头指针的节点并插入到 oldCh 的头指针前。
- 如果两个节点一致,则进行
然后 newCh 向后查找。
循环遍历结束后。判断交叉情况:
- 如果 oldCh 的头尾指针交叉,则表示 newCh 中有部分节点没有访问到,确定 newCh 中没有访问到的部分边界,如果 newCh 尾指针后一个节点为未定义,则表明 newCh 头指针及以后的节点都没有被访问到,否则未访问范围为当前 newCh 头指针到尾指针。
- 如果 newCh 的头尾指针交叉,则表示 oldCh 中有部分节点没有访问到,则移除这部分节点。
使用双指针的方式进行比较,以及克隆节点的方式,能够有效的提升性能。
可以从diff算法流程中得出,如果随意定义组件的key值则会造成不必要的步骤。diff算法对重排子组件的性能有明显的提升,使用了旧的节点元素进行拼接。
编译
codegen
codegen 的目标是把 AST 树转换成代码字符串。
整个 codegen 过程就是深度遍历 AST 树根据不同条件生成不同代码的过程。
实践
指令
v-if 和 v-for 不能同时使用,v-for 的优先级要比 v-if 高,但是如果需要一起使用时,使用 template 组件进行v-if 判断后再进行 v-for 循环渲染。v-for 循环渲染避免每个元素进行 v-if 判断。
v-pre
:跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
v-cloak
:这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none }
一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。用于优化显示。
v-once
:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
事件触发
事件穿透解决方案:
参考链接:
https://zhuanlan.zhihu.com/p/86347430
vue框架内置指令v-on:click有300ms的延迟响应,这是为了判断区分单击和双击。vue为移动端提供了触摸方法touchstart、touchmove、touchend,但却没有提供tap指令,因此需要自己手动定义v-tap去消除300ms延迟,提升移动端用户体验。
Event事件
event 在编译阶段生成相关的data,对于 DOM 事件在 patch 过程中的创建阶段和更新阶段执行 updateDOMListeners 生成 DOM 事件;对于自定义事件,会在组件初始化阶段通过 initEvents 创建。
原生 DOM 事件和自定义事件主要的区别在于添加和删除事件的方式不一样,并且自定义事件的派发是往当前实例上(子组件实例)派发,但是可以利用在父组件环境定义回调函数来实现父子组件的通讯。
双向数据绑定 v-model
在<input>
、<select>
、<textarea>
表单元素上创建双向数据绑定,会根据控件类型自动选取正确的方法来更新元素。负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。
v-model 会忽略所有表单元素的 value、checked、selected属性的初始值,而总是将 Vue 实例的数据作为数据来源。
- text 和 textarea 元素使用 value 属性和 input 事件
- checkbox 和 radio 使用 checked 属性和 change 事件
- select 字段将 value 属性作为 prop 并将 change 作为事件
修饰符:
.lazy:在 change 事件之后进行同步。默认状态为在 input 事件出发后进行数据同步。
.number:自动将用户输入值转为数值类型。如果值无法被 parseFloat() 解析,则返回原始值。
.trim:自动过滤用户输入的首尾空白字符。
应用场景:
表单使用
- input 组件
- v-model 的功能与
<input :value="message" @input="message=$event.target.value">
大致相同。 - v-model 和 v-bind:value 的方式区别,v-model内部使用了 compositionstart 事件监听,在用户使用输入法进行拼写的时候,拼写完成才触发 input 事件,减少了事件触发频率。
- v-model 的功能与
- input 组件
-
v-model 在子组件上使用会默认生成 prop 中的 value 属性和 input 事件。将父组件的值传入子组件 prop 的 value 中,使用 input 的函数进行派发传值,父子通讯。
也可以在子组件中定义 model 对象,指定 父组件的 v-model 对应子组件中自定义的 prop 和methods
总结:
- v-model 的本质就是语法糖,但是运行时也做了一些优化。
- v-model 即可以支持原生表单元素,也可以支持组件。在组件的实现中,可以配置子组件接收的 prop 名称,以及派发的事件名称。
组件
一个组件的 data
选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。避免组件复用时数据相互影响。使用闭包的原理封装出独立的环境。
数据流向:单向流动。父组件流向子组件。子组件不能修改prop值。
防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。
注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。如果子组件复制prop的引用类型最好要进行深拷贝。
子组件中的prop设置校验规则后,会在创建子组件实例之前校验prop。
组件注册
全局组件注册要在初始化Vue实例之前进行。使用Vue.component(...)
方式进行创建。
异步组件
异步组件使用工厂函数进行异步导入组件,将应用分割成小一些的代码块。
会进行两次及以上的渲染,因为 Vue 通常是数据驱动视图重新渲染,但是在整个异步组件加载过程中是没有数据发生变化的,所以要渲染异步组件需要强制刷新。
webpack + code splitting
Vue.component('async-webpack-example', function (resolve) {
// 这个特殊的 `require` 语法将会告诉 webpack
// 自动将你的构建代码切割成多个包,这些包
// 会通过 Ajax 请求加载
require(['./my-async-component'], resolve)
})
import异步导入
// 全局注册
Vue.component(
'async-webpack-example',
// 这个动态导入会返回一个 `Promise` 对象。
() => import('./my-async-component')
)
// 局部注册
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component')
}
})
高级异步组件 – 组件加载状态
const LoadingComp = {
template: '<div>loading</div>'
}
const ErrorComp = {
template: '<div>error</div>'
}
const AsyncComponent = () => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: import('./MyComponent.vue'),
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
组件间传值
使用v-bind prop传值
传入单个值。可以传入静态值或动态值。
// 父组件
<template>
<div id="app">
<HelloWorld v-bind:authorObj="author" v-bind:author="{ name: 'Veronica', company: 'Veridian Dynamics' }" />
</div>
</template>
// 子组件
props: {
author: Object,
authorObj: Object
}
传入多个值。将所有欲传入子组件的值全部放到一个对象中。
// 父组件
<template>
<div id="app">
<HelloWorld v-bind="post" />
</div>
</template>
...
data() {
return {
post: {
msg1: 'xxxx',
author: { name: 'Veronica', company: 'Veridian Dynamics' }
}
}
}
...
// 子组件
props: ['msg','author'],
省略v-bind prop传值
单个传值。
// 父组件
<template>
<div id="app">
<HelloWorld msg="Welcome to Your Vue.js App" :msg1="msg1" :bol="true" :num="123" :arr="[1, 2, 3]" />
</div>
</template>
// 子组件
props: {
msg: String,
msg1: String,
bol: Boolean,
num: Number,
arr: Array
}
注:只有String
类型才能省略:
进行静态传值,其他类型的值会被识别为字符串类型。其他类型使用静态传值时必须使用:
响应式原理
使用 Object.defineProperty(),将依赖关系定义为 watcher 数组并设置到每个值的 getter 中,当一个数据发生改变会将会触发订阅了当前数据的数据发生改变。
nextTick
vue维护的是一个函数队列,先进先出的规则,先声明的函数会先执行,所以如果nextTick在数据更新前声明,则拿到的数据是未更新的数据。同时也要注意数据更新后同步拿到DOM中innerHTML是旧数据,只有进行触发了update才能拿到新数据,nextTick就可以使用微任务机制,在宏任务执行完成即更新数据后(update hook & activate hook)执行微任务(nextTick)获取DOM中的数据,此时数据已经是新数据了。
vue的数据更新是异步的。
nextTick理解其实就是回调函数,但是由于回调函数层层传递过于复杂,使用微任务机制可以更加清晰明了,不用在意宏任务里面做了什么操作,只需要宏任务执行完成后立马执行微任务。
回调函数本质也是在执行某些操作后立马执行回调函数,但是回调函数会影响到多层函数。
数据改变无法检测到的情况
对于引用类型如对象设置新属性(obj.a = 'a'
)和数组添加元素(arr[1] = 1
),无法触发数据的setter,所以要使用Vue.set 接口进行解决,操作数组数据的原生api如shift
等其实是底层进行了重写和封装,添加了数据改变的通知,对象的delete操作也是一样。
computed和watch
computed计算属性是基于它们的响应式依赖进行缓存的,只在相关响应式依赖发生改变时它们才会重新求值,如果新值和旧值相同也不会触发重新渲染。computed属性定义如果是函数则默认为getter函数,如果需要设置computed的值则将定义声明为对象,对象中的getter和setter函数分别设置到computed属性上。不推荐使用调用方法是因为当重新渲染时会重新执行一遍调用方法,而使用computed属性能够使用缓存值减少重新计算。
watch侦听属性是在一些数据变动时需要做额外的操作时使用,如当数据发生变化时请求接口。watch实现引用类型的深度监听,需要定义handler函数和deep属性。可以使用数组声明需要回调的一系列函数。使用sync属性能立马响应watch的回调而不用等到nextTick时才进行响应。使用immediate属性则会在监听开始之后被立即调用。
watch监听引用类型(数组和对象,即使定义deep属性也不行),拿不到oldVal,oldVal和newVal是一样的。但是如果对象监听到具体的某个属性,则可以获取到oldVal。
watch侦听属性除了使用数据的方式声明还可以使用实例挂载的方式vm.$watch
,实例方法能够做更加复杂的操作。Vue 实例将会在实例化时调用 $watch()
,遍历 watch 对象的每一个 property。
应用场景:computed计算属性适合在模板渲染中,某个值是依赖了其他的响应式对象甚至是计算属性计算而来;watch侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑。
$set使用
$set方法对象的使用:this.$set(this.obj, “key”, “value”),在对象中新增一个属性,将该属性手动进行响应式处理。
push()、pop()、shift()、unshift()、splice()、sort()、reverse()这些方法会改变被操作的数组,所以会触发数组更改的响应式。
插槽 v-slot
如果子组件的 template
中没有包含一个 <slot>
元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
普通插槽在父组件中定义子组件中 slot 的内容是在父组件的作用域中编译的,父组件的内容无法访问到子组件中的内容。
如果想要访问到子组件的作用域,则需要使用作用域插槽。
可以使用 #
进行缩写。
如果父组件没有定义 slot 内容,则子组件的 slot 如果定义了默认内容则会显示默认内容。
普通插槽
父组件
<template>
<div id="app">
<HelloWorld>
<template v-slot>slot</template>
</HelloWorld>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
},
}
</script>
子组件
<template>
<div class="hello">
<slot>默认内容</slot>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
}
</script>
在父组件编译和渲染阶段生成vnodes,所以数据的作用域是父组件实例,子组件渲染的时候直接拿到这些渲染好的vnodes。
具名插槽
父组件
<template>
<div id="app">
<HelloWorld>
<template v-slot:header>Header</template>
<template v-slot:footer>Footer</template>
</HelloWorld>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
},
}
</script>
子组件
<template>
<div class="hello">
<slot name="header"></slot>
<slot name="footer"></slot>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
}
</script>
作用域插槽
插槽需要访问子组件中才有的数据。子组件中的 slot 上绑定父组件需要使用的值,即插槽prop。在父组件就可以拿到插槽 prop 合并成的对象,父组件将这个对象自定义命名,然后就可以在插槽中使用子组件中的值了。
父组件
<template>
<div id="app">
<HelloWorld>
<template #header="slotProps">
app: {{ slotProps.testObj.test }}\{{ slotProps.test }}
</template>
</HelloWorld>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
},
}
</script>
子组件
<template>
<div class="hello">
<slot name="header" :testObj="testObj" :test="test">
HelloWorld: {{ test }}
</slot>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
test: 'xxx',
testObj: {
test: 'ss'
},
}
},
}
</script>
父组件在编译和渲染阶段并不会直接生成 vnodes ,而是在父节点 vnode 的 data 中保留一个scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,只有在渲染子组件阶段才会执行这个渲染函数生成 vnodes ,所以父组件中的 slot 可以访问到子组件的插槽prop。
keep-alive
抽象组件,保存组件状态,防止重复渲染导致性能问题。<keep-alive>
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
keep-alive 要求被切换到的组件都要有自己的名字,不论时通过组件的name选项还是局部/全局注册。
不会在函数式组件中正常工作,因为函数式组件没有缓存实例。
抽象组件,自身不会渲染一个DOM元素,也不会出现在组件的父组件链中。
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component v-bind:is="currentTabComponent"></component>
</keep-alive>
当组件在 keep-alive
内被切换,会执行 activated 和 deactivated 生命周期钩子会执行。
这两个生命周期钩子是 keep-alive 特有的。
组件初始化时,mounted 生命周期钩子执行完后,才执行activated 生命周期钩子。但如果组件已经进行缓存了,则组件再次被激活时不会触发mounted生命周期钩子函数,只会触发activated 生命周期钩子函数。
include
- 字符串或正则表达式。只有名称匹配的组件会被缓存。匹配首先检查组件自身的
name
选项,如果name
选项不可用,则匹配它的局部注册名称 (父组件components
选项的键值)。匿名组件不能被匹配。exclude
- 字符串或正则表达式。任何名称匹配的组件都不会被缓存。max
- 数字。最多可以缓存多少组件实例。
<!-- 逗号分隔字符串 -->
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
<!-- 使用max -->
<keep-alive :max="10">
<component :is="view"></component>
</keep-alive>
在首次渲染时会直接执行 activated 钩子函数,在再次激活组件时会在 nextTick 后先执行 activated 钩子函数然后执行 updated 钩子函数。失活组件的时候会执行 deactivated 钩子函数。
总结
keep-alive 组件时一个内置抽象组件,它的实现通过自定义 render 函数并且利用了插槽。
keep-alive 组件的渲染分为首次渲染和缓存渲染。首次渲染时会将实例缓存起来,当命中缓存,则不会再执行 created 和 mounted 钩子函数,而会执行 activated 钩子函数。销毁时也不会执行 destroyed 钩子函数,而会执行 deactivated 钩子函数。
keep-alive 的 max 属性采用的最近最少使用算法(LRU)进行处理,避免缓存内容过多占用内容。
过渡动画
transition
内置抽象组件。
当插入或删除包含在 transition
组件中的元素时,Vue 将会做以下处理:
- 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。
- 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。
- 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。(注意:此指浏览器逐帧动画机制,和 Vue 的
nextTick
概念不同)
过渡的css类名:
v-enter
:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。v-enter-active
:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。v-enter-to
:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时v-enter
被移除),在过渡/动画完成之后移除。v-leave
:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。v-leave-active
:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。v-leave-to
:定义离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时v-leave
被删除),在过渡/动画完成之后移除。
以上的过渡类名为默认类名,可以自定义声明 name ,会将自定义name 替换掉 v-,如 name 为 fade 则类名为 fade-enter。
v-enter-active
和v-leave-active
可以控制进入/离开过渡的不同的缓和曲线。
transition-group
会渲染成真实的元素。默认为一个
<span>
。你也可以通过tag
attribute 更换为其他元素。过渡模式不可用,因为我们不再相互切换特有的元素。
内部元素总是需要提供唯一的
key
attribute 值。CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身。
transition-group组件属性是在transition组件上进行扩展的,所以transition组件的属性transition-group组件也可以使用。
总结
transition-group
组件时为了做列表的过渡,它会渲染成真实的元素。
当我们去修改列表的数据的时候,如果是添加或者删除数据,则会触发相应元素本身的过渡动画,这点和 <transition>
组件实现效果一样,除此之外 <transition-group>
还实现了 move 的过渡效果,让我们的列表过渡动画更加丰富。
问题
v-if 和 v-show 的区别
v-show 会在页面首次渲染的时候进行渲染加载,修改条件后不会重新渲染。
v-if 不会在页面初始化的时候渲染,但是条件为真值时会进行渲染加载,条件为假值时会进行销毁。
组件显示条件修改不是很频繁则使用 v-if,反之使用 v-show。
为何 v-for 中要用key
key:key
的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。
更新key值可以完整地触发组件的生命周期钩子,触发过渡。
描述Vue组件生命周期(有父子组件的情况)
更详细的参考:https://juejin.cn/post/6904536680183791623
父子组件通讯
子组件获取父组件数据:
- 父组件 props 传值
- 子组件访问父组件实例:this.$parent
- 子组件访问根组件实例:this.$root
- 分发监听 this.$root.$emit(‘eventName’, data)
- inject - 在任何后代组件里,我们都可以使用
inject
选项来接收指定的我们想要添加在这个实例上的 property - vuex
父组件获取子组件传值:
- 子组件 this.$emit(‘eventName’, data)
- 建立监听 this.$root.$on(‘eventName’, callback)
- provide - 选项允许我们指定我们想要提供给后代组件的数据/方法
- this.$refs.elm
- vuex
- v-model
兄弟组件通讯
- 父组件搭桥,兄弟组件通过 $emit 方式将值传递给父组件,父组件再传值给兄弟组件
- vuex
- eventBus
描述组件渲染和更新的过程
参考链接:https://blog.csdn.net/qq_42072086/article/details/108006061
渲染组件时,会通过 Vue.extend 方法构建子组件的构造函数,初始化组件的时候会进行实例化。终手动调用 $mount() 进行组件挂载渲染。更新组件时会进行 patchVnode 流程。核心就是diff算法。
双向数据绑定v-model的实现原理
为什么Vue组件中的data要使用function的方式
参考官网:data
必须是一个函数
一个组件的 data
选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。
对于组件复用而言,每个组件的data要相互独立,互不影响。如果data是一个对象,多个组件引用的则是同一个内存地址上的对象数据。使用函数返回一个对象,则形成了一个闭包,创建了独立的对象引用地址。
为什么要使用计算属性
避免在模板中放入太多的逻辑,导致模板过重且难以维护。
计算属性是基于它们的响应式依赖进行缓存的,只在相关响应式依赖发生改变时它们才会重新求值。
watch监听属性
watch监听属性不可以使用箭头函数进行定义,因为箭头函数中的this指向的是定义时的this,而不是执行时的this,所以不会指向Vue实例的上下文,而是 undefined
。
watch定义的函数中 this 指向为 当前组件实例 vm。
组件的选项和混入的选项是怎么合并的
- 数据对象(data选项),在内部进行递归合并,并在发生冲突时以组件数据优先;
- 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用;
- watch对象合并时,相同的key合成一个对象,且混入监听在组件监听之前调用;
- 值为对象的选项,如filters选项、computed选项、methods选项、components选项、directives选项,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
强制刷新组件的方法
- vm.$forceUpdate()。
- 组件上定义key值,修改key值。
销毁组件
- 没有 keep-alive 时的路由切换
- v-if=”false”
- 执行vm.$destroy()
其他
vue2.x
版本使用 Flow.js
进行数据类型检测。
router
注意点:不同的页面使用的是同一个component,默认情况下当这两个页面切换时并不会触发vue的created或者mounted钩子,可以通过watch $route的变化来做处理,另一种方法在 router-view 上加上一个唯一的key,来保证路由切换时都会重新渲染触发钩子了。
<router-view :key="key"></router-view>
computed: {
key() {
return this.$route.name !== undefined? this.$route.name + +new Date(): this.$route + +new Date()
}
}
vue实例方法
vm.$on(event, callback)
、vm.$once(event, callback)
、vm.$off([event, callback])
vm.$emit(eventName, [...args])
四个方法,vue自带了订阅-发布模式。
自检
面试题
(以上面试题,可看题目,部分解答有问题,自行判断)
推荐:vue面试题 - https://juejin.cn/post/6947847527253311496
更多阅读
vue源码分析 - https://juejin.cn/post/6886266739378618382#heading-9