
vue2 的生命周期
new Vue() 之后触发的第一个钩子,当前阶段 data、methods等都无法访问vue3 的生命周期
区别
comouted
vue 初次运行会对 computed 属性做初始化处理(initComputed),初始化的时候会对每一个 computed 属性用 watcher 包装起来 ,这里面会生成一个 dirty 属性值为 true;然后执行 defineComputed 函数来计算,计算之后会将 dirty 值变为 false,这里会根据 dirty 值来判断是否需要重新计算;如果属性依赖的数据发生变化,computed 的 watcher 会把 dirty 变为 true,这样就会重新计算 computed 属性的值
插槽允许你在组件的模板中定义占位符,并在使用组件时填充这些占位符,它提供了一种将内容分发到组件的方式
基于前端路由的概念,通过拦截浏览器URL变化,动态加载和渲染对应的组件,从而实现页面的无刷新切换。它利用了浏览器的 History API 或 hash 来管理 URL 的变化,并通过 Vue.js 的响应式系统和组件化机制,将路由与组件进行了深度集成
vue-router路由工作流程
<router-view> 组件进行渲染<router-view> 组件作为占位符,用于渲染匹配到的组件。<router-view> 组件vue2中通过拦截数组的原型方法来实现数组变化的检测和响应式更新(push、pop、shift、unshift、splice、sort、reverse),当你调用这些方法时,vue2会拦截这些方法的调用,并在方法执行前进行额外操作,以实现数组变化的检测和响应式更新
// 1. 创建一个用于存储订阅者的类
class Dep {
constructor() {
this.subs = [];
}
// 添加订阅者
addSub(sub) {
this.subs.push(sub);
}
// 通知订阅者更新
notify() {
this.subs.forEach(sub => sub.update());
}
}
// 2. 创建一个Watcher类,用于观察数据变化并更新视图
class Watcher {
constructor(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get(); // 初始化时获取一次值
}
// 获取当前属性的值
get() {
Dep.target = this; // 将当前watcher设为Dep的target
let value = this.vm[this.exp]; // 触发getter,添加订阅者
Dep.target = null; // 清除target
return value;
}
// 更新视图
update() {
let newValue = this.vm[this.exp];
if (newValue !== this.value) {
this.value = newValue;
this.cb(newValue);
}
}
}
// 3. 创建一个Vue实例类
class Vue {
constructor(data) {
this.data = data;
Object.keys(data).forEach(key => {
this[key] = this._proxyData(key);
});
this._initWatch();
}
// 初始化watcher
_initWatch() {
this._watchers = [];
let updateComponent = () => {
console.log('组件更新');
};
Object.keys(this.data).forEach(key => {
new Watcher(this, key, updateComponent);
});
}
// 数据代理,用于实现双向绑定
_proxyData(key) {
let self = this;
return new Proxy(this.data[key], {
get(target, prop) {
if (Dep.target) {
let dep = target.__dep__ || (target.__dep__ = new Dep());
dep.addSub(Dep.target);
}
return Reflect.get(target, prop);
},
set(target, prop, value) {
let result = Reflect.set(target, prop, value);
let dep = target.__dep__;
if (dep) {
dep.notify();
}
return result;
}
});
}
}
// 使用示例
let vm = new Vue({
data: {
message: 'Hello, Vue!'
}
});
// 在控制台输出message属性的变化
vm.$watch('message', (newVal, oldVal) => {
console.log(`Message changed from ${oldVal} to ${newVal}`);
});
// 修改message属性,视图和模型都会自动更新
vm.message = 'Hello, World!';组合式API,提供了一种新的方式来组织组件的逻辑。在组合API中,生命周期钩子函数以on开头,如onMounted,onUpdated等,这些钩子函数可以在setup函数中直接使用
选项API和组合API可以混合使用,建议使用组合API,提供了更好的逻辑复用和代码组织方式
因为setuo是在组件初始化阶段运行的,此时组件实例还未被创建,因此也就不存在this对象
优势
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse () {
const x = ref(0)
const y = ref(0)
function update (e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update);
})
return { x, y }
}import { ref } from 'vue'
export function useAsyncData(url) {
const data = ref(null)
const error = ref(null)
fetch(url).then(response => response.json()).then(json => (data.value = json)).catch(err => (error.value = err))
return {
data,
error
}
}在vue2中,响应式系统是基于Object.defineProperty实现的,它通过递归地遍历数据对象,为每个属性添加getter和setter,以实现数据的响应式,然而这种方式用一些局限性,例如无法检测对象属性的添加或删除,以及数组索引的变化
vue3中,采用一种全新的响应式系统,基于ES6的 Proxy和Reflect API,提供更加灵活和高效的响应式机制
Proxy 只会代理对象第一层,vue3如何处理这个问题的
对于嵌套的对象属性,Vue3采用了一种称为 深度响应式 机制,处理这个问题。也就是当一个响应式对象的属性也是一个对象,vue3会递归的将该对象转为响应式对象。这意味着只有在实际访问嵌套对象的属性时,才会对该嵌套对象进行代理
Vue 3 对虚拟 DOM 的 diff 算法进行了重大改进和优化,以提高性能并支持更多的场景。以下是 Vue 3 diff 算法升级的主要变化
Teleport 是 Vue 3 引入的一个新特性,它允许我们将组件的一部分模板"传送"到 DOM 中的其他位置,而不受组件层级的限制。这在处理模态框、弹出框、通知等场景时非常有用,因为这些元素通常需要放置在 DOM 结构的特定位置,以确保正确的样式和行为
<template>
<div>
<h1>组件内容</h1>
<Teleport to="body">
<div class="modal">
<h2>模态框内容</h2>
<button @click="closeModal">关闭</button>
</div>
</Teleport>
</div>
</template>
<script>
export default {
methods: {
closeModal() {
// 关闭模态框的逻辑
}
}
}
</script>在上面的示例中,我们使用了 <Teleport> 组件,并通过 to 属性指定了目标位置为 "body"。这意味着 <Teleport> 内部的模板内容将被传送到 <body> 标签下,而不是在当前组件的 DOM 结构中
特点和优势
Suspense 是 Vue 3 引入的一个新特性,它允许在组件树中协调对异步依赖的处理,并在等待异步组件时渲染一个加载状态
Suspense 组件有两个插槽:default 和 fallback。default 插槽用于渲染主要内容,而 fallback 插槽用于在主要内容加载完成之前渲染一个加载状态。
使用场景:
<template>
<Suspense>
<template #default>
<async-component />
</template>
<template #fallback>
<loading-spinner />
</template>
</Suspense>
</template>
<script>
import AsyncComponent from './AsyncComponent.vue';
import LoadingSpinner from './LoadingSpinner.vue';
export default {
components: {
AsyncComponent,
LoadingSpinner,
},
};
</script>在vue2中,每个组件都必须有一个根元素,即template中只能有一个根节点,这就会导致:有时候我们可能不需要一个额外的根节点,但为了满足根节点要求,不得不添加一个无意义的根节点。为了解决这个问题:Vue3引入了Fragment概念,Fragment 允许组件的 template 中包含多个根级别的节点,而不需要将它们包裹在一个单独的元素中
Tree-Shaking 是一种在打包过程中移除未使用代码的技术,它通过静态分析代码的导入和导出语句,确定哪些代码是实际使用的,并将未使用的代码从最终的打包文件中剔除,从而减小打包体积
vue3 通过采用ES6 Module语法优化打包策略
import { ref, computed } from 'vue' 的方式,只导入 ref 和 computed 这两个响应式 APIimport('./MyComponent.vue') 的方式动态导入组件,而不是一次性导入所有组件事件监听缓存
vue3中,事件监听缓存是一种优化技术、用于提升事件处理性能。它通过缓存事件处理程序来避免每次渲染时都创建新的事件处理程序函数,从而减少不必要的内存分配和函数创建开销
vue2中,没当组件重新渲染时,事件处理程序都会被重新创建,即使事件处理程序内容没有变化。这会导致一些性能问题
vue3中引入了事件监听缓存功能,自动缓存事件处理程序,并在组件重新渲染时重用缓存的事件处理程序,而不是每次创建新的
原理
vue3 移除了一些不常用的API(extend、set、directive、filter、$on $off $once $children $listener $scopedSlots),最重要的是tree shaking。比如ref、computed等仅仅在用到的时候才会打包、没用到的模块都会被剔除掉
vue3中采用proxy重写了响应式系统,proxy可以对整个对象进行监听,包括监听动态属性的增加、数组索引和数组长度变化、监听删除属性
watch 和 watchEffect都是监听器,watchEffect是一个副作用函数
import { ref, watch, watchEffect } from 'vue';
const count = ref(0);
// 使用 watch
watch(count, (newValue, oldValue) => {
console.log(`Count changed from ${oldValue} to ${newValue}`);
});
// 使用 watchEffect
watchEffect(() => {
console.log(`Count is: ${count.value}`);
});
// 修改 count 的值
count.value++;vue complier 是将模板字符串编译成渲染函数的工具,他的主要作用是将模板template编译为渲染函数render function,以便在运行时通过渲染函数生成DOM树,并最终映射到真实的DOM元素上
function compile(template) {
// 1. 解析模板字符串,生成 AST
const ast = parse(template);
// 2. 优化 AST,标记静态节点
optimize(ast);
// 3. 生成渲染函数的代码字符串
const code = generate(ast);
// 4. 创建渲染函数
const render = new Function(`with(this){return ${code}}`);
return render;
}
function parse(template) {
// 实现解析逻辑,将模板字符串转换成 AST
// ...
}
function optimize(ast) {
// 实现优化逻辑,标记静态节点
// ...
}
function generate(ast) {
// 实现生成逻辑,将 AST 转换成渲染函数的代码字符串
// ...
}vue2 Diff 算法(采用了双端 Diff 算法:同时从新旧children的两端进行比较,借助key值找到可以复用的节点)
对比头头、尾尾、头尾、尾头是否可以复用,如果可以复用,就进行节点的更新和移动操作
如果经过四个端点的比较,都没有可复用的节点,则将
拿新的一组子节点的头部去 map 中查找,如果找到可复用的节点,则将相应的节点进行更新,并将其移动到头部,然后头部指针右移
然而,拿新的一组子节点的头部节点去旧的一组子节点中寻找可复用的节点,并非总能找到,这说明这个新的头部节点是新增节点,只需要将其挂载到头部即可
经过上述处理,最后还剩下新的节点就批量新增,剩下旧的节点就批量删除
同层级比较:Vue2 的 Diff 算法只会在同一层级进行比较,不会跨层级比较。这样可以大大减少比较的复杂度,提高性能
节点比较:比较两个节点时,如果类型不同,则直接删除旧节点,创建并插入新节点,如果类型相同,那么继续比较节点的属性和子节点
列表比较:在比较列表时,vue2 使用了一种叫双端比较的策略,首先同时从两个列表的头部和尾部开始比较,如果头部或尾部的节点相同,那么直接更新节点。如果头尾都不同,那么通过一个键值对的映射关系找到相同的节点,然后进行移动。这种策略可以有效地处理列表的顺序变化
子节点比较:在比较子节点时,如果新的子节点是文本节点,那么直接更新文本内容,如果新的子节点是数组,那么使用列表比较的策略进行比较
vue3 Diff 算法(在 vue2 的基础上进行了优化,主要改进在于引入了静态节点标记和块的概念)
在下次 DOM 更新循环结束之后执行延迟回调,它可以用来获取更新后的 DOM 状态,或者在数据变化之后等待 DOM 更新完成后执行某些操作
原理是利用了浏览器的事件循环机制和任务队列,当vue检测到数据变化时,他会开启一个异步更新队列,并缓冲在同一事件循环中所有数据变更,如果同一个watcher出发多次,只会被推入到队列中一次
let callbacks = []
let pending = []
function flushCallbacks () {
pending = false
const copied = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copied.length; i++) {
copied[i]()
}
}
let timerFunc;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
}
} else if (typeof MutationObserver !== 'undefined' && isNative(MutationObserver)) {
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick(cb, ctx) {
let _resolve;
callbacks.push(() => {
if (cb) {
cb.call(ctx);
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
pending = true;
timerFunc();
}
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve;
});
}
}keepalive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染 。也就是所谓的组件缓存; 实现原理:将被缓存的组件实例存储到一个缓存对象中,当需要重新渲染这个组件时,会从缓存中获取到之前的实例,将其重新挂载到 DOM 上
props
function pruneCacheEntry(cache, key, keys, current) {
const cached = cache[key];
/* 判断当前没有处于被渲染状态的组件,将其销毁*/
if (cached && (!current || current.tag !== cache.tag)) {
cached.componentInstance.$destroy();
}
cache[key] = null;
remove(keys.key);
}
// 在该函数内对this.cache对象进行遍历,取出每一项的name值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry函数将其从this.cache对象剔除即可
function pruneCache(keepAliveInstance, filter) {
const { cache, keys, _vnode } = keepAliveInstance;
for (const key in cache) {
const cachedNode = cache[key];
if (cachedNode) {
const name = getComponentName(cachedNode.componentOptions); // 获取组件名
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode);
}
}
}
}
export default {
name: "keep-alive",
abstract: true, // 判断当前组件虚拟dom是否渲染成真的dom
props: {
include: patternTypes,
exclude: patternTypes,
max: [Number, String]
},
create() {
this.cache = Object.create(null);
this.keys = [];
},
destroy() {
for (const key in this.cache) {
// 删除所有的缓存
pruneCacheEntry(this.cache, key, this.keys);
}
},
mounted() {
// 实时监听黑白名单的变动
this.$watch("include", val => {
pruneCache(this, name => matches(val, name));
});
this.$watch("exclude", val => {
pruneCache(this, name => !matches(val, name));
});
},
methods: {},
render() {
const slot = this.$slots.default;
const vnode = getFirstComponentChild(slot);
// 获取该组件节点的componentOptions
const componentOptions = vnode && vnode.componentOptions;
if (componentOptions) {
const name = getComponentName(componentOptions);
const { include, exclude } = this;
/* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
if (
(include && !name) ||
!matches(include, name) ||
(exclude && name && matches(exclude, name))
) {
return vnode;
}
const { cache, keys } = this;
const key =
vnode.key == null
? componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : "")
: vnode.key;
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
remove(keys, key);
keys.push(key);
} else {
cache[key] = vnode;
keys.push(key);
/* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
vnode.data.keepAlive = true;
}
return vnode || (slot && slot[0]);
}
};表单修饰符
事件修饰符
鼠标按钮修饰符
键盘修饰符
v-bind修饰符
生命周期和vue2有所不同
// vue3 实现一个节流自定义指令
<script setup>
function throttle(func, delay) {
let timeoutId;
return function (...args) {
if (!timeoutId) {
timeoutId = setTimeout(() => {
func.apply(this, args);
timeoutId = null;
}, delay);
}
};
}
const vThrottle = {
beforeMount(el, binding) {
console.log(binding)
if (typeof binding.value !== 'function') {
throw new Error(`v-throttle 的值必须是一个函数`);
}
const delay = binding.arg || 200
const throttleFunc = throttle(binding.value, delay)
el.addEventListener('click', throttleFunc)
},
onUnmounted(el) {
el.removeEventListener('click', throttledFunc);
}
}
</script>// vue3 实现一键复制功能vue3中已将过滤器filters移除了,官方建议使用计算属性或者方法来替代过滤器
vue2 filters源码分析
// 在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要通过parseFilters
_s(_f('filterformat')(message))
// _f全名是 resolveFilter,作用是从this.$options.filters中找到过滤器并返回
// _s全称是 toString 过滤器处理后的结果会当作参数传递给 toString函数,最终 toString函数执行后的结果会保存到Vnode中的text属性中,渲染到视图中
function resolveFilter(id) {
return resolveAsset(this.$options, 'filters', id, true)
}
function resolveAsset (options, type, id, warnMissing) {
if (typeof id !== 'string') {
return
}
const assets = options[type]
if (hasOwn(assets, id)) {
return assets[id]
}
const camelizedId = camelize(id)
if (hasOwn(assets, camelizedId)) {
return assets[camelizedId]
}
const PascalCaseId = capitalize(camelizedId)
if (hasOwn(assets, PascalCaseId)) {
return assets[PascalCaseId]
}
const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (process.env.NODE_ENV !== 'production' && warnMisssing && !result) {
console.log('Failed to resolve ' + type.slice(0,-1) + ': ' + id, options)
}
return result
}
function toString (value) {
return value == null ? '' : (
typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)
// JSON.stringify()第三个参数可用来控制字符串里面的间距
)
}
function paserFilters (filter) {
let filters = filter.split('|')
let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组
let i
if (filters) {
for(i = 0;i < filters.length;i++) {
experssion = warpFilter(expression,filters[i].trim())
}
}
return expression
}
function warpFilter(exp, filter) {
// 首先判断过滤器是否有其他参数
const i = filter.indexof('(')
if ( i<0 ) { // 不含其他参数,直接进行过滤器表达式字符串的拼接
return `_f("${filter}")(${exp})`
} else {
const name = filter.slice(0,i) // 过滤器名称
const args = filter.slice(i+1) // 参数,但还多了 ‘)’
return `_f('${name}')(${exp},${args}` // 注意这一步少给了一个 ')'
}
}Object.difineProperty 和 Proxy
vue2 双向绑定原理
当一个组件被初始化时,vue会对组件的data进行初始化,将普通的对象变成响应式对象,在这个过程中,Vue会进行依赖收集,以便在数据发生变化时,能够通知所有依赖这个数据的地方
Dep类是依赖收集的核心,主要作用是管理所有的watcher,Dep类中有一个静态属性target,它指向当前正在计算的watcher,保证了同一时间全局只有一个watcher被计算,Dep中还有一个subs树形,用来存储所有依赖这个Dep的watcher
_createVNode 函数做的事情
// 除了类型必填以外,其他的参数都是可选的
h("div");
h("div", { id: "foo" });
// attribute 和 property 都能在 prop 中书写
// Vue 会自动将它们分配到正确的位置
h("div", { class: "bar", innerHTML: "hello" });
// 像 `.prop` 和 `.attr` 这样的的属性修饰符
// 可以分别通过 `.` 和 `^` 前缀来添加
h("div", { ".name": "some-name", "^width": "100" });
// 类与样式可以像在模板中一样
// 用数组或对象的形式书写
h("div", { class: [foo, { bar }], style: { color: "red" } });
// 事件监听器应以 onXxx 的形式书写
h("div", { onClick: () => {} });
// children 可以是一个字符串
h("div", { id: "foo" }, "hello");
// 没有 props 时可以省略不写
h("div", "hello");
h("div", [h("span", "hello")]);
// children 数组可以同时包含 vnodes 与字符串
h("div", ["hello", h("span", "hello")]);