乐趣区

关于vue.js:vue这些原理你都知道吗面试版

前言

在之前面试的时候我本人也常常会遇到一些 vue 原理的问题, 我也总结了下本人的常常的用到的, 不便本人学习, 明天也给大家分享进去, 欢送大家一起学习交换, 有更好的办法欢送评论区指出, 后序我也将继续整顿总结~

形容 Vue 与 React 区别

阐明概念:
vue: 是一套用于构建用户界面的 渐进式框架,Vue 的外围库只关注视图层
react: 用于构建用户界面的 JavaScript 库 申明式, 组件化

  1. 定位
  2. vue 渐进式 响应式
  • React 单向数据流
  • 写法
    vue:template,jsx
    react: jsx
  1. Hooks:vue3 和 react16 反对 hook
  2. UI 更新
  3. 文化
    vue 官网提供
    React 第三方提供, 本人抉择

整个 new Vue 阶段做了什么?

  1. vue.prototype._init(option)
  2. initState(vm)
  3. Observer(vm.data)
  4. new Observer(data)
  5. 调用 walk 办法, 遍历 data 中的每个属性, 监听数据的变动
  6. 执行 defineProperty 监听数据读取和设置

数据描述符绑定实现后, 咱们就能失去以下的流程图

  • 图中咱们能够看出,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 的响应式原理

参考 前端 vue 面试题具体解答

Vue 的三个外围类

  1. Observer : 给对象的属性增加 getter 和 setter , 用于 依赖收集 派发更新
  2. Dep : 用于收集以后响应式对象的依赖关系, 每个响应式对象都有 dep 实例,dep.subs = watcher[], 当数据产生变更的时候, 会通过 dep.notify() 告诉各个 watcher
  3. watcher: 是一个中介, 数据发生变化时通过 watcher 直达, 告诉组件 观察者对象,render watcher,computed watcher, user watcher
  4. 依赖收集
  • 须要用到数据的中央, 称为依赖
  • 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, 并且进行计算属性办法的调用,

留神

  1. 计算属性是基于他的响应式依赖进行缓存的, 只有依赖产生扭转的时候才会从新求值
  2. 意义: 比方计算属性办法外部操作十分频繁时, 遍历一个极大的数组, 计算一次可能要耗时 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 这种单页面利用 就是这样的规定.

路由守卫

  1. 全局路由守卫
  2. 前置路由守卫: 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) => {// ...},
    },
  ],
});
  1. 组件內路由守卫
    能够在组件内使用者两个钩子
  2. 通过路由规定, 进入该组件时被调用
beforeRouteEnter (to, from, next) {}
  • 通过路由规定, 来到该组件时调用
beforeRouteLeave (to, from, next) {}

残缺的导航解析过程

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创立好的组件实例会作为回调函数的参数传入。

路由模式

  1. history 模式 /:
    应用 pushStatereplaceState, 通过这两个 API 能够扭转 url 地址不产生申请,popState事件
  2. hash 模式# :

    hash 是 URL 中 hash(#)及前面的那局部, 罕用作锚点在页面内进行导航, 扭转 hash 值不会随着 http 申请发送给服务器, 通过 hashChange 事件监听 URL 的变动, 能够用他来实现更新页面局部内容的操作

vueRouter 的实现

分析 VueRouter 实质

通过应用 vueRouter 能够晓得

  1. 通过 new Router() 取得一个 router 实例, 我门引入的 VueRouter 其实就是一个类
class VueRouter {}
  1. 应用 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 函数字符串
退出移动版