zoukankan      html  css  js  c++  java
  • 前端常见的Vue面试题目汇总 广东靓仔

    欢迎关注前端早茶,与广东靓仔携手共同进阶

    前端早茶专注前端,一起结伴同行,紧跟业界发展步伐~

    1. 请说一下响应式数据的原理

    默认Vue在初始化数据时,会给data中的属性使用Object.defineProperty重新定义所有属性,当页面到对应属性时,会进行依赖收集(收集当前组件中的watcher)如果属性发生变化会通知相关依赖进行更新操作

    收集当前组件中的watcher,我进一步问你什么叫当前组件的 watcher?我面试时经常听到这种模糊的说法,感觉就是看了些造玩具的文章就说熟悉响应式原理了,起码的流程要清晰一些:

    1. 由于 Vue 执行一个组件的 render 函数是由 Watcher 去代理执行的,Watcher 在执行前会把 Watcher 自身先赋值给 Dep.target 这个全局变量,等待响应式属性去收集它
    2. 这样在哪个组件执行 render 函数时访问了响应式属性,响应式属性就会精确的收集到当前全局存在的 Dep.target 作为自身的依赖
    3. 在响应式属性发生更新时通知 Watcher 去重新调用 vm._update(vm._render()) 进行组件的视图更新

    响应式部分,如果你想在简历上写熟悉的话,还是要抽时间好好的去看一下源码中真正的实现,而不是看这种模棱两可的说法就觉得自己熟练掌握了。

    2. 为什么Vue采用异步渲染

    因为如果不采用异步更新,那么每次更新数据都会对当前租金按进行重新渲染,所以为了性能考虑,Vue会在本轮数据更新后,再去异步更新数据

    什么叫本轮数据更新后,再去异步更新数据?

    轮指的是什么,在 eventLoop 里的 task 和 microTask,他们分别的执行时机是什么样的,为什么优先选用 microTask,这都是值得深思的好问题。

    建议看看这篇文章: Vue源码详解之nextTick:MutationObserver只是浮云,microtask才是核心!

    3. nextTick实现原理

    nextTick方法主要是使用了宏任务和微任务,定义一个异步方法,多次调用nextTick会将方法存在队列中,通过这个异步方法清空当前队列。所以这个nextTick方法就是异步方法

    这句话说的很乱,典型的让面试官忍不住想要深挖一探究竟的回答。(因为一听你就不是真的懂)

    正确的流程应该是先去 嗅探环境,依次去检测

    Promise的then -> MutationObserver的回调函数 -> setImmediate -> setTimeout 是否存在,找到存在的就使用它,以此来确定回调函数队列是以哪个 api 来异步执行。

    在 nextTick 函数接受到一个 callback 函数的时候,先不去调用它,而是把它 push 到一个全局的 queue 队列中,等待下一个任务队列的时候再一次性的把这个 queue 里的函数依次执行。

    这个队列可能是 microTask 队列,也可能是 macroTask 队列,前两个 api 属于微任务队列,后两个 api 属于宏任务队列。

    简化实现一个异步合并任务队列:

    let pending = false
    // 存放需要异步调用的任务
    const callbacks = []
    function flushCallbacks () {
      pending = false
      // 循环执行队列
      for (let i = 0; i < callbacks.length; i++) {
        callbacks[i]()
      }
      // 清空
      callbacks.length = 0
    }
    
    function nextTick(cb) {
        callbacks.push(cb)
        if (!pending) {
          pending = true
          // 利用Promise的then方法 在下一个微任务队列中把函数全部执行 
          // 在微任务开始之前 依然可以往callbacks里放入新的回调函数
          Promise.resolve().then(flushCallbacks)
        }
    }

    测试一下:

    // 第一次调用 then方法已经被调用了 但是 flushCallbacks 还没执行
    nextTick(() => console.log(1))
    // callbacks里push这个函数
    nextTick(() => console.log(2))
    // callbacks里push这个函数
    nextTick(() => console.log(3))
    
    // 同步函数优先执行
    console.log(4)
    
    // 此时调用栈清空了,浏览器开始检查微任务队列,发现了 flushCallbacks 方法,执行。
    // 此时 callbacks 里的 3 个函数被依次执行。
    
    // 4
    // 1
    // 2
    // 3

    4. Vue优点

    虚拟DOM把最终的DOM操作计算出来并优化,由于这个DOM操作属于预处理操作,并没有真实的操作DOM,所以叫做虚拟DOM。最后在计算完毕才真正将DOM操作提交,将DOM操作变化反映到DOM树上

    看起来说的很厉害,其实也没说到点上。关于虚拟 DOM 的优缺点,直接看 Vue 作者尤雨溪本人的知乎回答,你会对它有进一步的理解:

    网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?

    双向数据绑定通过MVVM思想实现数据的双向绑定,让开发者不用再操作dom对象,有更多的时间去思考业务逻辑

    开发者不操作dom对象,和双向绑定没太大关系。React不提供双向绑定,开发者照样不需要操作dom。双向绑定只是一种语法糖,在表单元素上绑定 value 并且监听 onChange 事件去修改 value 触发响应式更新。

    我建议真正想看模板被编译后的原理的同学,可以去尤大开源的vue-template-explorer 网站输入对应的模板,就会展示出对应的 render 函数。

    运行速度更快,像比较与react而言,同样都是操作虚拟dom,就性能而言,vue存在很大的优势

    为什么快,快在哪里,什么情况下快,有数据支持吗?事实上在初始化数据量不同的场景是不好比较的,React 不需要对数据递归的进行 响应式定义

    而在更新的场景下 Vue 可能更快一些,因为 Vue 的更新粒度是组件级别的,而 React 是递归向下的进行 reconcilerReact 引入了 Fiber 架构和异步更新,目的也是为了让这个工作可以分在不同的 时间片 中进行,不要去阻塞用户高优先级的操作。

    Proxy是es6提供的新特性,兼容性不好,所以导致Vue3一致没有正式发布让开发者使用

    Vue3 没发布不是因为兼容性不好,工作正在有序推进中,新的语法也在不断迭代,并且发布 rfc 征求社区意见。

    Object.defineProperty的缺点:无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应

    事实上可以,并且尤大说只是为了性能的权衡才不去监听。数组下标本质上也就是对象的一个属性。

    5. React和Vue的比较

    React默认是通过比较引用的方式(diff)进行的,React不精确监听数据变化。

    比较引用和 diff 有什么关系,难道 Vue 就不 diff 了吗。

    Vue2.0可以通过props实现双向绑定,用vuex单向数据流的状态管理框架

    双向绑定是 v-model 吧。

    Vue 父组件通过props向子组件传递数据或回调

    Vue 虽然可以传递回调,但是一般来说还是通过 v-on:change 或者 @change 的方式去绑定事件吧,这和回调是两套机制。

    模板渲染方式不同,Vue通过HTML进行渲染

    事实上 Vue 是自己实现了一套模板引擎系统,HTML 可以被利用为模板的而已,你在 .vue 文件里写的 template 和 HTML 本质上没有关系。

    React组合不同功能方式是通过HoC(高阶组件),本质是高阶函数

    事实上高阶函数只是社区提出的一种方案被 React 所采纳而已,其他的方案还有 renderProps 和 最近流行的Hook

    Vue 也可以利用高阶函数 实现组合和复用。

    6. diff算法的时间复杂度

    两个数的完全的diff算法是一个时间复杂度为o(n3), Vue进行了优化O(n3)复杂度的问题转换成O(n)复杂度的问题(只比较同级不考虑跨级问题)在前端当中,你很少会跨级层级地移动Dom元素,所以Virtual Dom只会对同一个层级地元素进行对比

    听这个描述来说,React 没有对 O(n3) 的复杂度进行优化?事实上 React 和 Vue 都只会对 tag 相同的同级节点进行 diff,如果不同则直接销毁重建,都是 O(n) 的复杂度。

    7. 谈谈你对作用域插槽的理解

    单个插槽当子组件模板只有一个没有属性的插槽时, 父组件传入的整个内容片段将插入到插槽所在的 DOM 位置, 并替换掉插槽标签本身。

    跟 DOM 没关系,是在虚拟节点树的插槽位置替换。

    如果不加key,那么vue会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug

    不加 key 也不一定就会复用,关于 diff 和 key 的使用,建议大家还是找一些非造玩具的文章真正深入的看一下原理。

    为什么 Vue 中不要用 index 作为 key?(diff 算法详解)

    8. 组件中的data为什么是函数

    因为组件是用来复用的,JS里对象是引用关系,这样作用域没有隔离,而new Vue的实例,是不会被复用的,因此不存在引用对象问题

    这句话反正我压根没听懂,事实上如果组件里 data 直接写了一个对象的话,那么如果你在模板中多次声明这个组件,组件中的 data 会指向同一个引用。

    此时如果在某个组件中对 data 进行修改,会导致其他组件里的 data 也被污染。 而如果使用函数的话,每个组件里的 data 会有单独的引用,这个问题就可以避免了。

    这个问题我同样举个例子来方便理解,假设我们有这样的一个组件,其中的 data 直接使用了对象而不是函数:

    var Counter = {
        template: `<span @click="count++"></span>`
        data: {
            count: 0
        }
    }

    注意,这里的 Counter.data 是一个引用,也就是它是在当前的运行环境下全局唯一的,它在堆内存中占用了一部分空间。

    然后我们在模板中调用两次 Counter 组件:

    <div>
      <Counter id="a" />
      <Counter id="b" />
    </div>

    我们从原理出发,先看看它被编译成什么样的 render 函数:

    function render() {
      with(this) {
        return _c('div', [_c('Counter'), _c('Counter')], 1)
      }
    }

    每一个 Counter 会被 _c 所调用,也就是 createElement,想象一下 createElement 内部会发生什么,它会直接拿着 Counter 上的 data 这个引用去创建一个组件。 也就是所有的 Counter 组件实例上的 data 都指向同一个引用。

    此时假如 id 为 a 的 Counter 组件内部调用了 count++,会去对 data 这个引用上的 count 属性赋值,那么此时由于 id 为 b 的 Counter 组件内部也是引用的同一份 data,它也会感觉到变化而更新组件,这就造成了多个组件之间的数据混乱了。

    9. computed和watch有什么区别

    计算属性是基于他们的响应式依赖进行缓存的,只有在依赖发生变化时,才会计算求值,而使用 methods,每次都会执行相应的方法

    这也是一个一问就倒的回答,依赖变化是计算属性就重新求值吗?中间经历了什么过程,为什么说 computed 是有缓存值的?随便挑一个点深入问下去就站不住。 事实上 computed 会拥有自己的 watcher,它内部有个属性 dirty 开关来决定 computed 的值是需要重新计算还是直接复用之前的值。

    以这样的一个例子来说:

    computed: {
        sum() {
            return this.count + 1
        }
    }

    首先明确两个关键字:

    「dirty」 从字面意义来讲就是  的意思,这个开关开启了,就意味着这个数据是脏数据,需要重新求值了拿到最新值。

    「求值」 的意思的对用户传入的函数进行执行,也就是执行 return this.count + 1

    1. 在 sum 第一次进行求值的时候会读取响应式属性 count,收集到这个响应式数据作为依赖。并且计算出一个值来保存在自身的 value 上,把 dirty 设为 false,接下来在模板里再访问 sum 就直接返回这个求好的值 value,并不进行重新的求值。
    2. 而 count 发生变化了以后会通知 sum 所对应的 watcher 把自身的 dirty 属性设置成 true,这也就相当于把重新求值的开关打开来了。这个很好理解,只有 count 变化了, sum 才需要重新去求值。
    3. 那么下次模板中再访问到 this.sum 的时候,才会真正的去重新调用 sum 函数求值,并且再次把 dirty 设置为 false,等待下次的开启……

    后续我会考虑单独出一篇文章进行详细讲解。

    10. Watch中的deep:true是如何实现的

    当用户指定了watch中的deep属性为true时,如果当前监控的值是数组类型,会对对象中的每一项进行求值,此时会将当前watcher存入到对应属性的依赖中,这样数组中的对象发生变化时也会通知数据更新。

    不光是数组类型,对象类型也会对深层属性进行 依赖收集,比如监听了 obj,假如设置了 deep: true,那么对 obj.a.b.c = 5 这样深层次的修改也一样会触发 watch 的回调函数。本质上是因为 Vue 内部对设置了 deep 的 watch,会进行递归的访问(只要此属性也是响应式属性),而在此过程中也会不断发生依赖收集。

    在回答这道题的时候,同样也要考虑到 递归收集依赖 对性能上的损耗和权衡,才是一份合格的回答。

    11. action和mutation区别

    mutation是同步更新数据(内部会进行是否为异步方式更新数据的检测)

    内部并不能检测到是否异步更新,而是实例上有一个开关变量 _committing

    1. 只有在 mutation 执行之前才会把开关打开,允许修改 state 上的属性。
    2. 并且在 mutation 同步执行完成后立刻关闭。
    3. 异步更新的话由于已经出了 mutation 的调用栈,此时的开关已经是关上的,自然能检测到对 state 的修改并报错。具体可以查看源码中的 withCommit 函数。这是一种很经典对于 js单线程机制 的利用。

      Store.prototype._withCommit = function _withCommit (fn) {
      var committing = this._committing;
      this._committing = true;
      fn();
      this._committing = committing;
      };

    欢迎关注前端早茶,与广东靓仔携手共同进阶

    前端早茶专注前端,一起结伴同行,紧跟业界发展步伐~

  • 相关阅读:
    安装acdsee 3.1后出错
    周末游山
    SQL Server不存在或访问被拒绝 Windows里的一个bug
    将数据库的存储过程及其参数导出来的方法
    VS出错问题集锦
    25 网页设计之页脚设计灵感
    需求沟通和实施
    Mootools插件闪烁的标题
    最近常常干出一些骑着驴找驴的事来
    php表单在提交之后再后退,表单的内容默认是被清空的
  • 原文地址:https://www.cnblogs.com/cczlovexw/p/15736202.html
Copyright © 2011-2022 走看看