68 Vue面试考点答题分析

请说一下响应式数据的理解

参考答案

  • 核心点考察的是:数组和对象类型当值变化时如何劫持到。对象内部通过defineReactive方法,使用Object.defineProperty将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。 这里在回答时可以带出一些相关知识点(比如多层对象是通过递归来实现劫持,顺带提出Vue3中是使用proxy来实现响应式数据)
  • 核心点答出来了也可以在进行补充回答,内部依赖收集是怎样做到的,每个属性都拥有自己的dep属性,存放他所依赖的watcher,当属性变化后会通知自己对应的watcher去更新
  • 这里可以引出性能优化相关的内容
    • 对象层级过深,性能就会差
    • 不需要响应数据的内容不要放到data
    • Object.freeze() 可以冻结数据

Vue如何检测数组变化

  • 核心点考察的是:数组考虑性能原因没有用defineProperty对数组的每一项进行拦截,而是选择重写数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写
  • 核心点答出来了也可以在进行补充回答,在Vue中修改数组的索引和长度是无法监控到的。需要通过以上7种变异方法修改数组才会触发数组对应的watcher进行更新。数组中如果是对象数据类型也会进行递归劫持
  • 引发出的问题,那如果想更改索引更新数据怎么办?可以通过Vue.$set()来进行处理 => 核心内部用的是splice方法

Vue中模板编译原理

