day-114-one-hundred-and-fourteen-20230717-常见面试题-todoList-封装服务相关的组件-vue3生态-vue-router-pinia
常见面试题
v-if与v-for
- vue2模板编译网站
- vue3模板编译网站
- 查看一些第三方插件,可以通过npm或github来进行查找。
-
v-if 和 v-for的优先级问题
-
vue2 肯定是v-for他的优先级高, 在同一个元素中使用,会发生需要每循环一次就判断一次。 将数据计算出来再循环。 用计算属性来代替 v-for + if
<div v-for="item of [1,2,3,4,5]"> <template > <div v-if="item%2 === 0"></div> <template> </div>
- 在vue3 中v-for的优先级低于v-if。 v-if 会被抽离到v-for的上一层元素中,如果有依赖关系,建议提升到计算属性后使用 查看模板编译的结果
- 如果判断条件需要依赖v-for的结果,应该写成计算属性的⽅式。
- 在vue3 中v-for的优先级低于v-if。 v-if 会被抽离到v-for的上一层元素中,如果有依赖关系,建议提升到计算属性后使用 查看模板编译的结果
-
-
在vue3 中v-for的优先级低于v-if。 v-if 会被抽离到v-for的上一层元素中,如果有依赖关系,建议提升到计算属性后使用 查看模板编译的结果
模板编译
-
vue2
<div> <div v-for="item of [1, 2, 3, 4, 5]" v-if="item % 2 === 0"> <div>{{ item }}</div> </div> </div>
function render() { with(this) { return _c('div', _l(([1, 2, 3, 4, 5]), function (item) { return (item % 2 === 0) ? _c('div', [_c('div', [_v(_s(item))])]) : _e() }), 0) } }
-
vue3
<div> <div v-for="item of [1, 2, 3, 4, 5]" v-if="item % 2 === 0"> <div>{{ item }}</div> </div> </div>
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, createCommentVNode as _createCommentVNode } from "vue" export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", null, [ (_ctx.item % 2 === 0) ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, _renderList([1, 2, 3, 4, 5], (item) => { return _createElementVNode("div", null, [ _createElementVNode("div", null, _toDisplayString(item), 1 /* TEXT */) ]) }), 64 /* STABLE_FRAGMENT */)) : _createCommentVNode("v-if", true) ])) }
v-if与v-show
-
Vue 中的 v-show 和 v-if 怎么理解?
- v-if使用场景会更多一些, v-if 可以配合 v-else v-else-if (v-if 可以放到template上,但是v-show不行),控制的是display属性, 会选择默认的或者display:none v-show会编译成指令,v-if 会编译成三元表达式 (visibility:hidden opacity)
- v-if条件不成⽴不会渲染当前指令所在节点的DOM元素;
- v-show只切换当前dom的显示或者隐藏 display属性;
- v-if可以阻断内部代码是否执⾏,如果条件成⽴不会执⾏内部逻辑,如果⻚⾯逻辑在第⼀次加载的时候已经确认,后续不会频繁更改则采⽤v-if;
-
代码:
-
v-show
<div v-for="item of [1, 2, 3, 4, 5]" v-if="item % 2 === 0"> <template v-show="false"> <div>{{ item }}</div> </template> </div>
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, vShow as _vShow, withDirectives as _withDirectives, createCommentVNode as _createCommentVNode } from "vue" export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_ctx.item % 2 === 0) ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, _renderList([1, 2, 3, 4, 5], (item) => { return _createElementVNode("div", null, [ _withDirectives(_createElementVNode("template", null, [ _createElementVNode("div", null, _toDisplayString(item), 1 /* TEXT */) ], 512 /* NEED_PATCH */), [ [_vShow, false] ]) ]) }), 64 /* STABLE_FRAGMENT */)) : _createCommentVNode("v-if", true) } // Check the console for the AST
-
v-if
<div v-for="item of [1, 2, 3, 4, 5]" v-if="item % 2 === 0"> <template v-if="false"> <div>{{ item }}</div> </template> </div>
import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, toDisplayString as _toDisplayString, createCommentVNode as _createCommentVNode, createElementVNode as _createElementVNode } from "vue" const _hoisted_1 = { key: 0 } export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_ctx.item % 2 === 0) ? (_openBlock(), _createElementBlock(_Fragment, { key: 0 }, _renderList([1, 2, 3, 4, 5], (item) => { return _createElementVNode("div", null, [ false ? (_openBlock(), _createElementBlock("div", _hoisted_1, _toDisplayString(item), 1 /* TEXT */)) : _createCommentVNode("v-if", true) ]) }), 64 /* STABLE_FRAGMENT */)) : _createCommentVNode("v-if", true) } // Check the console for the AST
-
v-show源码
- fang/f20230717/vue3-lan-1/node_modules/vue/dist/vue.runtime.esm-browser.js
const vShow = {
beforeMount(el, { value }, { transition }) {
el._vod = el.style.display === "none" ? "" : el.style.display;
if (transition && value) {
transition.beforeEnter(el);
} else {
setDisplay(el, value);
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
transition.enter(el);
}
},
updated(el, { value, oldValue }, { transition }) {
if (!value === !oldValue)
return;
if (transition) {
if (value) {
transition.beforeEnter(el);
setDisplay(el, true);
transition.enter(el);
} else {
transition.leave(el, () => {
setDisplay(el, false);
});
}
} else {
setDisplay(el, value);
}
},
beforeUnmount(el, { value }) {
setDisplay(el, value);
}
};
function setDisplay(el, value) {
el.style.display = value ? el._vod : "none";
}
vue3中组件通信
-
Vue 3 中如何进行组件通信?
- 父-》子
- props 属性传递参数,子元素要通过props进行接收。
- attrs 属性传递参数,子元素不用接受。
- ref 获取子组件的实例。
- 可以通过provide 提供属性给儿子。
- 子-》父
- 绑定的事件可以通过props来接受,还可以通过attrs.onXxx来执行,emit 父亲给子组件绑定事件,子组件触发绑定的事件。
- 默认情况下所有的属性都会暴露出来 expose 来暴露属性。
- $parent
- 可以通过inject,拿到父组件方法进行通信。
- 平级传递参数
- 找到共同的父级。
- 跨级传递参数
- provide , inject (不建议在业务代码中使用) 数据来源不明确, 会发生重名的问题。
- eventBus -> mitt (vue3推荐使用mitt来做通信处理不在支持 eventBus)。
- vuex来进行数据处理。
- 父-》子
-
Vue 3 中如何进⾏组件通信?
- props
- $emit
- $slots
- $attrs
- expose / ref
- v-model
- provide / inject
- mitt
$parent通信
vue2的$parent通信
vue3的parent通信
-
父组件传递给子组件:
-
src/App.vue
<template> <Com></Com> <Com></Com> </template> <script setup> import { ref, provide } from "vue" import Com from "./components/com.vue" function parentChange() {} defineExpose({ change:parentChange }) </script>
-
src/components/com.vue
<template> 组件 </template> <script setup> import { getCurrentInstance, inject, onMounted } from "vue" const instance = getCurrentInstance() console.log("子组件访问父组件实例:instance.parent.exposed.change-->", instance.parent.exposed.change) defineProps(["modelValue"]) </script>
-
-
子组件传递给父组件:
-
src/App.vue
<template> <Com v-model:val="val"></Com> <Com v-model="val"></Com> </template> <script setup> import { ref, provide } from "vue" import Com from "./components/com.vue" const val = ref("aaa") const children = ref([]) provide("collect", function (instance) { // 用于收集子组件的 children.value.push(instance) console.log(`父组件中收集的子组件实例: children-->`, children); }) </script>
-
src/components/com.vue
<template> 组件 </template> <script setup> import { getCurrentInstance, inject, onMounted } from "vue" const instance = getCurrentInstance() const collect = inject("collect") console.log(collect) onMounted(() => { collect(instance) }) </script>
-
provide-inject通信
- 数据来源不明确,会优先最近的。但直系祖先中,每个组件都可以直接发布一个任意名称的provide,可能会有命名冲突。
- src/App.vue
<template>
<Com></Com>
</template>
<script setup>
// import { ref } from "vue"
// const val = ref("aaa")
import Com from "./components/com.vue"
import { provide } from "vue"
provide("collect", function (param) {
console.log(`父组件: param-->`, param)
})
provide("provideName", 666)
</script>
- src/components/com.vue
<template>
组件
</template>
<script setup>
import { inject, onMounted } from "vue"
const collect = inject("collect")
console.log(`子组件中:collect-->`, collect)
const provideName = inject("provideName")
console.log(`子组件中:provideName-->`, provideName)
onMounted(() => {
console.log(`子组件onMounted中:collect-->`, collect)
console.log(`子组件onMounted中:provideName-->`, provideName)
})
</script>
mitt
- 查看一些第三方插件,可以通过npm或github来进行查找。
- mitt
vue3模板优化
-
vue3 模版优化有哪些?
-
vue3中模版编译时会新增block节点 (收集动态节点的, 按照数组的维度来更新 靶向更新)
<template v-if="false"> <div>[1, 2, 3, 4, 5]</div> </template> <div>1</div> <div>1</div> <div>1</div>
const { openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, createTextVNode: _createTextVNode, Fragment: _Fragment } = Vue return function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock(_Fragment, null, [ false ? (_openBlock(), _createElementBlock("div", { key: 0 }, "[1, 2, 3, 4, 5]"))//新增block节点,收集动态节点的 : _createCommentVNode("v-if", true), _createTextVNode(), _createElementVNode("div", null, "1"), _createTextVNode(), _createElementVNode("div", null, "1"), _createTextVNode(), _createElementVNode("div", null, "1") ], 64 /* STABLE_FRAGMENT */)) } // Check the console for the AST
-
vue3模版编译中会对动态节点做标识 patchFlags 标记哪些属性是动态的,更新时只更新动态属性
<template v-if="false"> <div :style="{coloe:isTrue?'red':'blue'}" class="fang">[1, 2, 3, 4, 5]</div> </template>
const { normalizeStyle: _normalizeStyle, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode } = Vue return function render(_ctx, _cache, $props, $setup, $data, $options) { return false ? (_openBlock(), _createElementBlock("div", { key: 0, style: _normalizeStyle({coloe:_ctx.isTrue?'red':'blue'}), class: "fang" }, "[1, 2, 3, 4, 5]", 4 /* STYLE */))//patchFlags 标记哪些属性是动态的,更新时只更新动态属性; : _createCommentVNode("v-if", true) } // Check the console for the AST
-
vue3编译的时候会对函数进行缓存优化,不用每次都创建
<div v-for="item in arr"> <div @click="handleClick">88</div> </div>
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createElementVNode: _createElementVNode } = Vue return function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.arr, (item) => { return (_openBlock(), _createElementBlock("div", null, [ _createElementVNode("div", { onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))//对函数进行缓存优化 }, "88") ])) }), 256 /* UNKEYED_FRAGMENT */)) }
-
-
vue3编译的时候会将静态节点做静态提升,后续直接采用提升后的结果
<div v-for="item in arr"> <div>88</div> </div>
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createElementVNode: _createElementVNode } = Vue const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "88", -1 /* HOISTED */)//将静态节点做静态提升 const _hoisted_2 = [ _hoisted_1 ] return function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.arr, (item) => { return (_openBlock(), _createElementBlock("div", null, _hoisted_2)) }), 256 /* UNKEYED_FRAGMENT */)) } // Check the console for the AST
-
vue3中会对同一个静态节点超过20个 会转化成字符串来渲染
<div> <div>88</div> <div>88</div> <div>88</div> <div>88</div> <div>88</div> <div>88</div> <div>88</div> <div>88</div> <div>88</div> <div>12</div> <div>88</div> <div>88</div> <div>88</div> <div>88</div> <div>88</div> <div>88</div> </div>
const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<div>88</div><div>88</div><div>88</div><div>88</div><div>88</div><div>88</div><div>88</div><div>88</div><div>88</div><div>12</div><div>88</div><div>88</div><div>88</div><div>88</div><div>88</div><div>88</div>", 16)//会转化成字符串来渲染 const _hoisted_17 = [ _hoisted_1 ] return function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", null, _hoisted_17)) } // Check the console for the AST
v-model的原理
- v-model的原理
- 响应式原理 (Object.defineProperty , proxy) 和 双向绑定原理是一个东西么? (表单元素绑定v-model)
- 对于表单元素而言,input 的v-model 不能直接等于 value + input (内部会根据类型触发不同的事件 input,change ,对于输入框来说,做了中文处理)
- v-model 可以用在组件 (vue2中v-model是value + input)的语法糖。 vue3中名字被修改了.默认值 modelValue + update:modelValue 可以通过:的方式来修改名字。 .sync语法废弃了
响应式原理
-
vue2中的响应式就是Object.defineProperty。vue3就是Proxy。
-
说说你对双向绑定的理解,以及它的实现原理吗?
- 双向绑定的概念: vue 中双向绑定靠的是指令 v-model,可以绑定⼀个动态值到视图上,同时修改视图能改变数据对应的值(能修改的视图就是表单组件)
- 表单元素中的 v-model:
- 内部会根据标签的不同解析出不同的语法。并且这⾥有“额外”的处理逻辑;
- 例如 ⽂本框会被解析成 value + input 事件
- 例如 复选框会被解析成 checked + change 事件
- …
- 内部会根据标签的不同解析出不同的语法。并且这⾥有“额外”的处理逻辑;
- 组件中的 v-model
-
组件上的 v-model 默认会利⽤名为 modelValue 的 prop 和名为 onUpdate:modelValue 的事件。对于组件⽽⾔ v-model就是个语法糖。可⽤于组件中数据的双向绑定。
-
绑定的名字也可以修改:
<my v-model:a="a" v-model:b="b" v-model:c="c"> </my>
-
双向数据绑定
-
vue中的双向数据绑定就是对表单元素做了一些常见的数据绑定及事件绑定。具体来说主要就是v-model了。
-
v-model="state"
类似于:valule="state" @input="e=>state=e.target.value"
; -
代码示例:
-
src/App.vue
<template> <Com v-model:val="val"></Com> <Com v-model="val"></Com> <input type="text" v-model="val" /> <input type="text" :value="val" @input="(e) => (val = e.target.value)" /> {{ val }} </template> <script setup> import { ref } from "vue" const val = ref("aaa") </script>
-
src/components/com.vue
<template> 组件{{ modelValue }} <button @click="$emit('update:modelValue', 'abc')">修改</button> <br /> </template> <script setup> defineProps(["modelValue"]) // const emit = defineEmits(["update:modelValue"]) </script>
-
模板中对比v-model的绑定及编译
<input type="text" v-model="val"/>
const { vModelText: _vModelText, withDirectives: _withDirectives, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache, $props, $setup, $data, $options) {
return _withDirectives((_openBlock(), _createElementBlock("input", {
type: "text",
"onUpdate:modelValue": _cache[0] || (_cache[0] = $event => ((_ctx.val) = $event))
}, null, 512 /* NEED_PATCH */)), [
[_vModelText, _ctx.val]
])
}
// Check the console for the AST
v-model源码
- fang/f20230717/vue3-lan-1/node_modules/vue/dist/vue.runtime.esm-browser.js
const vModelText = {
created(el, { modifiers: { lazy, trim, number } }, vnode) {
el._assign = getModelAssigner(vnode);
const castToNumber = number || vnode.props && vnode.props.type === "number";
addEventListener(el, lazy ? "change" : "input", (e) => {
if (e.target.composing)
return;
let domValue = el.value;
if (trim) {
domValue = domValue.trim();
}
if (castToNumber) {
domValue = looseToNumber(domValue);
}
el._assign(domValue);
});
if (trim) {
addEventListener(el, "change", () => {
el.value = el.value.trim();
});
}
if (!lazy) {
addEventListener(el, "compositionstart", onCompositionStart);
addEventListener(el, "compositionend", onCompositionEnd);
addEventListener(el, "change", onCompositionEnd);
}
},
// set value on mounted so it's after min/max for type="range"
mounted(el, { value }) {
el.value = value == null ? "" : value;
},
beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
el._assign = getModelAssigner(vnode);
if (el.composing)
return;
if (document.activeElement === el && el.type !== "range") {
if (lazy) {
return;
}
if (trim && el.value.trim() === value) {
return;
}
if ((number || el.type === "number") && looseToNumber(el.value) === value) {
return;
}
}
const newValue = value == null ? "" : value;
if (el.value !== newValue) {
el.value = newValue;
}
}
};
const vModelCheckbox = {
// #4096 array checkboxes need to be deep traversed
deep: true,
created(el, _, vnode) {
el._assign = getModelAssigner(vnode);
addEventListener(el, "change", () => {
const modelValue = el._modelValue;
const elementValue = getValue(el);
const checked = el.checked;
const assign = el._assign;
if (isArray(modelValue)) {
const index = looseIndexOf(modelValue, elementValue);
const found = index !== -1;
if (checked && !found) {
assign(modelValue.concat(elementValue));
} else if (!checked && found) {
const filtered = [...modelValue];
filtered.splice(index, 1);
assign(filtered);
}
} else if (isSet(modelValue)) {
const cloned = new Set(modelValue);
if (checked) {
cloned.add(elementValue);
} else {
cloned.delete(elementValue);
}
assign(cloned);
} else {
assign(getCheckboxValue(el, checked));
}
});
},
// set initial checked on mount to wait for true-value/false-value
mounted: setChecked,
beforeUpdate(el, binding, vnode) {
el._assign = getModelAssigner(vnode);
setChecked(el, binding, vnode);
}
};
function setChecked(el, { value, oldValue }, vnode) {
el._modelValue = value;
if (isArray(value)) {
el.checked = looseIndexOf(value, vnode.props.value) > -1;
} else if (isSet(value)) {
el.checked = value.has(vnode.props.value);
} else if (value !== oldValue) {
el.checked = looseEqual(value, getCheckboxValue(el, true));
}
}
const vModelRadio = {
created(el, { value }, vnode) {
el.checked = looseEqual(value, vnode.props.value);
el._assign = getModelAssigner(vnode);
addEventListener(el, "change", () => {
el._assign(getValue(el));
});
},
beforeUpdate(el, { value, oldValue }, vnode) {
el._assign = getModelAssigner(vnode);
if (value !== oldValue) {
el.checked = looseEqual(value, vnode.props.value);
}
}
};
const vModelSelect = {
// <select multiple> value need to be deep traversed
deep: true,
created(el, { value, modifiers: { number } }, vnode) {
const isSetModel = isSet(value);
addEventListener(el, "change", () => {
const selectedVal = Array.prototype.filter.call(el.options, (o) => o.selected).map(
(o) => number ? looseToNumber(getValue(o)) : getValue(o)
);
el._assign(
el.multiple ? isSetModel ? new Set(selectedVal) : selectedVal : selectedVal[0]
);
});
el._assign = getModelAssigner(vnode);
},
// set value in mounted & updated because <select> relies on its children
// <option>s.
mounted(el, { value }) {
setSelected(el, value);
},
beforeUpdate(el, _binding, vnode) {
el._assign = getModelAssigner(vnode);
},
updated(el, { value }) {
setSelected(el, value);
}
};
function setSelected(el, value) {
const isMultiple = el.multiple;
if (isMultiple && !isArray(value) && !isSet(value)) {
warn(
`<select multiple v-model> expects an Array or Set value for its binding, but got ${Object.prototype.toString.call(value).slice(8, -1)}.`
);
return;
}
for (let i = 0, l = el.options.length; i < l; i++) {
const option = el.options[i];
const optionValue = getValue(option);
if (isMultiple) {
if (isArray(value)) {
option.selected = looseIndexOf(value, optionValue) > -1;
} else {
option.selected = value.has(optionValue);
}
} else {
if (looseEqual(getValue(option), value)) {
if (el.selectedIndex !== i)
el.selectedIndex = i;
return;
}
}
}
if (!isMultiple && el.selectedIndex !== -1) {
el.selectedIndex = -1;
}
}
function getValue(el) {
return "_value" in el ? el._value : el.value;
}
function getCheckboxValue(el, checked) {
const key = checked ? "_trueValue" : "_falseValue";
return key in el ? el[key] : checked;
}
const vModelDynamic = {
created(el, binding, vnode) {
callModelHook(el, binding, vnode, null, "created");
},
mounted(el, binding, vnode) {
callModelHook(el, binding, vnode, null, "mounted");
},
beforeUpdate(el, binding, vnode, prevVNode) {
callModelHook(el, binding, vnode, prevVNode, "beforeUpdate");
},
updated(el, binding, vnode, prevVNode) {
callModelHook(el, binding, vnode, prevVNode, "updated");
}
};
function resolveDynamicModel(tagName, type) {
switch (tagName) {
case "SELECT":
return vModelSelect;
case "TEXTAREA":
return vModelText;
default:
switch (type) {
case "checkbox":
return vModelCheckbox;
case "radio":
return vModelRadio;
default:
return vModelText;
}
}
}
function callModelHook(el, binding, vnode, prevVNode, hook) {
const modelToUse = resolveDynamicModel(
el.tagName,
vnode.props && vnode.props.type
);
const fn = modelToUse[hook];
fn && fn(el, binding, vnode, prevVNode);
}
.sync与v-model区别
- v-model对一些表单元素做的处理比.sync来得要好。
.sync与v-model
- vue3中,.sync已经被废弃。而
v-model:xxx=""
取代了:xxx.sync=""
; v-model:xxx=""
对应的是onUpdate:xxx
事件。
todoList
pc与移动端
-
如何能实现 pc + 移动端一套代码在一起?
- 2套样式, 格局普通大小,浏览器内核,来动态添加样式。
-
pc与移动端一套代码,两套样式。
- 样式为格局普通大小,浏览器内核,
toRaw与markRaw
- 在响应式数据reactive与ref中,如果它的属性也是一个对象。
响应式数据
的对象类型的属性值
在取值时也会变成响应式对象
,取值时会有get方法。- 性能优化,使用toRaw(响应式对象)拿到响应式对象的原对象,之后对原对应进行取值操作,不会触发get中的视图收集。
- markRaw是标记它是一个原始的值,不进行响应式处理。
toRaw
<script setup>
import { shallowReactive } from "vue"
import { readonly } from "vue"
import { shallowRef } from "vue"
import { markRaw, toRaw, reactive, ref } from "vue"
const targetData = [{ num: 1, id: 1 }]
const theRefData = ref(targetData)
console.log(`theRefData-->`, theRefData)
console.log(`theRefData[0]是一个响应式对象-->`, theRefData.value[0])
const handleRef = () => {
theRefData.value[0].num++
}
// toRaw 转换成原始值 markRaw(标记他是一个原始的值) isRef() isReactive() shallowRef showReactive
// readonly isReadonly
// toRaw 转换成原始值
const toRawData = toRaw(theRefData.value)
// 之后对toRawData进行取值操作,返回的不是响应式对象,也不会有get函数触发。
console.log(`toRawData-->`, toRawData)
</script>
<template>
<div class="todo">
<div>targetData - {{ targetData }}</div>
<div>theRefData - {{ theRefData }}</div>
<div>theRefData[0] - {{ theRefData[0] }}</div>
<div @click="handleRef()">handleRef</div>
<div>toRawData - {{ toRawData }}</div>
</div>
</template>
markRaw
<script setup>
import { shallowReactive } from "vue"
import { readonly } from "vue"
import { shallowRef } from "vue"
import { markRaw, toRaw, reactive, ref } from "vue"
const targetData = [{ num: 1, id: 1 }]
// markRaw标记括号中的值是一个原始的值,用于让它的get劫持中,不会更新视图。
const markRawData = markRaw(targetData)
console.log(`markRawData-->`, markRawData)
const markRawRef = ref(markRawData)
console.log(`markRawRef-->`, markRawRef)
const handleMarkRawRef = () => {
markRawRef.value[0].num++
console.log(`markRawRef.value[0].num-->`, markRawRef.value[0].num)
console.log(`markRawRef-->`, markRawRef)//值变了,但视图并没有更新
}
</script>
<template>
<div class="todo">
<div>targetData - {{ targetData }}</div>
<div>toRawData - {{ toRawData }}</div>
<div>markRawData - {{ markRawData }}</div>
<div>markRawRef - {{ markRawRef }}</div>
<div @click="handleMarkRawRef()">handleMarkRawRef</div>
</div>
</template>
其它api
- isRef、isReactive、shallowRef、shallowReacive、readonly、isReadonly。
v-if的vue2与vue3问题
-
vue3 中v-if 和 v-else 会认为是两个完全不同的元素;
- vue2 会尽可能的复用;
-
vue3 切换后,text输入框与password输入框的value不共用。
<div> <input type="text" v-if="flag"> <input type="password" v-else> <button @click="flag=!flag"></button> </div
const { openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, createTextVNode: _createTextVNode } = Vue return function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", null, [ (_ctx.flag) ? (_openBlock(), _createElementBlock("input", { key: 0, type: "text" })) : (_openBlock(), _createElementBlock("input", { key: 1, type: "password" })), _createTextVNode(), _createElementVNode("button", { onClick: $event => (_ctx.flag=!_ctx.flag) }, null, 8 /* PROPS */, ["onClick"]) ])) } // Check the console for the AST
-
vue2 切换后,text输入框与password输入框的value是共用的。
<div> <input type="text" v-if="flag"> <input type="password" v-else> <button @click="flag=!flag"></button> </div
function render() { with(this) { return _c('div', [(flag) ? _c('input', { attrs: { "type": "text" } }) : _c('input', { attrs: { "type": "password" } }), _c('button', { on: { "click": function ($event) { flag = !flag } } })]) } }
未拆分前代码
- src/App.vue
<template>
<!-- vue3 中v-if 和 v-else 会认为是两个完全不同的元素, vue2 会尽可能的复用 -->
<!-- <div>
<input type="text" v-if="flag">
<input type="password" v-else>
<button @click="flag=!flag"></button>
</div> -->
<div class="todo">
<van-field label="代办事项" v-model="todo">
<template #button>
<van-button size="small" type="primary" @click="addTodo">添加</van-button>
</template>
</van-field>
<main>
<!-- 如何能实现 pc + 移动端一起 (2套样式, 格局普通大小,浏览器内核,来动态添加样式) -->
<template v-for="list in lists" :key="list.id">
<van-cell :title="list.todo" v-if="updateTodoId != list.id">
<template #right-icon>
<van-button type="danger" size="mini" @click="remove(list.id)">删除</van-button>
<van-button type="warning" size="mini" @click="update(list.id, list.todo)"
>修改</van-button
>
</template>
</van-cell>
<van-cell v-else>
<template #default>
<van-field v-model="updateTodoText" autofocus></van-field>
</template>
<template #right-icon>
<van-button type="primary" size="mini" @click="sure()">确认</van-button>
<van-button type="default" size="mini" @click="cancel()">取消</van-button>
</template>
</van-cell>
</template>
</main>
</div>
</template>
<script setup>
import { toRaw, ref, getCurrentInstance } from "vue"
const { proxy } = getCurrentInstance()
const todo = ref("")
const lists = ref([])
let id = 0
const addTodo = () => {
if (todo.value.trim() == "") {
return proxy.$showToast("请输入内容来添加")
}
const curretTodo = {
id: ++id,
todo: todo.value
}
lists.value.push(curretTodo)
todo.value = ""
}
async function remove(todoId) {
// toRaw 转换成原始值 markRaw(标记他是一个原始的值) isRef() isReactive() shallowRef showReactive
// readonly isReadonly
try {
await proxy.$showConfirmDialog({
title: "删除",
message: "确认删除出吗?"
})
const rawLists = toRaw(lists.value)
// 根据数据获取到对应的idx
const idx = rawLists.findIndex((item) => item.id === todoId)
lists.value.splice(idx, 1)
} catch (e) {}
}
const updateTodoId = ref(-1) // 当前点击修改的那一项
const updateTodoText = ref("") // 输入的新内容
function update(todoId, todoText) {
updateTodoId.value = todoId
updateTodoText.value = todoText
}
// 确认发光法, 就是直接找到那一项 将他替换内容即可
function sure() {
let currentId = updateTodoId.value // 先取出值在找索引
let list = lists.value.find((item) => item.id === currentId)
list.todo = updateTodoText.value
cancel()
}
function cancel() {
updateTodoId.value = -1
updateTodoText.value = ""
}
</script>
<style>
.todo {
width: 400px;
margin: 100px auto;
}
.van-field {
border: 1px solid red;
}
</style>
拆分后代码
- 子组件一般放一些组件内部不涉及到业务数据的操作,而修改业务数据一般就放在父组件中。
- 代码示例:
- src/App.vue
<template>
<!-- vue3 中v-if 和 v-else 会认为是两个完全不同的元素, vue2 会尽可能的复用 -->
<!-- <div>
<input type="text" v-if="flag">
<input type="password" v-else>
<button @click="flag=!flag"></button>
</div> -->
<div class="todo">
<van-field label="代办事项" v-model="todo">
<template #button>
<van-button size="small" type="primary" @click="addTodo">添加</van-button>
</template>
</van-field>
<main>
<!-- 如何能实现 pc + 移动端一起 (2套样式, 格局普通大小,浏览器内核,来动态添加样式) -->
<template v-if="lists.length > 0">
<list :lists="lists" @remove="remove" @sure="sure"></list>
</template>
</main>
</div>
</template>
<script setup>
import List from "./components/list.vue"
import { toRaw, ref, getCurrentInstance } from "vue"
const { proxy } = getCurrentInstance()
const todo = ref("")
const lists = ref([])
let id = 0
const addTodo = () => {
console.log(`拆分后`)
if (todo.value.trim() == "") {
return proxy.$showToast("请输入内容来添加")
}
const curretTodo = {
id: ++id,
todo: todo.value
}
lists.value.push(curretTodo)
todo.value = ""
}
// 用组件 自己操作数据
const remove = (idx) => {
lists.value.splice(idx, 1)
}
// 数据在父组件修改
const sure = ({ id, text }) => {
let list = lists.value.find((item) => item.id === id)
list.todo = text
}
</script>
<style>
.todo {
width: 400px;
margin: 100px auto;
}
.van-field {
border: 1px solid red;
}
</style>
- src/components/list.vue
<template>
<template v-for="list in lists" :key="list.id">
<van-cell :title="list.todo" v-if="updateTodoId != list.id">
<template #right-icon>
<van-button type="danger" size="mini" @click="remove(list.id)">删除</van-button>
<van-button type="warning" size="mini" @click="update(list.id, list.todo)">修改</van-button>
</template>
</van-cell>
<van-cell v-else>
<template #default>
<van-field v-model="updateTodoText" autofocus></van-field>
</template>
<template #right-icon>
<van-button type="primary" size="mini" @click="sure()">确认</van-button>
<van-button type="default" size="mini" @click="cancel()">取消</van-button>
</template>
</van-cell>
</template>
</template>
<script setup>
import { getCurrentInstance } from "vue";
import { toRaw, ref } from "vue"
const props = defineProps(["lists"])
const emit = defineEmits(['remove','sure'])
const {proxy} = getCurrentInstance()
async function remove(todoId) {
// toRaw 转换成原始值 markRaw(标记他是一个原始的值) isRef() isReactive() shallowRef showReactive
// readonly isReadonly
try {
await proxy.$showConfirmDialog({
title: "删除",
message: "确认删除出吗?"
})
const rawLists = toRaw(props.lists)
// 根据数据获取到对应的idx
const idx = rawLists.findIndex((item) => item.id === todoId)
// lists.value.splice(idx, 1)
emit('remove', idx)
} catch (e) {
console.log(e)
}
}
const updateTodoId = ref(-1) // 当前点击修改的那一项
const updateTodoText = ref("") // 输入的新内容
function update(todoId, todoText) {
updateTodoId.value = todoId
updateTodoText.value = todoText
}
// 确认发光法, 就是直接找到那一项 将他替换内容即可
function sure() {
let currentId = updateTodoId.value // 先取出值在找索引
emit('sure', { id:currentId, text:updateTodoText.value })
cancel()
}
function cancel() {
updateTodoId.value = -1
updateTodoText.value = ""
}
</script>
封装服务相关的组件
- 示例:
-
src/App.vue
<template> <button @click="showToast">Toast显示</button> </template> <script setup> // 通过函数来渲染的组件,入口 (.vue文件) import toast from "./components/Toast" function showToast() { toast({ message: "显示提示框1", duration: 3000 }) } </script>
-
src/components/Toast/index.js
import ToastComponet from "./toast.vue" import { render, h } from "vue" export default function toast(options) { let message let duration if (typeof options === "object") { message = options.message duration = options.duration || 3000 } else { message = options duration = 3000 } // Vue.extend(toast).$mount() // 渲染器来渲染组件 render(toast,'...') const container = document.createElement("div") render( h( ToastComponet, { message, duration, onDestroy() { render(null, container) } }, { default: () => { return h("h1", "hello") } } ), container ) //这个函数的意思是在容器container中渲染一个组件,组件会有一个实例对象,实例对象中的虚拟节点对应的DOM元素会被插入到容器container中。而下一次如果再调用该render函数,则也是传入一个组件或null,下一个组件或null则会对应一个新的实例对象-同时该新实例对象会对应一个新虚拟节点,该新虚拟节点对应的DOM元素则会被插入到容器container中。在新容器container中新实例对象对应的新虚拟节点对应的DOM元素被插入到容器container时,旧实例对象被销毁,同时旧实例对象对应的旧虚拟节点对应的DOM也将被销毁并移除出DPM文档树中。 document.body.appendChild(container.firstElementChild) } // vue-router vuex
-
src/components/Toast/toast.vue
<template> <Transition name="fade" @after-leave="$emit('destroy')"> <div class="toast" v-show="show"> <slot>{{ message }}</slot> <div>{{ message }}</div> </div> </Transition> </template> <script setup> // enter-from (进入前的样式) 开始进入 enter-active 移除(enter-from) import { onMounted, onUnmounted, Transition, ref, useSlots } from "vue" const props = defineProps(["message", "duration"]) const emit = defineEmits(["destroy"]) const show = ref(false) onMounted(() => { show.value = true // 可以触发动画了 }) // 如何监控过渡效果的结束 const timer = setTimeout(() => { show.value = false }, props.duration) onUnmounted(() => { clearTimeout(timer) // 清理,有可能用户手动销毁组件内 }) </script> <style scoped> .toast { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); padding: 50px; background: rgba(0, 0, 0, 0.3); } .fade-enter-active, .fade-leave-active { transition: opacity 0.5s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; } </style>
-
生成虚拟节点
看下午的视频,约15分钟-弹框组件,看关于render(null, container)的内容。
- h函数与createVNode函数
- fang/f20230717/vue3-lan-1/node_modules/vue/dist/vue.esm-browser.js
function h(type, propsOrChildren, children) {
const l = arguments.length;
if (l === 2) {
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren]);
}
return createVNode(type, propsOrChildren);
} else {
return createVNode(type, null, propsOrChildren);
}
} else {
if (l > 3) {
children = Array.prototype.slice.call(arguments, 2);
} else if (l === 3 && isVNode(children)) {
children = [children];
}
return createVNode(type, propsOrChildren, children);
}
}
- render(null, container)这个函数的意思是在容器container中渲染一个组件,组件会有一个实例对象,实例对象中的虚拟节点对应的DOM元素会被插入到容器container中。而下一次如果再试用该render函数,则也是传入一个组件或null,下一个组件或null则会对应一个新的实例对象-同时该新实例对象会对应一个新虚拟节点,该新虚拟节点对应的DOM元素则会被插入到容器container中。在新容器container中新实例对象对应的新虚拟节点对应的DOM元素被插入到容器container时,旧实例对象被销毁,同时旧实例对象对应的旧虚拟节点对应的DOM也将被销毁并移除出DPM文档树中。
const container = document.createElement("div")//创建容器对象。
render(h(组件配置,),container) //在容器container中渲染一个组件,组件会有一个实例对象,实例对象中的虚拟节点对应的DOM元素会被插入到容器container中。
document.body.appendChild(container.firstElementChild)//把容器中的第一个子元素放到body中,但容器上会存在一个虚拟节点,该虚拟节点会对应该DOM元素,如果该虚拟节点被销毁,则该DOM也将被销毁。
render(null, container)//再调用该render函数,则也是传入一个组件或null,下一个组件或null则会对应一个新的实例对象-同时该新实例对象会对应一个新虚拟节点,该新虚拟节点对应的DOM元素则会被插入到容器container中。在新容器container中新实例对象对应的新虚拟节点对应的DOM元素被插入到容器container时,旧实例对象被销毁,同时旧实例对象对应的旧虚拟节点对应的DOM也将被销毁并移除出DPM文档树中。
过渡效果
-
<Transition>
是一个内置组件,这意味着它在任意别的组件中都可以被使用,无需注册。它可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上。进入或离开可以由以下的条件之一触发:- 由 v-if 所触发的切换
- 由 v-show 所触发的切换
- 由特殊元素
<component>
切换的动态组件 - 改变特殊的 key 属性
-
CSS过渡示例:
<button @click="show = !show">Toggle</button> <Transition> <p v-if="show">hello</p> </Transition>
/* 下面我们会解释这些 class 是做什么的 */ //进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。 .v-enter-from{ opacity: 0; } //进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。 .v-enter-active{ transition: opacity 0.5s ease; } //进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。 .v-enter-to{} //离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。 .v-leave-from{} //离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。 .v-leave-active { transition: opacity 0.5s ease; } //离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。 .v-leave-to { opacity: 0; }
如何监控过渡效果的结束
- 通过Transition组件内置的@after-leave事件来做。
vue3生态
vue-router
-
面试题
- hash 和 histroy区别 ?
- vue里面有三种模式 (hash模式 h5Api 抽象模式) 对于浏览器我们最常用的是前两种:
- hash 丑,hash后台是获取不到,不会像后台发起请求。 为什么需要服务端渲染 seo优化。 hash模式无法实现seo优化。 优点 刷新不会出现404
- history 漂亮, 能实现seo优化, 缺点会产生404问题
- 出现404如何解决呢? 如果没有对应的资源 直接返回首页;
- vue里面有三种模式 (hash模式 h5Api 抽象模式) 对于浏览器我们最常用的是前两种:
- hash 和 histroy区别 ?
-
后端在没有对应的资源时,直接返回首页,在后端返回首页之后,浏览器端的首页在渲染时,vue会根据当前的路径,再把首页渲染出来。
路由模式
- vue里面有三种模式 (hash模式 h5Api 抽象模式) 对于浏览器我们最常用的是前两种:
- hash 丑,hash后台是获取不到,不会像后台发起请求。 为什么需要服务端渲染 seo优化。 hash模式无法实现seo优化。 优点 刷新不会出现404
- history 漂亮, 能实现seo优化, 缺点会产生404问题
- 出现404如何解决呢? 如果没有对应的资源 直接返回首页
路由权限
- 实现权限?
- 访问权限 meta:{isLogin:true} 这种页面无论是否登录 都要被打包到想目中中
- 会将路由拆分成两部分 静态路由 动态路由 根据用户登录后动态添加路由 addRoute
生命周期
-
路由的导航守卫流程
- 完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫(2.5+)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
- 完整的导航解析流程
-
导航守卫执⾏流程
- 导航被触发。
- 在失活的组件⾥调⽤ beforeRouteLeave 守卫。
- 调⽤全局的 beforeEach 守卫。
- 在重⽤的组件⾥调⽤ beforeRouteUpdate 守卫(2.2+)。
- 在路由配置⾥调⽤ beforeEnter。
- 解析异步路由组件。
- 在被激活的组件⾥调⽤ beforeRouteEnter。
- 调⽤全局的 beforeResolve 守卫(2.5+)。
- 导航被确认。
- 调⽤全局的 afterEach 钩⼦。
- 触发 DOM 更新。
- 调⽤ beforeRouteEnter 守卫中传给 next 的回调函数,
- 创建好的组件实例会作为回调函数的参数传⼊。
跳转示例
-
src/router/index.js
import { createWebHistory, createMemoryHistory } from "vue-router" import { createRouter, createWebHashHistory } from "vue-router" import { h, Text } from "vue" const router = createRouter({ history: createWebHashHistory(), routes: [ { path: "/", component: () => import("../views/bar.vue") // meta 路由备注 // beforeEnter 路由钩子 // name 路由名 // children 路由儿子 // components :{} }, { path: "/foo", component: () => import("../views/foo.vue") // meta 路由备注 // beforeEnter 路由钩子 // name 路由名 // children 路由儿子 // components :{} }, { path: "/:pathMatch(.*)*", component: { render: () => h(Text, "not found") } } ] }) export default router
-
src/main.js
import { createApp } from 'vue' import App from './App.vue' import router from './router' const app = createApp(App) app.use(router) app.mount('#app')
核心:
import router from './router' app.use(router)
-
src/App.vue
<template> <router-link to="/"> <div>前往bar页面</div> </router-link> <router-link to="/foo"><div>前往foo页面</div> </router-link> <router-view></router-view> <button @click="forward()">123123</button> </template> <script setup> // 通过函数来渲染的组件,入口 (.vue文件) import { useRouter } from "vue-router" import { useRoute } from "vue-router" const router = useRouter() const routes = useRoute() function forward() { console.log(`router-->`, router) console.log(`routes-->`, routes) } </script>
-
src/views/bar.vue
<template> <h1>bar</h1> </template>
-
src/views/foo.vue
<template> <h1>foo</h1> </template>
vue-router4与vue-router3的对比
- vue2 中 用的是vue-router3。vue3 中用的是vue-router4 (只用h5的api)
- createRouter, createWebHistory() createHashHistory()
- this. r o u t e r 调用路由方法 t h i s . router 调用路由方法 this. router调用路由方法this.router.push() -> useRouter
- this. r o u t e 获取路由的属性 t h i s . route 获取路由的属性 this. route获取路由的属性this.route.params -> useRoutes
- 404 api 有差异
pinia
vuex
-
选项式
import { createStore, createLogger } from "vuex" const store = createStore({})
-
组合式
import { useStore } from "vuex" const store = useStore()
pinia与vuex的区别
-
vuex缺陷?
-
commit和dispatch 到底用哪一个? (mutation 和action区别) 修改状态交给mutation来操作,action 异步请求的封装 (可以复用异步的逻辑),有一个操作我们没有复用逻辑是不是就不需要action了? (必须经过action -》 mutation)里去。 pinia不在有mutation,只用action
-
树结构, 结构层次深.状态调用会变得冗余. 模块和状态命名冲突问题, 模块之间为了进行分割,而且每个模块要增加 namespaced:true属性
-
vuex提供了很多options的写法 createNamespaceHelpers(), mapState,mapAction mapMutations
-
store只能有一个, 而且都是optionsAPI
-
vuex不支持ts 4.x 只是将代码增加了类型,提示非常不友好, pinia 是未来的vuex
const store = new Vuex.Store({ state: { a: 1 }, modules: { a: { namespaced: true, modules: { a: { namespaced: true } } } } }) store.state.a
-
-
pinia 的好处
- 没有mutation了
- 可以有多个store (每个store都是独立的,可以相互调用)
- 支持ts,告别了树结构
- 支持optionsAPI 以及compositionAPI
pinia的使用
-
安装下载pinia插件
npm install pinia
-
三步曲
-
src/main.js 入口文件:
import { createApp } from 'vue' import App from './App.vue' import { createPinia } from 'pinia' const pinia = createPinia(); // createRouter const app = createApp(App) // pinia.use(function ({ store }) { // store.$state = JSON.parse(localStorage.getItem(store.$id) || '{}') // store.$subscribe(function ({storeId},state) { // 状态修改就触发 // localStorage.setItem(storeId, JSON.stringify(state)) // }) // }) // 以前的Vue写法 都改成app.xxx app.use(pinia); // 使用pinia app.mount('#app') // createPinia , defineStore // sessionStorage 和 localStrage的区别? 有没有场景 只能放到sessionStorage // 如果用local存储会有个问题,就是一直存储就是存储的最后一个,之前的一刷新就好变成最后一个 // 针对当前的会话场景 只能用session // 周五 周六讲vue3项目 pinia + router + 高端写法 都用上
核心代码:
import { createPinia } from 'pinia' const pinia = createPinia(); // createRouter app.use(pinia);
-
-
创建一个仓库
-
src/stores/counter.js
// import { defineStore } from "pinia" // 定义一个store // 为什么data是一个函数,为了保证每个组件的数据是唯一的 // vue2的vuex原理是什么? new Vue() // vue3里的pinia如何实现 reactive() + computed() /* import { ref, computed } from "vue" export const useCounter = defineStore("main", () => { const count = ref(0) const double = computed(() => { return count.value * 2 }) const increment = () => { console.log(`选项式-setup函数`) count.value++ } const decrement = () => { count.value-- } return { count, double, increment, decrement } }) */ import { defineStore } from "pinia" // 定义一个store export const useCounter = defineStore("main", { // vuex state: () => { return { count: 0 } }, getters: { double() { return this.count * 2 } }, actions: { increment() { console.log(`选项式-类vuex`) this.count++ }, decrement() { this.count-- } } }) // function setup() { // const count = ref(0); // const double = computed(() => { // return count.value * 2 // }) // const increment = () => { // count.value++ // } // const decrement = () => { // count.value-- // } // return { // count, // double, // increment, // decrement // } // }
核心代码:
import { defineStore } from "pinia" // 定义一个store export const useCounter = defineStore("main", { // vuex state: () => { return { count: 0 } }, getters: { double() { return this.count * 2 } }, actions: { increment() { console.log(`选项式-类vuex`) this.count++ }, decrement() { this.count-- } } })
-
-
组件中调用:
-
src/App.vue
<template> <div>counterStore.count - {{ counterStore.count }}</div> <div>counterStore.double - {{ counterStore.double }}</div> <button @click="counterStore.increment()">累加</button> <button @click="counterStore.decrement()">累减</button> </template> <script setup> // 通过函数来渲染的组件,入口 (.vue文件) import { useCounter } from "./stores/counter" const counterStore = useCounter() </script>
-
state为一个函数
- 为什么data是一个函数,为了保证每个组件的数据是唯一的。
vuex原理与pinia原理
- vue2的vuex原理是什么? new Vue()
- vue3里的pinia如何实现 reactive() + computed()
pinia的选项式
- src/stores/counter.js
import { defineStore } from "pinia" // 定义一个store
export const useCounter = defineStore("main", {
// vuex
state: () => {
return {
count: 0
}
},
getters: {
double() {
return this.count * 2
}
},
actions: {
increment() {
console.log(`pinia的选项式-类vuex`)
this.count++
},
decrement() {
this.count--
}
}
})
pinia的组合式-setup函数
- src/stores/counter.js
import { defineStore } from "pinia" // 定义一个store
import { ref, computed } from "vue"
export const useCounter = defineStore("main", () => {
const count = ref(0)
const double = computed(() => {
return count.value * 2
})
const increment = () => {
console.log(`pinia的组合式-setup函数`)
count.value++
}
const decrement = () => {
count.value--
}
return {
count,
double,
increment,
decrement
}
})
pinia的持久化存储
-
可以自己实现,也可以用官方的插件。
-
自己实现的思路是,每次数据变动,初始化时,调用pinia.use(),会从本地存储中取值并赋值给对应的pinia仓库。而之后可通过$subscribe()在pinia的值变动时,把最新的值存储到本地存储中。
import { createApp } from "vue" import App from "./App.vue" import { createPinia } from "pinia" const pinia = createPinia() // createRouter const app = createApp(App) pinia.use(function ({ store }) { console.log(`初始化-要从本地存储中取到值并填充到pinia仓库中` ); store.$state = JSON.parse(localStorage.getItem(store.$id) || "{}") store.$subscribe(function ({ storeId }, state) { console.log(`pinia仓库中的数据更新,取一遍pinia仓库中的值保存到本地存储中`) // 状态修改就触发 localStorage.setItem(storeId, JSON.stringify(state)) }) }) app.use(pinia) // 使用pinia app.mount("#app")
自己实现pinia的持久化存储
-
核心代码:
pinia.use(function ({ store }) { store.$state = JSON.parse(localStorage.getItem(store.$id) || "{}") store.$subscribe(function ({ storeId }, state) { // 状态修改就触发 localStorage.setItem(storeId, JSON.stringify(state)) }) })
-
示例代码:
-
src/main.js
import { createApp } from "vue" import App from "./App.vue" import { createPinia } from "pinia" const pinia = createPinia() // createRouter const app = createApp(App) pinia.use(function ({ store }) { store.$state = JSON.parse(localStorage.getItem(store.$id) || "{}") store.$subscribe(function ({ storeId }, state) { // 状态修改就触发 localStorage.setItem(storeId, JSON.stringify(state)) }) }) app.use(pinia) // 使用pinia app.mount("#app") // createPinia , defineStore // sessionStorage 和 localStrage的区别? 有没有场景 只能放到sessionStorage // 如果用local存储会有个问题,就是一直存储就是存储的最后一个,之前的一刷新就好变成最后一个 // 针对当前的会话场景 只能用session // 周五 周六讲vue3项目 pinia + router + 高端写法 都用上
核心代码:
pinia.use(function ({ store }) { store.$state = JSON.parse(localStorage.getItem(store.$id) || "{}") store.$subscribe(function ({ storeId }, state) { // 状态修改就触发 localStorage.setItem(storeId, JSON.stringify(state)) }) })
-
src/stores/counter.js
import { defineStore } from "pinia" // 定义一个store import { ref, computed } from "vue" export const useCounter = defineStore("main", () => { const count = ref(0) const double = computed(() => { return count.value * 2 }) const increment = () => { console.log(`pinia的组合式-setup函数`) count.value++ } const decrement = () => { count.value-- } return { count, double, increment, decrement } })
-
src/App.vue
<template> <div>counterStore.count - {{ counterStore.count }}</div> <div>counterStore.double - {{ counterStore.double }}</div> <button @click="counterStore.increment()">累加</button> <button @click="counterStore.decrement()">累减</button> </template> <script setup> // 通过函数来渲染的组件,入口 (.vue文件) import { useCounter } from "./stores/counter" const counterStore = useCounter() </script>
-
sessionStorage和localStrage
- sessionStorage 和 localStrage的区别?
- 有没有场景 只能放到sessionStorage中?
- 在本地页面中,如果打开一个同网页新的窗口下级如小说列表页进入小说章节页,并对窗口中的内容单纯地用一个字段来进行存储。如果用local存储会有个问题,就是一直存储就是存储的最后一个页面的内容,之前的一刷新就好变成最后一个页面的内容。针对当前的会话场景 只能用session。
- 有没有场景 只能放到sessionStorage中?
pinia的持久化存储官方插件
-
具体步骤
-
用你喜欢的包管理器安装依赖:
npm i pinia-plugin-persistedstate
-
将插件添加到 pinia 实例上
import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(piniaPluginPersistedstate)
-
创建 Store 时,将 persist 选项设置为 true。
-
使用选项式 Store 语法:
import { defineStore } from 'pinia' export const useStore = defineStore('main', { state: () => { return { someState: '你好 pinia', } }, persist: true, })
-
核心:
import { defineStore } from 'pinia' export const useStore = defineStore('main', { state: ..., persist: true,//核心; })
-
-
或者使用组合式 Store 语法:
import { defineStore } from 'pinia' export const useStore = defineStore( 'main', () => { const someState = ref('你好 pinia') return { someState } }, { persist: true, } )
-
核心:
import { defineStore } from 'pinia' export const useStore = defineStore('main',() => { ... },{ persist: true,//核心属性; })
-
-
-