有关于前端框架原理的面试问题汇总

90 阅读3分钟

1.vue插件机制及其原理

一、通常用来为Vue添加全局功能
  1. 添加全局方法或者属性。如vue-custom-element
  2. 添加全局资源:指令、过滤器、过度等。如vue-touch
  3. 通过全局混入来添加一些组件选项。如vue-router
  4. 添加Vue实例方法,通过把他们添加到Vue.prototype上实现
  5. 一个库,提供自己的API。如vue-router
二、使用插件方法

通过全局方法Vue.use()使用方法,它需要你调用new Vue()时启动应用之前完成。

三、插件原理

本质上插件就是一个对象,在对象里面调用 install 方法

  • Vue.js的插件应该暴露一个 install 方法,这个方法的第一个参数是Vue构造器,第二个参数是一个可选的选项对象
MyPlugin.install = function(Vue, options) {
  // 1 、添加全局方法或属性
  Vue.myGLobalMethod = function(){ };

  // 2、添加全局资源
  Vue.directive('my-directive', {
    bind(el, binding, vnode, oldVnode) {  }
  });

  // 3、注入组件选项
  Vue.mixin({
    created: function(){  }
  });

  // 4、添加实例方法
  Vue.prototype.$myMethod = function(methodOptions) {  }
}

1.vue插件机制及其原理

在vue中,当我们对某项数据进行频繁的更新时会有很严重的性能问题。比如我们对上述的num属性进行修改:

for(let i=0;i<100;i++){
    data.num=i;//每次的data数据的变化都会调用Watcher的update去更新DOM
}

上面的代码会导致num对应的Watcher的回调频繁执行(100次),其对应的就是100次的DOM更新,我们知道,DOM更新的性能成本是昂贵的,我们开发中应当尽量减少Dom操作。

so

优秀Vue作者肯定也是不允许这种情况发生的,vue就是使用nextTick来优化这个问题的。

简单的说就是每次数据变化之后不是立刻去执行DOM更新,而是要把数据变化的动作缓存起来,在合适的时机只执行一次的dom更新操作。这里就需要要设置一个合适的时间间隔,通过下面要介绍的事件循环机制可以很完美的解决。

我们的数据变化缓存可以依赖事件循环eventloop来完成;因为每次事件循环之间都有一次视图渲染,我们只需要在render之前完成对dom的更新即可,因此我们为了避免无效的DOM操作,需要将数据变更缓存起来,只保存最后一次数据最终的变更结果。

这里简单给出两种实现方法:setTimeout和Promise,我们常用的setTimeout会创建一个宏任务,而Promise.then创建一个微任务。

如果使用setTimeout宏任务实现异步更新队列,那么就是本次同步代码执行完成不执行视图更新,而是在下一次宏任务开始清空异步更新队列,处理缓存的DOM更新和开发者添加的nextTick回调。

使用Promise创建的是微任务,微任务会在本次事件循环同步代码执行结束后执行,使用setTimeout创建的是宏任务,同样会在此次同步代码执行完成后执行,区别是在setTimeout代码执行之前会穿插一次无效的视图渲染,因此我们尽量使用Promise创建微任务实现异步更新。

so,在vue中我们可以

由于Vue DOM更新是异步执行的,即修改数据时,视图不会立即更新,而是会监听数据变化,并缓存在同一事件循环中,等同一数据循环中的所有数据变化完成之后,再统一进行视图更新。为了确保得到更新后的DOM,所以设置了 Vue.nextTick()方法。

Watcher新updata函数

    update(newValue){
        //值发生变化才变更
        if(this.value!==newValue){
            this.value=newValue;
            //在异步更新队列中添加Watcher,用于后续更新
            updateQueue.push(this);
        }
    }
    //执行DOM更新等操作
    run(){
        this.cb(this.value);
    }

现在我们有了一个处理更新队列的函数,但是现在还缺少一个很重要的元素,就是执行此函数的时机,这时我们回忆一下我们的更新队列是异步更新队列,这里的异步即使用setTimeout或者Promise实现异步更新,这个实现过程就是nextTick的代码实现了,下面是简化版nextTick函数

