1、vue2响应式原理?
源码:
let obj = { name: 'poetry', age: 20 };
class Observer {
// 观测值
constructor(value) {
this.walk(value);
}
walk(data) {
// 对象上的所有属性依次进行观测
let keys = Object.keys(data);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = data[key];
defineReactive(data, key, value);
}
}
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(obj, key, value) {
// 创建一个dep
let dep = new Dep();
// 递归观察子属性
observe(value);
Object.defineProperty(obj, key, {
get() { // 收集对应的key 在哪个方法(组件)中被使用
if (Dep.target) { // watcher
dep.depend(); // 这里会建立 dep 和watcher的关系
}
return value;
},
set(newValue) {
if (newValue !== value) {
observer(newValue);
value = newValue; // 让key对应的方法(组件重新渲染)重新执行
dep.notify()
}
}
})
}
export function observe(value) {
// 如果传过来的是对象或者数组 进行属性劫持
if (
Object.prototype.toString.call(value) === "[object Object]" ||
Array.isArray(value)
) {
return new Observer(value);
}
}
class Dep {
constructor() {
this.subs = [] // subs [watcher]
}
depend() {
this.subs.push(Dep.target)
}
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
Dep.target = null;
observer(obj); // 响应式属性劫持
// 依赖收集 所有属性都会增加一个dep属性,
// 当渲染的时候取值了 ,这个dep属性 就会将渲染的watcher收集起来
// 数据更新 会让watcher重新执行
// 观察者模式
// 渲染组件时 会创建watcher
class Watcher {
constructor(render) {
this.get();
}
get() {
Dep.target = this;
render(); // 执行render
Dep.target = null;
}
update() {
this.get();
}
}
const render = () => {
console.log(obj.name); // obj.name => get方法
}
// 组件是watcher、计算属性是watcher
const watcher = new Watcher(render);
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
Watcher.update() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 模拟数据获取,触发getter
obj.name = 'poetries'
// 一个属性一个dep,一个属性可以对应多个watcher(一个属性可以在任何组件中使用、在多个组件中使用)
// 一个dep 对应多个watcher
// 一个watcher 对应多个dep (一个视图对应多个属性)
// dep 和 watcher是多对多的关系
2、vue3响应式原理?
//定义一个 reactive 函数,用于将普通对象转换为响应式对象
function reactive(obj) {
// 创建一个 WeakMap,用于存储原始对象和代理对象的映射关系
const weakMap = new WeakMap();
// 定义一个代理处理程序
const handler = {
get(target, key) {
// 获取原始对象的属性值
const value = Reflect.get(target, key);
// 如果属性值是对象,则递归调用 reactive 函数
if (typeof value === 'object' && value !== null) {
if (!weakMap.has(value)) {
weakMap.set(value, reactive(value));
}
return weakMap.get(value);
}
// 返回属性值
return value;
},
set(target, key, value) {
// 设置原始对象的属性值
const oldValue = Reflect.get(target, key);
if (oldValue !== value) {
Reflect.set(target, key, value);
// 触发更新
trigger(target, key, oldValue, value);
}
},
};
// 使用 Proxy 对象创建代理对象
return new Proxy(obj, handler);
}
// 定义一个 effect 函数,用于收集依赖
function effect(fn) {
const effectStack = [];
function runEffect() {
// 保存当前 effect 到栈中
effectStack.push(runEffect);
// 执行传入的函数
fn();
// 移除当前 effect 从栈中
effectStack.pop();
}
// 立即执行 effect
runEffect();
}
// 定义一个 trigger 函数,用于触发更新
function trigger(target, key, oldValue, newValue) {
// 遍历所有依赖,执行 effect 函数
for (const effect of effectStack) {
effect();
}
}
3、nextTick的原理?
定义:
nextTick是表示下一次dom更新结束之后延迟执行回调
作用:
Vue有个异步更新策略,意思是如果数据变化,Vue不会立刻更新DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick
使用场景:
DOM 更新后立即执行代码、获取dom元素的总数量等等
实现原理:
1、首先定义回调函数执行的数组以及判断函数执行的状态
callbacks数组以及pending状态
2、然后定义事件处理函数,flushCallbacks,使用for循环依次调用callbacks数组内的每一项并执行
3、定义timerFunc异步方法,采用优雅降级判断执行
4、使用多层判断进行优雅降级
1.判断是否支持promise使用isNative判断浏览器是否支持
2、使用isNative方法判断是否支持MutationObserver
MutationObserver是在dom更新完毕之后执行
设置状态并创建一个文本节点,执行过程中判断1和0的状态,总是在0和1状态中变换依旧保持执行阶段
3、判断是否支持setImmediate
4、如果都不支持的话就执行setTimeout
源码:
// src/core/utils/nextTick
let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false; //把标志还原为false
// 依次执行回调
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
let timerFunc; //定义异步方法 采用优雅降级
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是监听dom变化 也是一个异步方法
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") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降级采用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb) {
// 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组
callbacks.push(cb);
if (!pending) {
// 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false
pending = true;
timerFunc();
}
}
4、keepalive的原理?
定义:
keep-alive是vue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM
keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们
作用:
keep-alive用于缓存组件状态,避免组件的反复渲染,减少浏览器的重排和重绘;
属性:
-
- include - 字符串或正则表达式。只有名称匹配的组件会被缓存
- exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存
- max - 数字。最多可以缓存多少组件实例
实现原理:
1、首先在created生命周期内创建缓存对象以及缓存组件的keys集合
2、然后在执行render函数进行组件的渲染
1.获取默认组件 this.$slots.default ;默认缓存第一个子组件
2.把include和exclude结构出来然后判断是否要进行缓存
如果没有缓存返回当前虚拟节点
如果有缓存接着解构缓存对象cache以及key集合,使用cache[key]判断是否在缓存集合中,然后将原有缓存删除并且将新的缓存添加至末尾;如果没有缓存就直接缓存下来,最后进行判断max是否存在,存在即 将下标为0的删除并且添加到最后一项;
3、在mounted里监听include和exclude在该函数内部进行遍历缓存对象(cache),取出每一项的值并与新的缓存规则进行匹配,匹配不上则被删除;
源码:
export default {
name: "keep-alive",
abstract: true, //抽象组件
props: {
include: patternTypes, //要缓存的组件
exclude: patternTypes, //要排除的组件
max: [String, Number], //最大缓存数
},
created() {
this.cache = Object.create(null); //缓存对象 {a:vNode,b:vNode}
this.keys = []; //缓存组件的key集合 [a,b]
},
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys);
}
},
mounted() {
//动态监听include exclude
this.$watch("include", (val) => {
pruneCache(this, (name) => matches(val, name));
});
this.$watch("exclude", (val) => {
pruneCache(this, (name) => !matches(val, name));
});
},
render() {
const slot = this.$slots.default; //获取包裹的插槽默认值 获取默认插槽中的第一个组件节点
const vnode: VNode = getFirstComponentChild(slot); //获取第一个子组件
// 获取该组件节点的componentOptions
const componentOptions: ?VNodeComponentOptions =
vnode && vnode.componentOptions;
if (componentOptions) {
// 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag
const name: ?string = getComponentName(componentOptions);
const { include, exclude } = this;
// 不走缓存 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode
if (
// not included 不包含
(include && (!name || !matches(include, name))) ||
// excluded 排除里面
(exclude && name && matches(exclude, name))
) {
//返回虚拟节点
return vnode;
}
const { cache, keys } = this;
// 获取组件的key值
const key: ?string =
vnode.key == null
? // same constructor may get registered as different local components
// so cid alone is not enough (#3269)
componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : "")
: vnode.key;
// 拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存
if (cache[key]) {
//通过key 找到缓存 获取实例
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove(keys, key); //通过LRU算法把数组里面的key删掉
keys.push(key); //把它放在数组末尾
} else {
cache[key] = vnode; //没找到就换存下来
keys.push(key); //把它放在数组末尾
// prune oldest entry //如果超过最大值就把数组第0项删掉
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
vnode.data.keepAlive = true; //标记虚拟节点已经被缓存
}
// 返回虚拟节点
return vnode || (slot && slot[0]);
},
};
5、vue-router的原理?
定义:
Vue Router是Vue.js官方提供的路由管理库,用于实现前端路由。它通过改变URL来实现不同页面之间的切换,同时还提供了导航守卫、动态路由、嵌套路由等功能。路由分为hash路由和history路由
hash路由:
1.不美观 由端口号+/#/表示
2.可以通过location.hash获取hash值并且可以通过浏览器监听onHashChange事件来进行路由跳转/切换
3.hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制 hash 的切换
history路由:
1.通过popstate方法来监听路由的变化,
2.pushState 和 repalceState 两个 API 来操作实现 URL的切换
实现原理:
hash路由实现
定义插件并实现全局组件:Router类并挂载实例,然后实现router-link(跳转)和router-view(占位/渲染)组件
vuerouter插件/install全局插件:
-
-
-
- 在constructor构造函数中缓存path和route的映射关系;
-
-
-
-
-
-
- 定义一个响应式的current ,vue.util.defineReactive(this,'current',' ') ;
- 然后监听hashchange和load(页面刷新)事件,事件处理函数都为this.onHashChange.bind(this)修改onHashChange的this指向
- onHashchange事件处理函数内保存了当前的路由信息('/'或者当前hash值的#后的信息)
-
-
-
-
-
-
- install使用mixin混入将this.$options.router挂载到vue原型上;然后在进行注册router-link和router-view全局组件
-
-
-
-
-
-
- router-link内部为跳转到指定路由
- router-view
-
-
-
-
-
-
-
-
- 内部先创建要渲染的组件component
- 将路由映射对象和当前路由进行判断是否存在,存在则保存当前对应的组件
- 最后return返回 当前组件
-
-
-
-
源码:
// 我们的插件:
// 1.实现一个Router类并挂载期实例
// 2.实现两个全局组件router-link和router-view
let Vue;
class VueRouter {
// 核心任务:
// 1.监听url变化
constructor(options) {
this.$options = options;
// 缓存path和route映射关系
// 这样找组件更快
this.routeMap = {}
this.$options.routes.forEach(route => {
this.routeMap[route.path] = route
})
// 数据响应式
// 定义一个响应式的current,则如果他变了,那么使用它的组件会rerender
Vue.util.defineReactive(this, 'current', '')
// 请确保onHashChange中this指向当前实例
window.addEventListener('hashchange', this.onHashChange.bind(this))
window.addEventListener('load', this.onHashChange.bind(this))
}
onHashChange() {
// console.log(window.location.hash);
this.current = window.location.hash.slice(1) || '/'
}
}
// 插件需要实现install方法
// 接收一个参数,Vue构造函数,主要用于数据响应式
VueRouter.install = function (_Vue) {
// 保存Vue构造函数在VueRouter中使用
Vue = _Vue
// 任务1:使用混入来做router挂载这件事情
Vue.mixin({
beforeCreate() {
// 只有根实例才有router选项
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
})
// 任务2:实现两个全局组件
// router-link: 生成一个a标签,在url后面添加#
// <a href="#/about">aaaa</a>
// <router-link to="/about">aaa</router-link>
Vue.component('router-link', {
props: {
to: {
type: String,
required: true
},
},
render(h) {
// h(tag, props, children)
return h('a',
{ attrs: { href: '#' + this.to } },
this.$slots.default
)
// 使用jsx
// return <a href={'#'+this.to}>{this.$slots.default}</a>
}
})
Vue.component('router-view', {
render(h) {
// 根据current获取组件并render
// current怎么获取?
// console.log('render',this.$router.current);
// 获取要渲染的组件
let component = null
const { routeMap, current } = this.$router
if (routeMap[current]) {
component = routeMap[current].component
}
return h(component)
}
})
}
export default VueRouter
6、vuex的原理?
定义:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
五大核心属性:
-
-
- state:基本数据(数据源存放地)
- getters:从基本数据派生出来的数据;
- mutations:提交更改数据的方法,同步
- action:像一个装饰器,包裹mutations,使之可以异步
- modules:模块化Vuex
-
优/缺点:
统一管理数据;
如果使用不当就会造成全局数据紊乱;
实现原理:
使用mixin混入将this.options.$store方法挂载到vue原型上(vue.prototype.$store)
1.首先定义响应式的state
****options 为 this.$store**
data:{ $$state:options.state }
2.将mutations和actions挂载到this上,指定commit和dispatch的this执行,都为this
3.使用get/set state()将state变为只读形式不可以直接被修改.
4.定义commit和dispatch方法,实现思路基本一样只不过dispatch执行时最好在内部写一个定时器
commit :接收两个参数,第一个参数为type(函数名称)第二个参数为payload(传递的参数); 使用this._mutations[type]判断是否存在,不存在抛出错误,存在则执行回调并且传入两个参数(this.state,payload)
dispatch::接收两个参数,第一个参数为type(函数名称)第二个参数为payload(传递的参数); 使用this._actions[type]判断是否存在,不存在抛出错误,存在则执行回调并且传入两个参数(this,payload)
源码:
// 目标1:实现Store类,管理state(响应式的),commit方法和dispatch方法
// 目标2:封装一个插件,使用更容易使用
let Vue;
class Store {
constructor(options) {
// 定义响应式的state
// this.$store.state.xx
// 借鸡生蛋
this._vm = new Vue({
data: {
$$state: options.state
}
})
this._mutations = options.mutations
this._actions = options.actions
// 绑定this指向
this.commit = this.commit.bind(this)
this.dispatch = this.dispatch.bind(this)
}
// 只读
get state() {
return this._vm._data.$$state
}
set state(val) {
console.error('不能直接赋值呀,请换别的方式!!天王盖地虎!!');
}
// 实现commit方法,可以修改state
commit(type, payload) {
// 拿出mutations中的处理函数执行它
const entry = this._mutations[type]
if (!entry) {
console.error('未知mutaion类型');
return
}
entry(this.state, payload)
}
dispatch(type, payload) {
const entry = this._actions[type]
if (!entry) {
console.error('未知action类型');
return
}
// 上下文可以传递当前store实例进去即可
entry(this, payload)
}
}
function install(_Vue){
Vue = _Vue
// 混入store实例
Vue.mixin({
beforeCreate() {
if (this.$options.store) {
Vue.prototype.$store = this.$options.store
}
}
})
}
// { Store, install }相当于Vuex
// 它必须实现install方法
export default { Store, install }
7、Pinia的原理?
pinia是vue3用来实现全局状态管理;
相比于vuex来说pinia使用起来比较灵活;
pinia中只有state、getters和actions;
state是一个回调函数。用于存放数据;
actions同时支持异步和同步任务;
getters:计算属性用于计算和获取state的数据
源码:
// /store/index.js
import createPinia from "./createPinia";
import defineStore from "./defineStore";
export { createPinia, defineStore }
// main.js
import {createApp} from 'vue'
import {createPinia} from './store/index.js'
let app = createApp()
app.use(createPinia())
//store/useStore.js
import {defineStore} from 'pinia';
export const useStore = defineStore('main',{
state: () => ({
count: 0,
}),
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
},
})
// todolist.vue
<template>
<div>12456--{{ store.todos }}</div>
<button @click="store.addTodo('1234')">++++</button>
</template>
<script setup>
import { ref, reactive } from 'vue';
import useStore from '../stores/index.js';
let store = useStore();
</script>
// /store/createPinia.js
import { reactive } from 'vue'; // 导入reactive函数,用于创建响应式对象
import patch from './apis'; // 导入patch模块
export default () => {
const piniaStore = reactive({}); // 创建一个响应式对象piniaStore
function setSubStore(name, store) { // 定义一个setSubStore函数,用于设置子存储
if (!piniaStore[name]) { // 如果piniaStore中不存在该名称的子存储
piniaStore[name] = store; // 将store赋值给piniaStore的name属性
piniaStore[name].$patch = patch;
// 将patch赋值给piniaStore的name属性的$patch属性
}
return piniaStore // 返回piniaStore
};
function install(app) { // 定义一个install函数,用于安装插件
// 安装插件
app.provide('setSubStore', setSubStore);
// 将setSubStore函数提供给app的provide方法向全局注入
};
return {
install, // 返回install函数
}
}
// /store/defineStore.js
import { reactive, toRef, computed, inject } from 'vue'; // 导入Vue相关函数
// 导出一个函数,接收三个参数:name, state, actions, getters
export default function (name, { state, actions, getters }) {
let store = {}; // 定义一个空对象store
// 定义state
if (state && typeof state === 'function') { // 如果state存在且是一个函数
const _state = state() // 调用state函数并将结果赋值给_state
store.$state = reactive(_state)
// 将_state转换为响应式对象并赋值给store的$state属性
for (let key in _state) { // 遍历_state的属性
store[key] = toRef(store.$state, key);
// 将store.$state的属性转换为ref对象并赋值给store的同名属性
}
}
if (actions && Object.keys(actions).length > 0) { // 如果actions存在且有属性
for (let method in actions) { // 遍历actions的属性
store[method] = actions[method] // 将actions的属性赋值给store的同名属性
}
}
// 如果getters存在且有属性
if (getters && Object.keys(getters).length > 0) {
// 遍历getters的属性
for (let key in getters) {
// 将getters的属性转换为计算属性并赋值给store的同名属性
store[key] = computed(() => getters[key].call(store.$state))
// 将store的计算属性赋值给store.$state的同名属性
store.$state[key] = store[key]
}
}
console.log(store) // 打印store对象
return () => { // 返回一个函数
// 使用inject获取全局属性
const setSubStore = inject('setSubStore');
// 调用setSubStore函数并传入name和store,将返回值赋值给piniaStore
const piniaStore = setSubStore(name, store)
// 返回piniaStore的name属性
return piniaStore[name];
};
}