前言
在之前面试的时候我本人也常常会遇到一些vue原理的问题, 我也总结了下本人的常常的用到的,不便本人学习,明天也给大家分享进去, 欢送大家一起学习交换, 有更好的办法欢送评论区指出, 后序我也将继续整顿总结~
形容 Vue 与 React 区别
阐明概念:
vue:是一套用于构建用户界面的渐进式框架,Vue 的外围库只关注视图层
react:用于构建用户界面的 JavaScript 库 申明式, 组件化
- 定位
- vue 渐进式 响应式
- React 单向数据流
- 写法
vue:template,jsx
react: jsx
- Hooks:vue3 和 react16 反对 hook
- UI 更新
- 文化
vue 官网提供
React 第三方提供,本人抉择
整个 new Vue 阶段做了什么?
- vue.prototype._init(option)
- initState(vm)
- Observer(vm.data)
- new Observer(data)
- 调用 walk 办法,遍历 data 中的每个属性,监听数据的变动
- 执行 defineProperty 监听数据读取和设置
数据描述符绑定实现后,咱们就能失去以下的流程图
[外链图片转存失败,源站可能有防盗链机制,倡议将图片保留下来间接上传(img-Dq3kKRrB-1665551088442)(https://p3-juejin.byteimg.com…)]
- 图中咱们能够看出,vue 初始化的时候,进行了数据的 get\set 绑定,并创立了一个
- dep 对象就是用来依赖收集, 他实现了一个公布订阅模式,完后了数据 data 的渲染视图 watcher 的订阅
class Dep {
// 依据 ts 类型提醒,咱们能够得出 Dep.target 是一个 Watcher 类型。
static target: ?Watcher;
// subs 寄存收集到的 Watcher 对象汇合
subs: Array<Watcher>;
constructor() {
this.subs = [];
}
addSub(sub: Watcher) {
// 收集所有应用到这个 data 的 Watcher 对象。
this.subs.push(sub);
}
depend() {
if (Dep.target) {
// 收集依赖,最终会调用下面的 addSub 办法
Dep.target.addDep(this);
}
}
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
// 调用对应的 Watcher,更新视图
subs[i].update();
}
}
}
形容 vue 的响应式原理
[外链图片转存失败,源站可能有防盗链机制,倡议将图片保留下来间接上传(img-D2L76LQa-1665551088445)(https://p1-juejin.byteimg.com…)]
Vue 的三个外围类
Observer
:给对象的属性增加 getter 和 setter ,用于依赖收集和派发更新Dep
:用于收集以后响应式对象的依赖关系,每个响应式对象都有 dep 实例,dep.subs = watcher[]
,当数据产生变更的时候,会通过dep.notify()
告诉各个 watcherwatcher
:是一个中介,数据发生变化时通过 watcher 直达,告诉组件 观察者对象,render watcher,computed watcher, user watcher- 依赖收集
- 须要用到数据的中央,称为依赖
- 在
getter
中收集依赖,在setter
中触发依赖 initState
, 对 computed 属性初始化时,会触发computed
watcher
依赖收集initState
, 对监听属性初始化的时候,触发user
watcher
依赖收集render
,触发render
watcher
依赖收集- 派发更新
Object.defindeProperty
- 组件中对响应式的数据进行了批改,会触发 setter 逻辑
dep.notify()
- 遍历所有 subs,调用每一个 watcher 的 update 办法
总结:
当创立一个 vue 实例时, vue 会遍历 data 里的属性, Objeect.defineProperty 为属性增加 getter 和 setter 对数据的读取进行劫持
getter:依赖收集
setter:派发更新
每个组件的实例都有对应的 watcher 实例
计算属性的原理
computed watcher 计算属性的监听器,格式化转换,求值等操作
computed watcher 持有一个 dep 实例,通过 dirty 属性标记计算属性是否须要从新求值
当 computed 依赖值扭转后,就会告诉订阅的 watcher 进行更新,对于 computed watcher 会将 dirty 属性设置为 true,并且进行计算属性办法的调用,参考:前端vue面试题具体解答
留神
- 计算属性是基于他的响应式依赖进行缓存的,只有依赖产生扭转的时候才会从新求值
- 意义:比方计算属性办法外部操作十分频繁时,遍历一个极大的数组,计算一次可能要耗时 1s,如果依赖值没有变动的时候就不会从新计算
nextTick 原理
概念
nextTick 的作用是在下一次 DOM 更新循环完结后,执行提早回调,nextTick 就是创立一个异步工作,要他等到同步工作执行完后才执行
应用
在数据变动后要执行某个操作,而这个操作依赖因数据的扭转而扭转 dom,这个操作应该放到 nextTick 中
vue2 中的实现
<template>
<div>{{ name }}</div>
</template>
<script>
export default { data() { return { name: ""
} }, mounted() { console.log(this.$el.clientHeight) // 0
this.name = "better"
console.log(this.$el.clientHeight) // 0
this.$nextTick(() => { console.log(this.$el.clientHeight) // 18
}); }};
</script>
咱们发现间接获取最新的 DOM 相干的信息是拿不到的,只有在 nextTick 中能力获取罪状的 DOM 信息
原理剖析
在执行 this.name = ‘better’ 会触发 Watcher 更新, Watcher 会把本人放到一个队列,而后调用 nextTick()函数
应用队列的起因:
比方多个数据变更更新视图屡次的话,性能上就不好了, 所以对视图更新做一个异步更新的队列,防止反复计算和不必要的 DOM 操作,在下一轮工夫循环的时候刷新队列,并执行已去重的工作(nextTick 的回调函数),更新视图
export function queueWatcher (watcher: Watcher) {
...
// 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick 里调用
nextTick(flushSchedulerQueue)
}
这里的参数flushSchedulerQueue
办法就会被放入事件循环中,主线程工作执行完后就会执行这个函数,对 watcher 队列排序,遍历,执行 watcher 对应的 run 办法,而后 render,更新视图
也就是在执行 this.name = ‘better’的时候,工作队列能够了解为[flushSchedulerQueue],而后在下一行的 console.log,因为会更新视图工作flushSchedulerQueue
在工作队列中没有执行,所以无奈拿到更后的视图
而后在执行 this.$nextTick(fn)的时候,增加一个异步工作,这时的工作队列能够了解为[flushSchedulerQueue, fn], 而后同步工作执行完了,接着按程序执行工作队列里的工作, 第一个工作执行就会更新视图,前面天然能失去更新后的视图了
nextTick 源码
源码分为两个局部:一个是判断以后环境能应用的最合适的 API 并保留异步函数,二是调用异步函数执行回调队列 1 环境判断 次要是判断用哪个宏工作或者微工作,因为宏工作的耗费工夫是大于微工作的,所以先应用微工作, 用以下的判断程序
- promise
- MutationObserver
- setImmediate
- setTimeout
export let isUsingMicroTask = false; // 是否启用微工作开关
const callbacks = []; // 回调队列
let pending = false; // 异步控制开关,标记是否正在执行回调函数
// 该办法负责执行队列中的全副回调
function flushCallbacks() {
// 重置异步开关
pending = false;
// 避免nextTick里有nextTick呈现的问题
// 所以执行之前先备份并清空回调队列
const copies = callbacks.slice(0);
callbacks.length = 0;
// 执行工作队列
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
let timerFunc; // 用来保留调用异步工作办法
// 判断以后环境是否反对原生 Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// 保留一个异步工作
const p = Promise.resolve();
timerFunc = () => {
// 执行回调函数
p.then(flushCallbacks);
// ios 中可能会呈现一个回调被推入微工作队列,然而队列没有刷新的状况
// 所以用一个空的计时器来强制刷新工作队列
if (isIOS) setTimeout(noop);
};
isUsingMicroTask = true;
} else if (
!isIE &&
typeof MutationObserver !== 'undefined' &&
(isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
// 不反对 Promise 的话,在反对MutationObserver的非 IE 环境下
// 如 PhantomJS, iOS7, Android 4.4
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);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 应用setImmediate,尽管也是宏工作,然而比setTimeout更好
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 以上都不反对的状况下,应用 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
环境判断完结就会失去一个提早回调函数timerFunc
而后进入外围的 nextTick
2 nextTick()函数源码 在应用的时候就是调用 nextTick()这个办法
- 把传入的回调函数放进回调队列 callbacks
- 执行保留的异步工作 timerFunc,就会遍历 callbacks 执行相应的回调函数了
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve;
// 把回调函数放入回调队列
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
if (!pending) {
// 如果异步开关是开的,就关上,示意正在执行回调函数,而后执行回调函数
pending = true;
timerFunc();
}
// 如果没有提供回调,并且反对 Promise,就返回一个 Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve) => {
_resolve = resolve;
});
}
}
能够看到最初有返回一个 Promise,是能够让咱们在不传参的时候用
this.$nextTick().then(()=>{ ... })
vue3 中剖析
点击按钮更新 DOM 内容, 并获取最新的 DOM 内容
<template>
<div ref="test">{{name}}</div>
<el-button @click="handleClick">按钮</el-button>
</template>
<script setup>
import { ref, nextTick } from 'vue'
const name = ref("better") const test = ref(null) async function handleClick(){ name.value = '掘金'
console.log(test.value.innerText) // better
await nextTick() console.log(test.value.innerText) // 掘金
} return { name, test, handleClick } </script>
在应用形式下面有了一些变动,事件循环的原理还是一样的,只是加了几个专门保护队列的办法,以及关联到 effect
vue3 nextTick 源码分析
const resolvedPromise: Promise<any> = Promise.resolve();
let currentFlushPromise: Promise<void> | null = null;
export function nextTick<T = void>(
this: T,
fn?: (this: T) => void,
): Promise<void> {
const p = currentFlushPromise || resolvedPromise;
return fn ? p.then(this ? fn.bind(this) : fn) : p;
}
简略来看就是一个 Promise
nextTick 承受一个函数为参数,同时会创立一个微工作,在咱们页面调用 nextTick 的时候,会执行该函数,把咱们的参数 fn 赋值给 p.then(fn) ,在队列的工作实现后, fn 就执行了
因为加了几个保护队列的办法,所以执行程序是 queueJob
-> queueFlush
-> flushJobs
-> nextTick 参数的 fn
flushJobs 该办法次要负责解决队列工作,次要逻辑如下
- 先解决前置工作队列
- 依据 Id 排列队列
- 遍历执行队列工作
- 执行结束后清空并重置队列
- 执行后置队列工作
- 如果还有就递归继续执行
vue Router
路由就是一组 key-value 的对应关系,在前端我的项目中说的路由能够了解为 url-视图之间的映射关系,这种映射是单向的,url 变动不会走 http 申请,然而会更新切换前端 UI 视图,像 vue 这种单页面利用 就是这样的规定.
路由守卫
- 全局路由守卫
- 前置路由守卫:
beforeEach
路由切换之前被调用
- 全局解析守卫:
beforeResolve
在每次导航时就会触发,然而确保在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后 2,解析守卫就被正确调用,如确保用户能够拜访自定义 meta 属性requiresCamera
的路由:
router.beforeResolve(async (to) => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission();
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,而后勾销导航
return false;
} else {
// 意料之外的谬误,勾销导航并把谬误传给全局处理器
throw error;
}
}
}
});
router.beforeResolve
是获取数据或执行任何其余操作(如果用户无奈进入页面时你心愿防止执行的操作)的现实地位。
- 后置路由守卫 :
afterEach
路由切换之后被调用requiresCamera
的路由: - 独享路由守卫
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
},
},
],
});
- 组件內路由守卫
能够在组件内使用者两个钩子 - 通过路由规定,进入该组件时被调用
beforeRouteEnter (to, from, next) {
}
- 通过路由规定,来到该组件时调用
beforeRouteLeave (to, from, next) {
}
残缺的导航解析过程
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫(2.2+)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫(2.5+)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter
守卫中传给next
的回调函数,创立好的组件实例会作为回调函数的参数传入。
路由模式
- history 模式
/
:
应用pushState
和replaceState
,通过这两个 API 能够扭转 url 地址不产生申请,popState
事件 -
hash 模式
#
:hash 是 URL 中 hash(#)及前面的那局部,罕用作锚点在页面内进行导航,扭转 hash 值不会随着 http 申请发送给服务器,通过
hashChange
事件监听 URL 的变动,能够用他来实现更新页面局部内容的操作
vueRouter 的实现
分析 VueRouter 实质
通过应用 vueRouter 能够晓得
- 通过 new Router() 取得一个 router 实例,我门引入的 VueRouter 其实就是一个类
class VueRouter {}
- 应用 Vue.use(),而 Vue.use 的一个准则就是执行对象的 install 这个办法,所有,咱们能够再一步假如 VueRouter 有 install 这个办法
所以得出
//myVueRouter.js
class VueRouter {}
VueRouter.install = function () {};
export default VueRouter;
剖析 Vue.use
Vue.use(plugin)
用法:
用于装置 vue.js 插件,如果插件是一个对象,必须提供 install 办法,如果插件是一个函数,它会被作为 install 办法,调用 install 办法的时候,会将 vue 作为参数传入,install 办法被同一个插件屡次调用时,插件也只会被装置一次
作用:
注册插件,此时只须要调用 install 办法并将 Vue 作为参数传入 1. 插件的类型,能够是 install 办法,也能够是一个蕴含 install 办法的对象 2. 插件只能被装置一次,保障插件列表中不能有反复的插件
须要将 Vue 作为 install 办法第一个参数传入,先将 Vue 保存起来,将传进来的 Vue 创立两个组件 router-link 和 router-view
//myVueRouter.js
let Vue = null;
class VueRouter {}
VueRouter.install = function (v) {
Vue = v;
console.log(v);
//新增代码
Vue.component('router-link', {
render(h) {
return h('a', {}, '首页');
},
});
Vue.component('router-view', {
render(h) {
return h('h1', {}, '首页视图');
},
});
};
export default VueRouter;
install
个别是给每个 vue 实例增加货色的,路由中就是增加$router
和$route
,留神:每个组件增加的$router
是同一个和$route
是同一个,防止只是根组件有这个 router 值,应用代理的思维
//myVueRouter.js
let Vue = null;
class VueRouter {}
VueRouter.install = function (v) {
Vue = v;
// 新增代码
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.router) {
// 如果是根组件
this._root = this; //把以后实例挂载到_root上
this._router = this.$options.router;
} else {
//如果是子组件
this._root = this.$parent && this.$parent._root;
}
Object.defineProperty(this, '$router', {
get() {
return this._root._router;
},
});
},
});
Vue.component('router-link', {
render(h) {
return h('a', {}, '首页');
},
});
Vue.component('router-view', {
render(h) {
return h('h1', {}, '首页视图');
},
});
};
export default VueRouter;
欠缺 VueRouter 类
首先明确下是实例化的时候传了 v 的参数为 mode(路由模式), routes(路由表),在类的结构器中传参
class VueRouter {
constructor(options) {
this.mode = options.mode || 'hash';
this.routes = options.routes || []; //你传递的这个路由是一个数组表
}
}
然而咱们间接解决 routes 的非常不不便的,所以咱们先要转换成 key:value 的格局
createMap(routes) {
return routes.reduce((pre,current) => {
pre[current.path] = current.component
return pre
},{})
}
vue 模板编译的原理
vue 中模板 template 无奈被浏览器解析并渲染,因为这不属于浏览器的规范,不是正确的 html 语法,所有须要将 template 转换成一个 JavaScript 函数,这样浏览器就能够执行这一个函数并渲染出对应的 html 元素,就能够让视图跑起来了,这个过程就叫做模板编译。模板编译又分为三个阶段,解析parse
, 优化optimize
, 生成generate
,最终生成可执行函数render
- 解析阶段 : 应用大量的正则表达式对 template 字符串进行解析,将标签,指令,属性等转化为形象语法树 AST
- 优化阶段: 遍历 AST,找打其中的一些动态节点进行标记, 不便在页面重渲染的时候进行 diff 比拟时,间接跳过这些动态节点,优化 runtime 的性能
- 生成阶段: 将最终的 AST 转化为 render 函数字符串
发表回复