共计 36783 个字符,预计需要花费 92 分钟才能阅读完成。
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
能够开发出更多其余的性能,而不用担心 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
- 全局
Vue API
已更改为应用应用程序实例 - 全局和外部
API
曾经被重构为可tree-shakable
3.2 模板指令
- 组件上
v-model
用法已更改 <template v-for>
和 非v-for
节点上key
用法已更改- 在同一元素上应用的
v-if
和v-for
优先级已更改 v-bind="object"
当初排序敏感v-for
中的ref
不再注册ref
数组
3.3 组件
- 只能应用一般函数创立性能组件
functional
属性在单文件组件(SFC)
- 异步组件当初须要
defineAsyncComponent
办法来创立
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
keyCode
反对作为v-on
的修饰符$on
,$off
和$once
实例办法- 过滤
filter
- 内联模板
attribute
$destroy
实例办法。用户不应再手动治理单个Vue
组件的生命周期。
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
的区别
$route
是“路由信息对象”,包含path
,params
,hash
,query
,fullPath
,matched
,name
等路由信息参数。- 而
$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)
}
}
仔细阅读下面的代码,咱们失去以下论断:
- 在调用
beforeCreate
之前,数据初始化并未实现,像data
、props
这些属性无法访问到 - 到了
created
的时候,数据曾经初始化实现,可能拜访data
、props
这些属性,但这时候并未实现dom
的挂载,因而无法访问到dom
元素 - 挂载办法是调用
vm.$mount
办法
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 */)
}
仔细阅读下面的代码,咱们能够失去以下论断:
- 初始化程序:
props
、methods
、data
data
定义的时候可选择函数模式或者对象模式(组件只能为函数模式)
对于数据响应式在这就不开展具体阐明
上文提到挂载办法是调用 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)
}
浏览下面代码,咱们能失去以下论断:
- 不要将根元素放到
body
或者html
上 - 能够在对象中定义
template/render
或者间接应用template
、el
示意元素选择器 - 最终都会解析成
render
函数,调用compileToFunctions
,会将template
解析成render
函数
对 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
}
浏览下面代码,咱们失去以下论断:
- 会触发
boforeCreate
钩子 - 定义
updateComponent
渲染页面视图的办法 - 监听组件数据,一旦发生变化,触发
beforeUpdate
生命钩子
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
、Store
)的分割; - 属性扭转后,告诉计算属性从新计算
实现时,次要如下
- 初始化
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
有什么毛病吗?你在开发过程中有遇到什么问题吗?- 刷新浏览器,
vuex
中的state
会从新变为初始状态。解决方案 - 插件vuex-persistedstate
action
和mutation
的区别是什么?为什么要辨别它们?
action
中解决异步,mutation
不能够mutation
做原子操作action
能够整合多个mutation
的汇合mutation
是同步更新数据(外部会进行是否为异步形式更新数据的检测)$watch
严格模式下会报错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 是什么?
- 在
Vue3
中,能够应用传统的Vuex
来实现状态治理,也能够应用最新的pinia
来实现状态治理,咱们来看看官网如何解释pinia
的:Pinia
是Vue
的存储库,它容许您跨组件 / 页面共享状态。- 实际上,
pinia
就是Vuex
的升级版,官网也说过,为了尊重原作者,所以取名pinia
,而没有取名Vuex
,所以大家能够间接将pinia
比作为Vue3
的Vuex
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 [email protected] my-vite-app --template vue-ts
pinia
根底应用
yarn add 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
很简略,调用 p inia
中的 defineStore
函数即可,该函数接管两个参数:
name
:一个字符串,必传项,该store
的惟一id
。options
:一个对象,store
的配置项,比方配置store
内的数据,批改数据的办法等等。
咱们能够定义任意数量的 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
- 有时候咱们批改了
state
数据,想要将它还原,这个时候该怎么做呢?就比方用户填写了一部分表单,忽然想重置为最初始的状态。 - 此时,咱们间接调用
store
的$reset()
办法即可,持续应用咱们的例子,增加一个重置按钮
<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: "女",
});
};
- 有教训的小伙伴可能发现了,咱们采纳这种批量更改的形式仿佛代价有一点大,如果咱们
state
中有些字段无需更改,然而依照上段代码的写法,咱们必须要将 state 中的所有字段例举出了。 - 为了解决该问题,
pinia
提供的$patch
办法还能够接管一个回调函数,它的用法有点像咱们的数组循环回调函数了。
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
属性getters
是defineStore
参数配置项外面的另一个属性- 能够把
getter
设想成Vue
中的计算属性,它的作用就是返回一个新的后果,既然它和Vue
中的计算属性相似,那么它必定也是会被缓存的,就和computed
一样
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 个大点:
state
getters
actions
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', '小红')
},
},