let callbacks=[];//事件队列,包含异步dom更新队列和用户添加的异步事件
let pending=false;//控制变量,每次宏任务期间执行一次flushCallbacks清空callbacks
funciton nextTick(cb){
   callbacks.push(cb);
   if(!pending){
      pending=true;
      //这里也可以使用Promise,Promise创建的是微任务,微任务会在本次事件循环同步代码执行结束后执行,使用setTimeout创建的是宏任务,同样会在此次同步代码执行完成后执行,区别是在setTimeout代码执行之前会穿插一次无效的视图渲染,因此我们尽量使用Promise创建微任务实现异步更新。
      if(Promise){
          Promise.resovle().then(()=>{
              flushCallbacks();
          })
      }
      setTimeout(()=>{
          flushCallbacks();
      })
   }
}
function flushCallbacks(){
    pending=false;//状态重置
    callbacks.forEach(cb=>{
        callbacks.shift()();
    })
}

3.vue computed设计原理

image.png

在initState的时候会初始化initComputed,默认初始化计算属性initComputed的时候会获取用户定义的方法,内部会创建一个watcher(new Watcher),将用户定义的方法传入,这个watcher有一个computedWatcherOptions标识lazy:true,默认创建出来watcher并不会去执行,watcher内部有个属性叫dirty,如果是计算属性默认dirty就是true。watcher的第二次传入的是一个getter用户定义的属性,在watcher对传入的属性方法进行判断,如果是function就把它存到一个getter上,当lazy是true就undefind的什么也不做,false的话就执行this.get()进行依赖追加表示不是计算属性是watch。

在state文件中有一个defineComputed方法,用来定义计算属性,它的底层也使用的是Object.defineProperty,在这个方法中又定义了一个createComputedGetter方法用来创建getter,在取值的时候会执行createComputedGetter方法返回一个computedGetter函数来,里面会对计算属性的watcher中dirty进行判断,当为true时,watcher才会去执行计算属性进行求值(watcher.evaluate),在evaluate中会执行get方法,get方法就是让当前的getter执行,在求值的过程中进行依赖收集。在取值之前pushTarget(this)就是将watcher放到全局,进行依赖收集,会把当前的计算属性的watcher收集起来,等数据发生变化,就会触发watcher的update执行,在watcher的update中,如果lazy为true是计算属性,就会将dirty改为true在取值时重新进行求值,在求值完成后就会把dirty改为false,如果在取值时dirty为false就会直接将值返回。

4.scoped原理

为组件实例生成一个唯一标识,给组件中的每个标签对应的dom元素添加一个标签属性,data-v-xxxx 给中的每个选择器的最后一个选择器添加一个属性选择器,原选择器[data-v-xxxx],如:原选择器为.container #id div,则更改后选择器为.container #id div[data-v-xxxx]

5.vue-router原理

1.hash模式,主要是hashHistory:hash(“#”)符号的本来作用是加在URL中指示网页中的位置,hash虽然出现在URL中,但不会被包括在HTTP请求中。它是用来指导浏览器动作的,对服务器端完全无用,因此,改变hash不会重新加载页面,每一次改变hash(window.location.hash),都会在浏览器的访问历史中增加一个记录,改变hash可以监听到hashchange事件:

window.addEventListener(“hashchange”, funcRef, false) 底层处理路由更新(push)的流程: 1)push方法 对window的hash直接赋值 window.location.hash = route.fullPath 2)router是怎么做到在每一个Vue组件中都能使用的呢? 在插件加载的地方,即VueRouter的install方法中通过Vue.mixin()全局注册一个混合,影响到每一个组件。

  export function install (Vue) {
  
  Vue.mixin({
    beforeCreate () {
      if (isDef(this.$options.router)) {
        this._router = this.$options.router
        this._router.init(this)
        Vue.util.defineReactive(this, '_route', this._router.history.current)
      }
      registerInstance(this, this)
    },
  })
}

看代码我们知道Vue.mixin()在beforeCreate钩子中通过Vue.util.defineReactive()定义了响应式的route属性,所谓响应式其实就是,当route值改变时,调用Vue的render方法,更新视图

