Vue3 有了解过吗?能说说跟 vue2 的区别吗?
1. 哪些变化
从上图中,我们可以概览Vue3
的新特性,如下:
1.1 速度更快
vue3
相比vue2
重写了虚拟Dom
实现
编译模板的优化
更高效的组件初始化
undate
性能提高 1.3~2 倍
SSR
速度提高了 2~3 倍
1.2 体积更小
通过webpack
的tree-shaking
功能,可以将无用模块“剪辑”,仅打包需要的
能够tree-shaking
,有两大好处:
vue
可以开发出更多其他的功能,而不必担忧vue
打包出来的整体体积过多
1.3 更易维护
compositon Api
可与现有的Options API
一起使用
灵活的逻辑组合与复用
Vue3
模块可以和其他框架搭配使用
更好的 Typescript 支持
VUE3
是基于typescipt
编写的,可以享受到自动的类型定义提示
1.4 编译器重写
1.5 更接近原生
可以自定义渲染 API
1.6 更易使用
响应式 Api
暴露出来
轻松识别组件重新渲染原因
2. Vue3 新增特性
Vue 3 中需要关注的一些新功能包括:
framents
Teleport
composition Api
createRenderer
2.1 framents
在 Vue3.x
中,组件现在支持有多个根节点
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
复制代码
2.2 Teleport
Teleport
是一种能够将我们的模板移动到 DOM
中 Vue app
之外的其他位置的技术,就有点像哆啦 A 梦的“任意门”
在vue2
中,像 modals
,toast
等这样的元素,如果我们嵌套在 Vue
的某个组件内部,那么处理嵌套组件的定位、z-index
和样式就会变得很困难
通过Teleport
,我们可以在组件的逻辑位置写模板代码,然后在 Vue
应用范围之外渲染它
<button @click="showToast" class="btn">打开 toast</button>
<!-- to 属性就是目标位置 -->
<teleport to="#teleport-target">
<div v-if="visible" class="toast-wrap">
<div class="toast-msg">我是一个 Toast 文案</div>
</div>
</teleport>
复制代码
2.3 createRenderer
通过createRenderer
,我们能够构建自定义渲染器,我们能够将 vue
的开发模型扩展到其他平台
我们可以将其生成在canvas
画布上
关于createRenderer
,我们了解下基本使用,就不展开讲述了
import { createRenderer } from '@vue/runtime-core'
const { render, createApp } = createRenderer({
patchProp,
insert,
remove,
createElement,
// ...
})
export { render, createApp }
export * from '@vue/runtime-core'
复制代码
2.4 composition Api
composition Api,也就是组合式api
,通过这种形式,我们能够更加容易维护我们的代码,将相同功能的变量进行一个集中式的管理
关于compositon api
的使用,这里以下图展开
简单使用:
export default {
setup() {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => console.log('component mounted!'))
return {
count,
double,
increment
}
}
}
复制代码
3. 非兼容变更
3.1 Global API
3.2 模板指令
3.3 组件
3.4 渲染函数
渲染函数API
改变
$scopedSlots
property 已删除,所有插槽都通过 $slots
作为函数暴露
自定义指令 API 已更改为与组件生命周期一致
一些转换class
被重命名了:
v-enter
-> v-enter-from
v-leave
-> v-leave-from
组件 watch
选项和实例方法 $watch
不再支持点分隔字符串路径,请改用计算函数作为参数
在 Vue 2.x
中,应用根容器的 outerHTML
将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x
现在使用应用程序容器的 innerHTML
。
3.5 其他小改变
destroyed
生命周期选项被重命名为 unmounted
beforeDestroy
生命周期选项被重命名为 beforeUnmount
[prop default
工厂函数不再有权访问 this
是上下文
自定义指令 API 已更改为与组件生命周期一致
data
应始终声明为函数
来自 mixin
的 data
选项现在可简单地合并
attribute
强制策略已更改
一些过渡 class
被重命名
组建 watch 选项和实例方法 $watch
不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。
<template>
没有特殊指令的标记 (v-if/else-if/else
、v-for
或 v-slot
) 现在被视为普通元素,并将生成原生的 <template>
元素,而不是渲染其内部内容。
在Vue 2.x
中,应用根容器的 outerHTML
将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x
现在使用应用容器的 innerHTML
,这意味着容器本身不再被视为模板的一部分。
3.6 移除 API
vue-router 守卫
导航守卫 router.beforeEach
全局前置守卫
to: Route
: 即将要进入的目标(路由对象)
from: Route
: 当前导航正要离开的路由
next: Function
: 一定要调用该方法来 resolve
这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截)
执行效果依赖 next 方法的调用参数。
next()
: 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
next(false)
:取消进入路由,url 地址重置为 from 路由地址(也就是将要离开的路由地址)
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next) => {
next();
});
router.beforeResolve((to, from, next) => {
next();
});
router.afterEach((to, from) => {
console.log('afterEach 全局后置钩子');
});
复制代码
路由独享的守卫 你可以在路由配置上直接定义 beforeEnter
守卫
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
复制代码
组件内的守卫你可以在路由组件内直接定义以下路由导航守卫
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用,我们用它来禁止用户离开
// 可以访问组件实例 `this`
// 比如还未保存草稿,或者在用户离开前,
将setInterval销毁,防止离开之后,定时器还在调用。
}
}
复制代码
defineProperty 和 proxy 的区别
Vue 在实例初始化时遍历 data 中的所有属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。这样当追踪数据发生变化时,setter 会被自动调用。
Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
但是这样做有以下问题:
添加或删除对象的属性时,Vue 检测不到。因为添加或删除的对象没有在初始化进行响应式处理,只能通过$set
来调用Object.defineProperty()
处理。
无法监控到数组下标和长度的变化。
Vue3 使用 Proxy 来监控数据的变化。Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。相对于Object.defineProperty()
,其有以下特点:
Proxy 直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。
Proxy 可以监听数组的变化。
ref 和 reactive 异同
这是Vue3
数据响应式中非常重要的两个概念,跟我们写代码关系也很大
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
const obj = reactive({ count: 0 })
obj.count++
复制代码
ref
接收内部值(inner value
)返回响应式Ref
对象,reactive
返回响应式代理对象
从定义上看ref
通常用于处理单值的响应式,reactive
用于处理对象类型的数据响应式
两者均是用于构造响应式数据,但是ref
主要解决原始值的响应式问题
ref
返回的响应式数据在 JS 中使用需要加上.value
才能访问其值,在视图中使用会自动脱ref
,不需要.value
;ref
可以接收对象或数组等非原始值,但内部依然是reactive
实现响应式;reactive
内部如果接收Re
f 对象会自动脱ref
;使用展开运算符(...
)展开reactive
返回的响应式对象会使其失去响应性,可以结合toRefs()
将值转换为Ref
对象之后再展开。
reactive
内部使用Proxy
代理传入对象并拦截该对象各种操作,从而实现响应式。ref
内部封装一个RefImpl
类,并设置get value/set value
,拦截用户对值的访问,从而实现响应式
参考:前端vue面试题详细解答
异步组件是什么?使用场景有哪些?
分析
因为异步路由的存在,我们使用异步组件的次数比较少,因此还是有必要两者的不同。
体验
大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们
import { defineAsyncComponent } from 'vue'
// defineAsyncComponent定义异步组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容
const AsyncComp = defineAsyncComponent(() => {
// 加载函数返回Promise
return new Promise((resolve, reject) => {
// ...可以从服务器加载组件
resolve(/* loaded component */)
})
})
// 借助打包工具实现ES模块动态导入
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
复制代码
回答范例
在大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。
我们不仅可以在路由切换时懒加载组件,还可以在页面组件中继续使用异步组件,从而实现更细的分割粒度。
使用异步组件最简单的方式是直接给defineAsyncComponent
指定一个loader
函数,结合 ES 模块动态导入函数import
可以快速实现。我们甚至可以指定loadingComponent
和errorComponent
选项从而给用户一个很好的加载反馈。另外Vue3
中还可以结合Suspense
组件使用异步组件。
异步组件容易和路由懒加载混淆,实际上不是一个东西。异步组件不能被用于定义懒加载路由上,处理它的是vue
框架,处理路由组件加载的是vue-router
。但是可以在懒加载的路由组件中使用异步组件
computed 和 watch 有什么区别?
computed:
computed
是计算属性,也就是计算值,它更多用于计算值的场景
computed
具有缓存性,computed 的值在 getter 执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取 computed 的值时才会重新调用对应的 getter 来计算
computed
适用于计算比较消耗性能的计算场景
watch:
更多的是「观察」的作用,类似于某些数据的监听回调,用于观察props
$emit
或者本组件的值,当数据变化时来执行回调进行后续操作
无缓存性,页面重新渲染时值不变化也会执行
小结:
当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为 computed
如果你需要在某个数据变化时做一些事情,使用 watch 来观察这个数据变化
$route
和$router
的区别
vue2.x 详细
1. 分析
首先找到vue
的构造函数
源码位置:src\core\instance\index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
复制代码
options
是用户传递过来的配置项,如data、methods
等常用的方法
vue
构建函数调用_init
方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法
initMixin(Vue); // 定义 _init
stateMixin(Vue); // 定义 $set $get $delete $watch 等
eventsMixin(Vue); // 定义事件 $on $once $off $emit
lifecycleMixin(Vue);// 定义 _update $forceUpdate $destroy
renderMixin(Vue); // 定义 _render 返回虚拟dom
复制代码
首先可以看initMixin
方法,发现该方法在Vue
原型上定义了_init
方法
源码位置:src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
// 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else { // 合并vue属性
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 初始化proxy拦截器
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化组件生命周期标志位
initLifecycle(vm)
// 初始化组件事件侦听
initEvents(vm)
// 初始化渲染方法
initRender(vm)
callHook(vm, 'beforeCreate')
// 初始化依赖注入内容,在初始化data、props之前
initInjections(vm) // resolve injections before data/props
// 初始化props/data/method/watch/methods
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 挂载元素
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
复制代码
仔细阅读上面的代码,我们得到以下结论:
initState
方法是完成props/data/method/watch/methods
的初始化
源码位置:src\core\instance\state.js
export function initState (vm: Component) {
// 初始化组件的watcher列表
vm._watchers = []
const opts = vm.$options
// 初始化props
if (opts.props) initProps(vm, opts.props)
// 初始化methods方法
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// 初始化data
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
复制代码
我们和这里主要看初始化data
的方法为initData
,它与initState
在同一文件上
function initData (vm: Component) {
let data = vm.$options.data
// 获取到组件上的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)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
// 属性名不能与方法名重复
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// 属性名不能与state名称重复
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)) { // 验证key值的合法性
// 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据
proxy(vm, `_data`, key)
}
}
// observe data
// 响应式监听data是数据的变化
observe(data, true /* asRootData */)
}
复制代码
仔细阅读上面的代码,我们可以得到以下结论:
关于数据响应式在这就不展开详细说明
上文提到挂载方法是调用vm.$mount
方法
源码位置:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 获取或查询元素
el = el && query(el)
/* istanbul ignore if */
// vue 不允许直接挂载到body或页面文档上
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
// 存在template模板,解析vue模板文件
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 通过选择器获取元素内容
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
/**
* 1.将temmplate解析ast tree
* 2.将ast tree转换成render语法字符串
* 3.生成render方法
*/
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
复制代码
阅读上面代码,我们能得到以下结论:
对template
的解析步骤大致分为以下几步:
将html
文档片段解析成ast
描述符
将ast
描述符解析成字符串
生成render
函数
生成render
函数,挂载到vm
上后,会再次调用mount
方法
源码位置:src\platforms\web\runtime\index.js
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
// 渲染组件
return mountComponent(this, el, hydrating)
}
复制代码
调用mountComponent
渲染组件
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 如果没有获取解析的render函数,则会抛出警告
// render是解析模板文件生成的
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
// 没有获取到vue的模板文件
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
// 执行beforeMount钩子
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
// 定义更新函数
updateComponent = () => {
// 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 监听当前组件状态,当有数据变化时,更新组件
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 数据更新引发的组件更新
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
复制代码
阅读上面代码,我们得到以下结论:
updateComponent
方法主要执行在vue
初始化时声明的render
,update
方法
render的作用主要是生成
vnode
源码位置:src\core\instance\render.js
// 定义vue 原型上的render方法
Vue.prototype._render = function (): VNode {
const vm: Component = this
// render函数来自于组件的option
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
// 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
复制代码
_update
主要功能是调用patch
,将vnode
转换为真实DOM
,并且更新到页面中
源码位置:src\core\instance\lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
// 设置当前激活的作用域
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
// 执行具体的挂载逻辑
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}
复制代码
2. 结论
new Vue
的时候调用会调用_init
方法
定义 $set
、$get
、$delete
、$watch
等方法
定义 $on
、$off
、$emit
、$off
等事件
定义 _update
、$forceUpdate
、$destroy
生命周期
调用$mount
进行页面的挂载
挂载的时候主要是通过mountComponent
方法
定义updateComponent
更新函数
执行render
生成虚拟DOM
_update
将虚拟DOM
生成真实DOM
结构,并且渲染到页面中
说说 vue 内置指令
你觉得 vuex 有什么缺点
分析
相较于redux
,vuex
已经相当简便好用了。但模块的使用比较繁琐,对ts
支持也不好。
体验
使用模块:用起来比较繁琐,使用模式也不统一,基本上得不到类型系统的任何支持
const store = createStore({
modules: {
a: moduleA
}
})
store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState使用
store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的
store.getters['a/c'] // -> 有namespaced时要加path,使用模式又和state不一样
store.commit('d') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutation
store.commit('a/d') // -> 有namespaced时要加path,配合mapMutations使用感觉也没简化
复制代码
回答范例
vuex
利用响应式,使用起来已经相当方便快捷了。但是在使用过程中感觉模块化这一块做的过于复杂,用的时候容易出错,还要经常查看文档
比如:访问state
时要带上模块key
,内嵌模块的话会很长,不得不配合mapState
使用,加不加namespaced
区别也很大,getters
,mutations
,actions
这些默认是全局,加上之后必须用字符串类型的 path 来匹配,使用模式不统一,容易出错;对 ts 的支持也不友好,在使用模块时没有代码提示。
之前Vue2
项目中用过vuex-module-decorators
的解决方案,虽然类型支持上有所改善,但又要学一套新东西,增加了学习成本。pinia
出现之后使用体验好了很多,Vue3 + pinia
会是更好的组合
原理
下面我们来看看vuex
中store.state.x.y
这种嵌套的路径是怎么搞出来的
首先是子模块安装过程:父模块状态parentState
上面设置了子模块名称moduleName
,值为当前模块state
对象。放在上面的例子中相当于:store.state['x'] = moduleX.state
。此过程是递归的,那么store.state.x.y
安装时就是:store.state['x']['y'] = moduleY.state
//源码位置 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115
if (!isRoot && !hot) {
// 获取父模块state
const parentState = getNestedState(rootState, path.slice(0, -1))
// 获取子模块名称
const moduleName = path[path.length - 1]
store._withCommit(() => {
// 把子模块state设置到父模块上
parentState[moduleName] = module.state
})
}
复制代码
v-show 与 v-if 有什么区别?
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
Vue computed 实现
实现时,主要如下
初始化 data
, 使用 Object.defineProperty
把这些属性全部转为 getter/setter
。
初始化 computed
, 遍历 computed
里的每个属性,每个 computed
属性都是一个 watch
实例。每个属性提供的函数作为属性的 getter
,使用 Object.defineProperty
转化。
Object.defineProperty getter
依赖收集。用于依赖发生变化时,触发属性重新计算。
若出现当前 computed
计算属性嵌套其他 computed
计算属性时,先进行其他的依赖收集
Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?
1)Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题
Vue 使用了 Object.defineProperty 实现双向数据绑定
在初始化实例时对属性执行 getter/setter 转化
属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的(这也就造成了 Vue 无法检测到对象属性的添加或删除)
所以 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
2)接下来我们看看框架本身是如何实现的呢?
Vue 源码位置:vue/src/core/instance/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val
return val
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
复制代码
我们阅读以上源码可知,vm.$set 的实现原理是:
如果目标是数组,直接使用数组的 splice 方法触发相应式;
如果目标是对象,会先判读属性是否存在、对象是否是响应式,
最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理
defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法
Vue 的 diff 算法详细分析
1. 是什么
diff
算法是一种通过同层的树节点进行比较的高效算法
其有两个特点:
比较只会在同层级进行, 不会跨层级比较
在 diff 比较的过程中,循环从两边向中间比较
diff
算法在很多场景下都有应用,在 vue
中,作用于虚拟 dom
渲染成真实 dom
的新旧 VNode
节点比较
2. 比较方式
diff
整体策略为:深度优先,同层比较
比较只会在同层级进行, 不会跨层级比较
比较的过程中,循环从两边向中间收拢
下面举个vue
通过diff
算法更新的例子:
新旧VNode
节点如下图所示:
第一次循环后,发现旧节点 D 与新节点 D 相同,直接复用旧节点 D 作为diff
后的第一个真实节点,同时旧节点endIndex
移动到 C,新节点的 startIndex
移动到了 C
第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff
后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 endIndex
移动到了 B,新节点的 startIndex
移动到了 E
第三次循环中,发现 E 没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex
移动到了 A。旧节点的 startIndex
和 endIndex
都保持不动
第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff
后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex
移动到了 B,新节点的startIndex
移动到了 B
第五次循环中,情形同第四次循环一样,因此 diff
后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex
移动到了 C,新节点的 startIndex 移动到了 F
新节点的 startIndex
已经大于 endIndex
了,需要创建 newStartIdx
和 newEndIdx
之间的所有节点,也就是节点 F,直接创建 F 节点对应的真实节点放到 B 节点后面
3. 原理分析
当数据发生改变时,set
方法会调用Dep.notify
通知所有订阅者Watcher
,订阅者就会调用patch
给真实的DOM
打补丁,更新相应的视图
源码位置:src/core/vdom/patch.js
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) { // 没有新节点,直接执行destory钩子函数
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 判断旧节点和新节点自身一样,一致执行patchVnode
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 否则直接销毁及旧节点,根据新节点生成dom元素
if (isRealElement) {
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
}
oldVnode = emptyNodeAt(oldVnode)
}
return vnode.elm
}
}
}
复制代码
patch
函数前两个参数位为oldVnode
和 Vnode
,分别代表新的节点和之前的旧节点,主要做了四个判断:
没有新节点,直接触发旧节点的destory
钩子
没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用 createElm
旧节点和新节点自身一样,通过 sameVnode
判断节点是否一样,一样时,直接调用 patchVnode
去处理这两个节点
旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点
下面主要讲的是patchVnode
部分
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果新旧节点一致,什么都不做
if (oldVnode === vnode) {
return
}
// 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
const elm = vnode.elm = oldVnode.elm
// 异步占位符
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 如果新旧都是静态节点,并且具有相同的key
// 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
// 也不用再有其他操作
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 如果vnode不是文本节点或者注释节点
if (isUndef(vnode.text)) {
// 并且都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 并且子节点不完全一致,则调用updateChildren
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
// 如果只有新的vnode有子节点
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// elm已经引用了老的dom节点,在老的dom节点上添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 如果老节点是文本节点
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
// 如果新vnode和老vnode是文本节点或注释节点
// 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
复制代码
patchVnode
主要做了几个判断:
新节点是否是文本节点,如果是,则直接更新dom
的文本内容为新节点的文本内容
新节点和旧节点如果都有子节点,则处理比较更新子节点
只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新DOM
,并且添加进父节点
只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把DOM
删除
子节点不完全一致,则调用updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 旧头索引
let newStartIdx = 0 // 新头索引
let oldEndIdx = oldCh.length - 1 // 旧尾索引
let newEndIdx = newCh.length - 1 // 新尾索引
let oldStartVnode = oldCh[0] // oldVnode的第一个child
let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
let newStartVnode = newCh[0] // newVnode的第一个child
let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
// 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果oldVnode的第一个child不存在
if (isUndef(oldStartVnode)) {
// oldStart索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// 如果oldVnode的最后一个child不存在
} else if (isUndef(oldEndVnode)) {
// oldEnd索引左移
oldEndVnode = oldCh[--oldEndIdx]
// oldStartVnode和newStartVnode是同一个节点
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// patch oldStartVnode和newStartVnode, 索引左移,继续循环
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode和newEndVnode是同一个节点
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// patch oldEndVnode和newEndVnode,索引右移,继续循环
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode和newEndVnode是同一个节点
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// patch oldStartVnode和newEndVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// oldStart索引右移,newEnd索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果oldEndVnode和newStartVnode是同一个节点
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// patch oldEndVnode和newStartVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// oldEnd索引左移,newStart索引右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 如果都不匹配
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果未找到,说明newStartVnode是一个新的节点
if (isUndef(idxInOld)) { // New element
// 创建一个新Vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
// 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
// 比较两个具有相同的key的新节点是否是同一个节点
//不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
if (sameVnode(vnodeToMove, newStartVnode)) {
// patch vnodeToMove和newStartVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 清除
oldCh[idxInOld] = undefined
// 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
// 移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 如果key相同,但是节点不相同,则创建一个新的节点
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
// 右移
newStartVnode = newCh[++newStartIdx]
}
}
复制代码
while
循环主要处理了以下五种情景:
当新老 VNode
节点的 start
相同时,直接 patchVnode
,同时新老 VNode
节点的开始索引都加 1
当新老 VNode
节点的 end
相同时,同样直接 patchVnode
,同时新老 VNode
节点的结束索引都减 1
当老 VNode
节点的 start
和新 VNode
节点的 end
相同时,这时候在 patchVnode
后,还需要将当前真实 dom
节点移动到 oldEndVnode
的后面,同时老 VNode
节点开始索引加 1,新 VNode
节点的结束索引减 1
当老 VNode
节点的 end
和新 VNode
节点的 start
相同时,这时候在 patchVnode
后,还需要将当前真实 dom
节点移动到 oldStartVnode
的前面,同时老 VNode
节点结束索引减 1,新 VNode
节点的开始索引加 1
如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:
从旧的 VNode
为 key
值,对应 index
序列为 value
值的哈希表中找到与 newStartVnode
一致 key
的旧的 VNode
节点,再进行patchVnode
,同时将这个真实 dom
移动到 oldStartVnode
对应的真实 dom
的前面
调用 createElm
创建一个新的 dom
节点放到当前 newStartIdx
的位置
小结
当数据发生改变时,订阅者watcher
就会调用patch
给真实的DOM
打补丁
通过isSameVnode
进行判断,相同则调用patchVnode
方法
patchVnode
做了以下操作:
找到对应的真实dom
,称为el
如果都有都有文本节点且不相等,将el
文本节点设置为Vnode
的文本节点
如果oldVnode
有子节点而VNode
没有,则删除el
子节点
如果oldVnode
没有子节点而VNode
有,则将VNode
的子节点真实化后添加到el
如果两者都有子节点,则执行updateChildren
函数比较子节点
updateChildren
主要做了以下操作:
设置新旧VNode
的头尾指针
新旧头尾指针进行比较,循环向中间靠拢,根据情况调用patchVnode
进行patch
重复流程、调用createElem
创建一个新节点,从哈希表寻找 key
一致的VNode
节点再分情况操作
vuex 是什么?怎么使用?哪种功能场景使用它?
Vuex
是一个专为 Vue.js
应用程序开发的状态管理模式。vuex
就是一个仓库,仓库里放了很多对象。其中 state
就是数据源存放地,对应于一般 vue 对象里面的 data
里面存放的数据是响应式的,vue
组件从 store
读取数据,若是 store
中的数据发生改变,依赖这相数据的组件也会发生更新它通过 mapState
把全局的 state
和 getters
映射到当前组件的 computed
计算属性
vuex
一般用于中大型 web
单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用 vuex
的必要性不是很大,因为完全可以用组件 prop
属性或者事件来完成父子组件之间的通信,vuex
更多地用于解决跨组件通信以及作为数据中心集中式存储数据。
使用Vuex
解决非父子组件之间通信问题 vuex
是通过将 state
作为数据中心、各个组件共享 state
实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于 State
中能有效解决多层级组件嵌套的跨组件通信问题
vuex
的 State
在单页应用的开发中本身具有一个“数据库”的作用,可以将组件中用到的数据存储在 State
中,并在 Action
中封装数据读写的逻辑。这时候存在一个问题,一般什么样的数据会放在 State
中呢? 目前主要有两种数据会使用 vuex
进行管理:
包括以下几个模块
state
:Vuex
使用单一状态树,即每个应用将仅仅包含一个store
实例。里面存放的数据是响应式的,vue
组件从 store
读取数据,若是 store
中的数据发生改变,依赖这相数据的组件也会发生更新。它通过 mapState
把全局的 state
和 getters
映射到当前组件的 computed
计算属性
mutations
:更改Vuex
的store
中的状态的唯一方法是提交mutation
getters
:getter
可以对 state
进行计算操作,它就是 store
的计算属性虽然在组件内也可以做计算属性,但是 getters
可以在多给件之间复用如果一个状态只在一个组件内使用,是可以不用 getters
action
:action
类似于 muation
, 不同在于:action
提交的是 mutation
,而不是直接变更状态action
可以包含任意异步操作
modules
:面对复杂的应用程序,当管理的状态比较多时;我们需要将vuex
的store
对象分割成模块(modules
)
modules
:项目特别复杂的时候,可以让每一个模块拥有自己的state
、mutation
、action
、getters
,使得结构非常清晰,方便管理
回答范例
思路
给定义
必要性阐述
何时使用
拓展:一些个人思考、实践经验等
回答范例
Vuex
是一个专为 Vue.js
应用开发的 状态管理模式 + 库 。它采用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
我们期待以一种简单的“单向数据流”的方式管理应用,即状态 -> 视图 -> 操作单向循环的方式。但当我们的应用遇到多个组件共享状态时,比如:多个视图依赖于同一状态或者来自不同视图的行为需要变更同一状态。此时单向数据流的简洁性很容易被破坏。因此,我们有必要把组件的共享状态抽取出来,以一个全局单例模式管理。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这是vuex
存在的必要性,它和react
生态中的redux
之类是一个概念
Vuex
解决状态管理的同时引入了不少概念:例如state
、mutation
、action
等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用 Vuex
反而是繁琐冗余的,一个简单的 store
模式就足够了。但是,如果要构建一个中大型单页应用,Vuex
基本是标配。
我在使用vuex
过程中感受到一些等
可能的追问
vuex
有什么缺点吗?你在开发过程中有遇到什么问题吗?
action
和mutation
的区别是什么?为什么要区分它们?
流程顺序:“相应视图—>修改 State”拆分成两部分,视图触发Action
,Action再触发
Mutation`。
基于流程顺序,二者扮演不同的角色:Mutation
:专注于修改State
,理论上是修改State
的唯一途径。Action
:业务代码、异步请求
角色不同,二者有不同的限制:Mutation
:必须同步执行。Action
:可以异步,但不能直接操作State
双向数据绑定的原理
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter 这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化
compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个 update()方法 ③待属性变动 dep.notice()通知时,能调用自身的 update()方法,并触发 Compile 中绑定的回调,则功成身退。
MVVM 作为数据绑定的入口,整合 Observer、Compile 和 Watcher 三者,通过 Observer 来监听自己的 model 数据变化,通过 Compile 来解析编译模板指令,最终利用 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果。
写过自定义指令吗 原理是什么
指令本质上是装饰器,是 vue 对 HTML 元素的扩展,给 HTML 元素增加自定义功能。vue 编译 DOM 时,会找到指令对象,执行指令的相关方法。
自定义指令有五个生命周期(也叫钩子函数),分别是 bind、inserted、update、componentUpdated、unbind
1. bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
2. inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
3. update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
4. componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
5. unbind:只调用一次,指令与元素解绑时调用。
复制代码
原理
1.在生成 ast 语法树时,遇到指令会给当前元素添加 directives 属性
2.通过 genDirectives 生成指令代码
3.在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中调用对应的钩子
4.当执行指令对应钩子函数时,调用对应指令定义的方法
用过 pinia 吗?有什么优点?
1. pinia 是什么?
2. 为什么要使用 pinia?
Vue2
和Vue3
都支持,这让我们同时使用Vue2
和Vue3
的小伙伴都能很快上手。
pinia
中只有state
、getter
、action
,抛弃了Vuex
中的Mutation
,Vuex
中mutation
一直都不太受小伙伴们的待见,pinia
直接抛弃它了,这无疑减少了我们工作量。
pinia
中action
支持同步和异步,Vuex
不支持
良好的Typescript
支持,毕竟我们Vue3
都推荐使用TS
来编写,这个时候使用pinia
就非常合适了
无需再创建各个模块嵌套了,Vuex
中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia
中每个store
都是独立的,互相不影响。
体积非常小,只有1KB
左右。
pinia
支持插件来扩展自身功能。
支持服务端渲染
3. pinna 使用
pinna文档(opens new window)
准备工作
我们这里搭建一个最新的Vue3 + TS + Vite
项目
npm create vite@latest my-vite-app --template vue-ts
复制代码
pinia
基础使用
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "pinia";
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
app.mount("#app");
复制代码
2.1 创建store
//sbinsrc/store/user.ts
import { defineStore } from 'pinia'
// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
// 其它配置项
})
复制代码
创建store
很简单,调用 pinia
中的defineStore
函数即可,该函数接收两个参数:
我们可以定义任意数量的store
,因为我们其实一个store
就是一个函数,这也是pinia
的好处之一,让我们的代码扁平化了,这和Vue3
的实现思想是一样的
2.2 使用store
<!-- src/App.vue -->
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
console.log(store);
</script>
复制代码
2.3 添加state
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
});
复制代码
2.4 读取state
数据
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>性别:{{ sex }}</p>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
const name = ref<string>(store.name);
const age = ref<number>(store.age);
const sex = ref<string>(store.sex);
</script>
复制代码
上段代码中我们直接通过store.age
等方式获取到了store
存储的值,但是大家有没有发现,这样比较繁琐,我们其实可以用解构的方式来获取值,使得代码更简洁一点
import { useUsersStore, storeToRefs } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store); // storeToRefs获取的值是响应式的
复制代码
2.5 修改state
数据
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>性别:{{ sex }}</p>
<button @click="changeName">更改姓名</button>
</template>
<script setup lang="ts">
import child from './child.vue';
import { useUsersStore, storeToRefs } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store);
const changeName = () => {
store.name = "张三";
console.log(store);
};
</script>
复制代码
2.6 重置state
<button @click="reset">重置store</button>
// 重置store
const reset = () => {
store.$reset();
};
复制代码
当我们点击重置按钮时,store
中的数据会变为初始状态,页面也会更新
2.7 批量更改state
数据
如果我们一次性需要修改很多条数据的话,有更加简便的方法,使用store
的$patch
方法,修改app.vue
代码,添加一个批量更改数据的方法
<button @click="patchStore">批量修改数据</button>
// 批量修改数据
const patchStore = () => {
store.$patch({
name: "张三",
age: 100,
sex: "女",
});
};
复制代码
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
复制代码
2.8 直接替换整个state
pinia
提供了方法让我们直接替换整个state
对象,使用store
的$state
方法
store.$state = { counter: 666, name: '张三' }
复制代码
上段代码会将我们提前声明的state
替换为新的对象,可能这种场景用得比较少
getters
属性
3.1 添加getter
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 10,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return state.age + 100;
},
},
})
复制代码
上段代码中我们在配置项参数中添加了getter
属性,该属性对象中定义了一个getAddAge
方法,该方法会默认接收一个state
参数,也就是state
对象,然后该方法返回的是一个新的数据
3.2 使用getter
<template>
<p>新年龄:{{ store.getAddAge }}</p>
<button @click="patchStore">批量修改数据</button>
</template>
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
// 批量修改数据
const patchStore = () => {
store.$patch({
name: "张三",
age: 100,
sex: "女",
});
};
</script>
复制代码
上段代码中我们直接在标签上使用了store.gettAddAge
方法,这样可以保证响应式,其实我们state
中的name
等属性也可以以此种方式直接在标签上使用,也可以保持响应式
3.3 getter
中调用其它getter
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return state.age + 100;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
});
复制代码
3.3 getter
传参
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
});
复制代码
<p>新年龄:{{ store.getAddAge(1100) }}</p>
复制代码
actions
属性
前面我们提到的state
和getter
s 属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data
数据和computed
计算属性一样。
那么,如果我们有业务代码的话,最好就是卸载actions
属性里面,该属性就和我们组件代码中的methods
相似,用来放置一些处理业务逻辑的方法。
actions
属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法
4.1 添加actions
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
actions: {
// 在实际场景中,该方法可以是任何逻辑,比如发送请求、存储token等等。大家把actions方法当作一个普通的方法即可,特殊之处在于该方法内部的this指向的是当前store
saveName(name: string) {
this.name = name;
},
},
});
复制代码
4.2 使用actions
使用actions
中的方法也非常简单,比如我们在App.vue
中想要调用该方法
const saveName = () => {
store.saveName("poetries");
};
复制代码
总结
pinia
的知识点很少,如果你有 Vuex 基础,那么学起来更是易如反掌
pinia 无非就是以下 3 个大点:
Vue 组件如何通信?
Vue 组件通信的方法如下:
props/$emit+v-on
: 通过 props 将数据自上而下传递,而通过 $emit 和 v-on 来向上传递信息。
EventBus: 通过 EventBus 进行信息的发布与订阅
vuex: 是全局数据管理库,可以通过 vuex 管理全局的数据流
$attrs/$listeners
: Vue2.4 中加入的$attrs/$listeners
可以进行跨级的组件通信
provide/inject:以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,这成为了跨组件通信的基础
还有一些用 solt 插槽或者 ref 实例进行通信的,使用场景过于有限就不赘述了。
v-model 的原理?
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text 和 textarea 元素使用 value 属性和 input 事件;
checkbox 和 radio 使用 checked 属性和 change 事件;
select 字段将 value 作为 prop 并将 change 作为事件。
以 input 表单元素为例:
<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
复制代码
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
父组件:
<ModelChild v-model="message"></ModelChild>
子组件:
<div>{{value}}</div>
props:{
value: String
},
methods: {
test1(){
this.$emit('input', '小红')
},
},
复制代码
评论