Vue 高频原理面试篇+详细解答

作者: 林一一呢 | 来源:发表于2021-05-25 11:17 被阅读0次

    原文首地址 掘金

    三连哦 更多好文 github

    大家好,我是林一一,这是一篇关于 vue 的原理面试题,如果能够完全弄懂相信对大家很有帮助。

    面试题篇

    1.老生常谈之, MPA/SPA 的理解,优缺点是什么?

    MPA 多页面应用。

    • 构成:有多个页面 html 构成,
    • 跳转方式:页面的跳转是从一个页面到另一个页面
    • 刷新的方式:全页面刷新
    • 页面数据跳转:依赖 URL/cookie/localStorage
    • 跳转后的资源 会重新加载
    • 优点:对 SEO 比较友好,开发难度低一点。
      SPA单页面应用
    • 页面组成:由一个外壳页面包裹,多个页面(组件)片段组成
    • 跳转方式:在外壳页面中跳转,将片段页面(组件)显示或隐藏
    • 刷新方式:页面片段的局部刷新
    • 页面的数据跳转:组件间的传值比较容易
    • 跳转后的资源 不会重新加载
    • 缺点:对 SEO 搜索不太友好需要单独做配置,开发难度高一点需要专门的开发框架

    iframe 实际上是 MPA,但是可以实现 SPA 的一些效果,但是本身由不少问题。

    2.老生常谈之,为什么需要有这些 MVC/MVVM 模式?谈谈你对 MVC,MVVM 模式的区别,

    目的:借鉴后端的思想,职责划分和分层

    • Vue, React 不是真正意义上的 MVVM 更不是 MVC,两者核心只处理视图层 view

    MVC模式

    MVC.jpg

    单向的数据,用户的每一步操作都需要重新请求数据库来修改视图层的渲染,形成一个单向的闭环。比如 jQuery+underscore+backbone

    • M:model 数据存放层
    • V: view:视图层 页面
    • C: controller:控制器 js 逻辑层。

    controller 控制层将数据层 model层 的数据处理后显示在视图层 view层,同样视图层 view层 接收用户的指令也可以通过控制层 controller,作用到数据层 model。所以 MVC的缺点是视图层不能和数据层直接交互。

    MVVM模式

    隐藏了 controller 控制层,直接操控 View 视图层和 Model 数据层。

    MVVM.jpg
    • M:model 数据模型
    • V: view 视图模板
    • VM:view-model 视图数据模板(vue处理的层,vue 中的definedProperty 就是处理 VM 层的逻辑)

    双向的数据绑定:model 数据模型层通过数据绑定 Data Bindings 直接影响视图层 View,同时视图层 view 通过监听 Dom Listener 也可以改变数据模型层 model

    • 数据绑定和DOM事件监听就是 viewModelVue 主要做的事。也就是说:只要将 数据模型层Model 的数据挂载到 ViewModelVue 就可以实现双向的数据绑定。
    • 加上 vuex/redux 可以作为 vue和reactmodel 数据层。
    var vm = new Vue()
    

    vm 就是 view-model 数据模型层,data:就是vm view-model 层所代理的数据。

    • 综上两者的区别:MVC 的视图层和数据层交互需要通过控制层 controller 属于单向链接。MVVM 隐藏了控制层 controller,让视图层和数据层可以直接交互 属于双向连接。

    3. 说一下对 Vue 中响应式数据的理解

    小tip:响应式数据指的是数据发生了变化,视图可以更新就是响应式的数据

    • vue 中实现了一个 definedReactive 方法,方法内部借用 Object.definedProperty() 给每一个属性都添加了 get/set 的属性。
    • definedReactive 只能监控到最外层的对象,对于内层的对象需要递归劫持数据。
    • 数组则是重写的7个 push pop shift unshift reverse sort splice 来给数组做数据拦截,因为这几个方法会改变原数组
    • 扩展:
    // src\core\observer\index.js
    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      // 准备给属性添加一个 dep 来依赖收集 Watcher 用于更新视图。
      const dep = new Dep()
      // some code
    
      // observe() 用来观察值的类型,如果是属性也是对象就递归,为每个属性都加上`get/set`
      let childOb = !shallow && observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            // 这里取数据时依赖收集
            const value = getter ? getter.call(obj) : val
            if (Dep.target) {
                dep.depend()
                // childOb 是对对像进行收集依赖
                if (childOb) {
                    childOb.dep.depend()
    
                    //这里对数组和内部的数组进行递归收集依赖,这里数组的 key 和 value 都有dep。
                    if (Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            return value
        },
        set: function reactiveSetter (newVal) {
          // 属性发生改变,这里会通知 watcher 更新视图
        }
      })
    }
    

    上面的 Dep(类) 是用来干嘛的?答:用来收集渲染的 WatcherWatcher 又是一个啥东西?答:watcher 是一个类,用于更新视图的

    4. Vue 是怎么检测数组的变化的?

    • vue 没有对数组的每一项用 definedProperty() 来数据拦截,而是通过重写数组的方法push pop shift unshift reverse sort splice
    • 手动调用 notify,通知 render watcher,执行 update
    • 数组中如果有对象类型(对象和数组)的话会进行数据拦截。
    • 所以通过修改数组下标和数组长度是不会进行数据拦截的,也就不会有响应式变化。例如arr[0] = 1, arr.length = 2 都不会有响应式
    • 扩展:
    // src\core\observer\array.js
    const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse']
    methodsToPatch.forEach(function (method) {
      const original = arrayProto[method]
      def(arrayMethods, method, function mutator (...args) {
        const result = original.apply(this, args)
        const ob = this.__ob__
        let inserted
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = args
            break
          case 'splice':
            inserted = args.slice(2)
            break
        }
        // 新增的类型再次观察
        if (inserted) ob.observeArray(inserted)
        // 手动调用 notify 派发更新
        ob.dep.notify()
        return result
      })
    })
    

    5.Vue 是怎样依赖收集的?(dep 和 Watcher 是什么关系)

    dep.jpg

    tip:Dep 是一个用来负责收集 Watcher 的类,Watcher 是一个封装了渲染视图逻辑的类,用于派发更新的。需要注意的是 Watcher 是不能直接更新视图的还需要结合Vnode经过patch()中的diff算法才可以生成真正的DOM

    • 每一个属性都有自己的 dep 属性,来存放依赖的 Watcher,属性发生变化后会通知 Watcher 去更新。
    • 在用户获取(getter) 数据时 Vue 给每一个属性都添加了 dep 属性来(collect as Dependency)收集 Watcher。在用户 setting 设置属性值时 dep.notify() 通知 收集的Watcher 重新渲染。详情见上面的 defineReactive()
    • Dep依赖收集类 其和 Watcher类 是多对多双向存储的关系
    • 每一个属性都可以有多个 Watcher 类,因为属性可能在不同的组件中被使用。
    • 同时一个 Watcher 类 也可以对应多个属性。

    6. Vue 中的模板编译

    <img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/bc2f78cda8514103b4d0a29acb1a4c9a~tplv-k3u1fbpfcp-watermark.image" width="80%" height="360px"/>

    Vue中模板编译:其实就是将 template 转化成 render 函数。说白了就是将真实的 DOM(模板) 编译成虚拟 dom(Vnode)

    • 第一步是将 template 模板字符串转换成 ast 语法树(parser 解析器),这里使用了大量的正则来匹配标签的名称,属性,文本等。
    • 第二步是对 AST 进行静态节点 static 标记,主要用来做虚拟 DOM 的渲染优化(optimize优化器),这里会遍历出所有的子节点也做静态标记
    • 第三步是 使用 ast语法树 重新生成 render 函数 代码字符串 code。(codeGen 代码生成器)

    为什么要静态标记节点,如果是静态节点(没有绑定数据,前后不需要发生变化的节点)那么后续就不需要 diff 算法来作比较。

    7. 生命周期钩子实现原理

    • vue 中的生命周期钩子只是一个回调函数,在创建组件实例化的过程中会调用对应的钩子执行。
    • 使用Vue.mixin({})混入的钩子或生命周期中定义了多个函数,vue 内部会调用mergeHook() 对钩子进行合并放入到队列中依次执行
    • 扩展
    // src\core\util\options.js
    function mergeHook (
      parentVal: ?Array<Function>,
      childVal: ?Function | ?Array<Function>
    ): ?Array<Function> {
      const res = childVal
        ? parentVal
          ? parentVal.concat(childVal) // 合并
          : Array.isArray(childVal)
            ? childVal
            : [childVal]
        : parentVal
      return res
        ? dedupeHooks(res)
        : res
    }
    

    8.老生常谈之 vue 生命周期有哪些,一般在哪里发送请求?

    • beforeCreate: 刚开始初始化 vue 实例,在数据观测observer之前调用,还没有创建 data/methods 等属性
    • created: vue 实例初始化结束,所有的属性已经创建。
    • beforeMount: 在 vue 挂载数据到页面上之前,触发这个钩子,render 函数此时被触发。
    • mounted: el 被 创建的vm.$el替换,vue 初始化的数据已经挂载到页面之上,这里可以访问到真实的 DOM。一般会在这里请求数据。
    • beforeUpdate: 数据更新时调用,也就是在虚拟 dom 重新渲染之前。
    • updated: 数据变化导致虚拟 dom 发生重新渲染之后发生。
    • beforeDestroy: 实例销毁之前调用该钩子,此时实例还在。vm.$destroy 触发两个方法。
    • destroyed: Vue 实例销毁之后调用。所有的事件监听都会被接触。

    请求数据要看具体的业务需求决定在哪里发送 ajax

    9.Vue.mixin({})的使用场景和原理

    • 使用场景:用于抽离一个公共的业务逻辑实现复用。
    • 实现原理:调用 mergeOptions() 方法采用策略模式针对不同的属性合并。混入的数据和组件的数据有冲突就采用组件本身的。
    • Vue.mixin({}) 缺陷,1.可能会导致混入的属性名和组件属性名发生命名冲突;2. 数据依赖的来源问题
    • 扩展
    export function mergeOptions (
      parent: Object,
      child: Object,
      vm?: Component
    ): Object {
      // some code
      if (!child._base) {
        if (child.extends) {
          parent = mergeOptions(parent, child.extends, vm)
        }
        if (child.mixins) {
          for (let i = 0, l = child.mixins.length; i < l; i++) {
            parent = mergeOptions(parent, child.mixins[i], vm)
          }
        }
      }
    
     // 递归遍历合并组件和混入的属性
      const options = {}
      let key
      for (key in parent) {
        mergeField(key)
      }
      for (key in child) {
        if (!hasOwn(parent, key)) {
          mergeField(key)
        }
      }
      function mergeField (key) {
        const strat = strats[key] || defaultStrat
        options[key] = strat(parent[key], child[key], vm, key)
      }
      return options
    }
    

    10.老生常谈之 vue 组件中的data 为什么必须是一个函数?

    • 这和 js 本身机制相关,data 函数中返回的对象引用地址不同,就能保证不同组件之间的数据不相互污染。
    • Vue.mixin() 中如果混入data属性,那么 data 也必须是一个函数。因为Vue.mixin()也可以多处使用。
    • 实例中data可以是一个对象也可以是一个函数,因为我们一个页面一般只初始化一个Vue实例(单例)

    11. 老生常谈之 vue 中 vm.$nextTick(cb)实现原理和场景

    • 场景:在 dom 更新循环结束后调用,用于获取更新后的 dom 数据
    • 实现原理:vm.$nextTick(cb) 是一个异步的方法为了兼容性做了很多降级处理依次有 promise.then,MutationObserver,setImmediate,setTimeout。在数据修改后不会马上更新视图,而是经过 set 方法 notify 通知 Watcher 更新,将需要更新的 Watcher 放入到一个异步队列中,nexTick 的回调函数就放在 Watcher 的后面,等待主线程中同步代码执行借宿然后依次清空队列中,所以 vm.nextTick(callback) 是在 dom 更新结束后执行的。

    上面将对列中Watcher 依次清空就是 vue 异步批量更新的原理。提一个小思考:为什么不直接使用setTimeout代替?因为setTimeout是一个宏任务,宏任务多性能也会差。关于事件循环可以看看 JS 事件循环

    12.老生常谈之 watch 和 computed 区别

    • computed 内部就是根据 Object.definedProperty() 实现的
    • computed 具备缓存功能,依赖的值不发生变化,就不会重新计算。
    • watch 是监控值的变化,值发生变化时会执行对应的回调函数。
    • computedwatch 都是基于 Watcher类 来执行的。

    computed 缓存功能依靠一个变量 dirty,表示值是不是脏的默认是 true,取值后是 false,再次取值时 dirty 还是 false 直接将还是上一次的取值返回。

    // src\core\instance\state.js computed 取值函数
    function createComputedGetter (key) {
      return function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          if (watcher.dirty) {  // 判断值是不是脏 dirty
            watcher.evaluate()
          }
          if (Dep.target) {
            watcher.depend()
          }
          return watcher.value
        }
      }
    }
    
    
    // src\core\instance\state.js watch 实现
      Vue.prototype.$watch = function (
        expOrFn: string | Function,
        cb: any,
        options?: Object
      ): Function {
        const vm: Component = this
        if (isPlainObject(cb)) {
          return createWatcher(vm, expOrFn, cb, options)
        }
        options = options || {}
        options.user = true
        // 实例化 watcher
        const watcher = new Watcher(vm, expOrFn, cb, options)
        if (options.immediate) {
          const info = `callback for immediate watcher "${watcher.expression}"`
          pushTarget()
          invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
          popTarget()
        }
        return function unwatchFn () {
          watcher.teardown()
        }
      }
    

    参考

    Vue 模板编译原理

    Vue.nextTick 的原理和用途

    结束

    谢谢大家阅读到这里,如果觉得写的还可以,欢迎三连呀,我是林一一,下次见。

    相关文章

      网友评论

        本文标题:Vue 高频原理面试篇+详细解答

        本文链接:https://www.haomeiwen.com/subject/klvqsltx.html