`

  • 核心点考察的是:如何将template转换成render函数(这里要注意的是我们在开发时尽量不要使用template,因为将template转化成render方法需要在运行时进行编译操作会有性能损耗,同时引用带有compiler包的vue体积也会变大。默认.vue文件中的template处理是通过vue-loader来进行处理的并不是通过运行时的编译 - 后面我们会说到默认vue项目中引入的vue.js是不带有compiler模块的)
  • 模板编译过程
    • template模板转换成ast语法树 - parserHTML
    • 对静态语法做静态标记 - markUp
    • 重新生成代码 - codeGen
    • 核心点答出来了也可以在进行补充回答 (模板引擎的实现原理就是new Function + with来进行实现的)
        function compileToFunctions(template) {
      // 我们需要把html字符串变成render函数
      // 1.把html代码转成ast语法树  ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
      // 很多库都运用到了ast 比如 webpack babel eslint等等
      let ast = parse(template);
      // 2.优化静态节点:对ast树进行标记,标记静态节点
        if (options.optimize !== false) {
          optimize(ast, options);
        }
    
      // 3.通过ast 重新生成代码
      // 我们最后生成的代码需要和render函数一样
      // 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
      // _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
      let code = generate(ast);
      //   使用with语法改变作用域为this  之后调用render函数可以使用call改变this 方便code里面的变量取值
      let renderFn = new Function(`with(this){return ${code}}`);
      return renderFn;
    }
* `vue-loader`中处理`template`属性主要靠的是`vue-template-compiler`模块
        const VueTemplateCompiler = require('vue-template-compiler');
    const {render} = VueTemplateCompiler.compile("<div id="hello">{{msg}}</div>");
    console.log(render.toString())

生命周期钩子是如何实现的

  • 核心点考察的是:Vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法
  • 核心点答出来了也可以在进行补充回答:内部主要是使用callHook方法来调用对应的方法。核心是一个发布订阅模式,将钩子订阅好(内部采用数组的方式存储),在对应的阶段进行发布
    export function callHook(vm, hook) {
      // 依次执行生命周期对应的方法
      const handlers = vm.$options[hook];
      if (handlers) {
        for (let i = 0; i < handlers.length; i++) {
          handlers[i].call(vm); //生命周期里面的this指向当前实例
        }
      }
    }
    
    // 调用的时候
    Vue.prototype._init = function (options) {
      const vm = this;
      vm.$options = mergeOptions(vm.constructor.options, options);
      callHook(vm, "beforeCreate"); //初始化数据之前
      // 初始化状态
      initState(vm);
      callHook(vm, "created"); //初始化数据之后
      if (vm.$options.el) {
        vm.$mount(vm.$options.el);
      }
    };
    
    // 销毁实例实现
    Vue.prototype.$destory = function() {
    	 // 触发钩子
        callHook(vm, 'beforeDestory')
        // 自身及子节点
        remove() 
        // 删除依赖
        watcher.teardown() 
        // 删除监听
        vm.$off() 
        // 触发钩子
        callHook(vm, 'destoryed')
    }

Vue.mixin的使用场景和原理

  • 核心点考察的是:Vue.mixin的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准
  • 核心点答出来了也可以在进行补充回答:mixin中有很多缺陷 "命名冲突问题"、"依赖问题"、"数据来源问题",这里强调一下mixin的数据是不会被共享的

nextTick在哪里使用?原理是

  • 核心点考察的是:nextTick中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。原理就是异步方法(promise,mutationObserver,setImmediate,setTimeout)经常与事件环一起来问(宏任务和微任务)
  • 核心点答出来了也可以在进行补充回答:vue多次更新数据,最终会进行批处理更新。内部调用的就是nextTick实现了延迟更新,用户自定义的nextTick中的回调会被延迟到更新完成后调用,从而可以获取更新后的DOM

Vue为什么需要虚拟DOM

  • 核心点考察的是:Virtual DOM就是用js对象来描述真实DOM,是对真实DOM的抽象,由于直接操作DOM性能低但是js层的操作效率高,可以将DOM操作转化成对象操作,最终通过diff算法比对差异进行更新DOM(减少了对真实DOM的操作)。虚拟DOM不依赖真实平台环境从而也可以实现跨平台。
  • 核心点答出来了也可以在进行补充回答:虚拟DOM的实现就是普通对象包含tagattrschildren等属性对真实节点的描述。(本质上就是在JSDOM之间的一个缓存)

Vue中的diff原理

核心点考察的是:Vuediff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较

比较过程

  1. 先比较是否是相同节点
  2. 相同节点比较属性,并复用老节点
  3. 比较儿子节点,考虑老节点和新节点儿子的情况
  4. 优化比较:头头尾尾头尾尾头
  5. 比对查找进行复用

Vue3中采用最长递增子序列实现diff算法

Vue中computed和watch的区别

  • 核心点考察的是:computedwatch都是基于Watcher来实现的,分别是计算属性watcher和用户watchercomputed属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行(可以用模板渲染,取值的过程中不支持异步方法)watch则是监控值的变化,当值发生变化时调用对应的回调函数
  • 核心点答出来了也可以在进行补充回答:computed不会立即执行,内部通过defineProperty进行定义。并且通过dirty属性来检测依赖的数据是否发生变化。watch则是立即执行将老值保存在watcher上,当数据更新时重新计算新值,将新值和老值传递到回调函数中

Vue.set方法是如何实现的

核心点考察的是:为什么$set可以触发更新,我们给对象和数组本身都增加了dep属性。当给对象新增不存在的属性则触发对象依赖的watcher去更新,当修改数组索引时我们调用数组本身的splice方法去更新数组

    export function set (target: Array | Object, key: any, val: any): any {
      // 1.是开发环境 target 没定义或者是基础类型则报错
      if (process.env.NODE_ENV !== 'production' &&
          (isUndef(target) || isPrimitive(target))
      ) {
          warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      // 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
      if (Array.isArray(target) && isValidArrayIndex(key)) {
          target.length = Math.max(target.length, key)
          target.splice(key, 1, val)
          return val
      }
      // 3.如果是对象本身的属性,则直接添加即可
      if (key in target && !(key in Object.prototype)) {
          target[key] = val
          return val
      }
      const ob = (target: any).__ob__
      // 4.如果是Vue实例 或 根数据data时 报错
      if (target._isVue || (ob && ob.vmCount)) {
          process.env.NODE_ENV !== 'production' && warn(
          'Avoid adding reactive properties to a Vue instance or its root $data ' +
          'at runtime - declare it upfront in the data option.'
          )
          return val
      }
      // 5.如果不是响应式的也不需要将其定义成响应式属性
      if (!ob) {
          target[key] = val
          return val
      }
      // 6.将属性定义成响应式的
      defineReactive(ob.value, key, val)
      // 7.通知视图更新
      ob.dep.notify()
      return val
    }

Vue.use是干什么的?原理是什么

核心点考察的是:Vue.use是用来使用插件的,我们可以在插件中扩展全局组件、指令、原型方法等

    Vue.use = function (plugin: Function | Object) {
      // 插件不能重复的加载
      const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
      if (installedPlugins.indexOf(plugin) > -1) {
          return this
      }
      // additional parameters
      const args = toArray(arguments, 1)
      args.unshift(this)  // install方法的第一个参数是Vue的构造函数,其他参数是Vue.use中除了第一个参数的其他参数
      if (typeof plugin.install === 'function') { // 调用插件的install方法
          plugin.install.apply(plugin, args)  Vue.install = function(Vue,args){}
      } else if (typeof plugin === 'function') { // 插件本身是一个函数,直接让函数执行
          plugin.apply(null, args) 
      }
      installedPlugins.push(plugin) // 缓存插件
      return this
    }

vue-router有几种钩子函数?具体是什么及执行流程是怎样的

核心点考察的是:路由钩子的执行流程, 钩子函数种类有: 全局守卫路由守卫组件守卫

完整的导航解析流程

  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 的回调函数,创建好的组件实例会作为回调函数的参数传入

阅读全文

Last Updated:
Contributors: leeguooooo