2.history模式,主要利用HTML5History 我们主要利用这个HTML5History来操作浏览器历史记录栈,主要方法有back(), forward(), go()来读取浏览器路由历史并控制跳转,HTML5新增pushState(), replaceState()2个方法来修改历史信息,调用这两个方法修改历史信息后,虽然当前URL改变了,但浏览器不会立即发送请求该URL,这就满足单页面应用”更新视图但不重新请求页面“的需求,修改浏览器历史记录后会触发popstate事件,我们可以通过监听popstate事件

5.keep-alive原理

1.用法:我们想要缓存某个组件,只要用<[keep-alive]>组件将其包裹就行。常用的用法是包裹组件缓存动态组件,或者包裹缓存路由页面。 2.组件可以接收三个属性:

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  • max - 数字。最多可以缓存多少组件实例。

3.原理

    export default {
  name: 'keep-alive',
  abstract: true,
 
  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number]
  },
 
  created () {
    this.cache = Object.create(null)
    this.keys = []
  },
 
  destroyed () {
    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))
    })
  },
 
  render() {
    /* 获取默认插槽中的第一个组件节点 */
    const slot = this.$slots.default
    const vnode = getFirstComponentChild(slot)
    /* 获取该组件节点的componentOptions */
    const componentOptions = vnode && vnode.componentOptions
 
    if (componentOptions) {
      /* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
      const name = getComponentName(componentOptions)
 
      const { include, exclude } = this
      /* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
      if (
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }
 
      const { cache, keys } = this
      const key = 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
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      } else {
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }
 
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}
它有created,destroyed,mounted,render四个钩子。接下来我们就说说这四个钩子分别干了什么,最后再总结下整体流程。

created钩子会创建一个cache对象,用来作为缓存容器,保存vnode节点。

destroyed钩子则在组件被销毁的时候清除cache缓存中的所有组件实例。

render钩子作用:【

  1. 先获取到插槽里的内容

  2. 调用getFirstComponentChild方法获取第一个子组件,获取到该组件的name,如果有name属性就用name,没有就用tag名。

  3. 用获取到的name和传入的include,exclude属性进行匹配,如果匹配不成功,则表示不缓存该组件,直接返回这个组件的 vnode,否则的话走下一步缓存:

  4. 缓存机制:用拿到的name去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存: 命中缓存时会直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个。

如果没有命中缓存,即该组件还没被缓存过,则以该组件的key为键,组件vnode为值,将其存入this.cache中,并且把key存入this.keys中。此时再判断this.keys中缓存组件的数量是否超过了设置的最大缓存数量值this.max,如果超过了,则把第一个缓存组件删掉。

mounted钩子

在这个钩子函数里,调用了pruneCache方法,以观测 include 和 exclude 的变化。

** 6.vue编译原理**

第一步是将 模板字符串 转换成 element ASTs(解析器)
第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)

7.vuex`的核心原理

vuex`的核心原理,通过全局混入`beforeCreate`,将`store`实例注入到每个`Vue`组件中,因此每个组件都能通过`this.$store`来获取到唯一的`store`实例。通过借助`vue`的`data`和`computed`来实现`state`的响应式和`getters`缓存特性。严格模式通过`vue`的`watch`来监听,如果`state`的修改不是通过`commit`方法则会抛出警告。`action`方法都会包裹一层`promise`,所以`action`返回的是`promise`。这也就是为什么说要在`mutation`里面处理同步,在`action`里面处理异步

** 8.vue的render函数**

在vue中我们使用模板HTML语法来组建页面的,使用render函数我们可以用js语言来构建DOM。因为vue是虚拟DOM,所以在拿到template模板时也要转译成VNode的函数,而用render函数构建DOM,vue就免去了转译的过程。

  当使用render函数描述虚拟DOM时,vue提供一个函数,这个函数是就构建虚拟DOM所需要的工具。官网上给他起了个名字叫createElement。还有约定它的简写叫h

render 函数即渲染函数,它是个函数,render 函数的返回值是VNode(即:虚拟节点,也就是我们要渲染的节点)
createElement 是 render 函数的参数,它本身也是个函数,并且有三个参数。接来下我们重点介绍这三个参数

  1. String,表示的是HTML 标签名

  2. Object ,一个含有数据的组件选项对象

  3. Function ,返回了一个含有标签名或者组件选项对象的async 函数