• (一)Vue常见面试题,看看你都会了吗?


    怎样封装一个组件?

    //父组件
    <template>
        <div>
         <h1>{{title}}</h1>
         <child :name="name" :age="age" :hobby="hobby" @titleChanged="titleChanged"></child>
        </div>
    </template>
    import child from "./components/child"
    export default {
       name: 'App',
       data(){
         return{
             title: '父级内容',
            name: 'hello',
           age: 19,
            hobby: ['swim','run','walk']
         }
       },
       components:{
         "child":child
       },
        titleChanged(val){
            this.title = val;
        }
    }       
    
    //子组件
    <template>
      <div class="hello">
        <h3>{{name}}</h3>
     <p>{{age}}</p>
        <ul>
          <li v-for="h in hobby">{{h}}</li> //遍历传递过来的值,然后呈现到页面
        </ul>
     <button @click="changeTitle">向父级传值</button>
      </div>
    </template>
    <script>
    export default {
     name: 'HelloWorld',
       props:{
         users:{           //这个就是父组件中子标签自定义名字
            type:String,
            required:true
         },
         age: {
          type: Number,
             default: 0,
         },
         hobby: {
             type: Array,
             defautl: ()=>[]
         }
       },
       methods: {
           changeTitle(){
               this.$emit("titleChanged","子向父组件传值");
           }
       }
    }
    </script>
    

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

    Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。

    function observe (obj) { // 我们来用它使对象变成可观察的
      // 判断类型
      if (!obj || typeof obj !== 'object') {
        return
      }
      Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
      })
      function defineReactive (obj, key, value) {
        // 递归子属性
        observe(value)
        Object.defineProperty(obj, key, {
          enumerable: true, //可枚举(可以遍历)
          configurable: true, //可配置(比如可以删除)
          get: function reactiveGetter () {
            console.log('get', value) // 监听
            return value
          },
          set: function reactiveSetter (newVal) {
            observe(newVal) //如果赋值是一个对象,也要递归子属性
            if (newVal !== value) {
              console.log('set', newVal) // 监听
              render()
              value = newVal
            }
          }
        })
      }
    }
    

    observe这个函数传入一个 obj(需要被追踪变化的对象),通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理,以此来达到实现侦测对象变化。值得注意的是,observe 会进行递归调用。

    因为 Vue 通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。如果是删除属性,我们可以用vm.$delete实现,那如果是新增属性,该怎么办呢?

    1. 可以使用 Vue.set(location, a, 1) 方法向嵌套对象添加响应式属性;
    2. 也可以给这个对象重新赋值,比如data.location = {...data.location,a:1}

    Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写,具体代码如下:

    let methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push'];
    // 先获取到原来的原型上的方法
    let arrayProto = Array.prototype;
    // 创建一个自己的原型 并且重写methods这些方法
    let proto = Object.create(arrayProto)
    methods.forEach(method => {
      proto[method] = function() {
        // AOP
        arrayProto[method].call(this, ...arguments)
        render()
      }
    })
    function observer(obj) {
      // 把所有的属性定义成set/get的方式
      if (Array.isArray(obj)) {
        obj.__proto__ = proto
        return
      }
      if (typeof obj == 'object') {
        for (let key in obj) {
          defineReactive(obj, key, obj[key])
        }
      }
    }
    function defineReactive(data, key, value) {
      observer(value)
      Object.defineProperty(data, key, {
        get() {
          return value
        },
        set(newValue) {
          observer(newValue)
          if (newValue !== value) {
            render()
            value = newValue
          }
        }
      })
    }
    observer(obj)
    function $set(data, key, value) {
      defineReactive(data, key, value)
    }
    

    这种方法将数组的常用方法进行重写,进而覆盖掉原生的数组方法,重写之后的数组方法需要能够被拦截。

    vue中模板编译原理?

    关于 Vue 编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,这三个部分是有前后关系的:

    第一步是将 模板字符串 转换成 element ASTs(解析器)

    
    <div>
      <p>{{name}}</p>
    </div>
    

    上面这样一个简单的 模板 转换成 element AST 后是这样的:

    {
      tag: "div"
      type: 1,
      staticRoot: false,
      static: false,
      plain: true,
      parent: undefined,
      attrsList: [],
      attrsMap: {},
      children: [
          {
          tag: "p"
          type: 1,
          staticRoot: false,
          static: false,
          plain: true,
          parent: {tag: "div", ...},
          attrsList: [],
          attrsMap: {},
          children: [{
              type: 2,
              text: "{{name}}",
              static: false,
              expression: "_s(name)"
          }]
        }
      ]
    }
    

    这段模板字符串会扔到 while 中去循环,然后 一段一段 的截取,把截取到的 每一小段字符串 进行解析,直到最后截没了,也就解析完了

    第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
    优化器的目标是找出那些静态节点并打上标记,而静态节点指的是 DOM 不需要发生变化的节点。

    每次重新渲染的时候不需要为静态节点创建新节点;在 Virtual DOM 中 patching 的过程可以被跳过。

    第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)

    {
      render: `with(this){return _c('div',[_c('p',[_v(_s(name))])])}`
    }
    

    通过递归去拼一个函数执行代码的字符串,递归的过程根据不同的节点类型调用不同的生成方法,如果发现是一颗元素节点就拼一个 _c(tagName, data, children) 的函数调用字符串,然后 datachildren 也是使用 AST 中的属性去拼字符串。

    如果 children 中还有 children 则递归去拼;最后拼出一个完整的 render 函数代码。

    vue中diff的原理?

    渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?diff算法能够帮助我们。

    我们先根据真实DOM生成一颗virtual DOM,当virtual DOM某个节点的数据改变后会生成一个新的Vnode,然后VnodeoldVnode作对比,发现有不一样的地方就直接修改在真实的DOM上,然后使oldVnode的值为Vnode

    diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。

    使用双指针形式,对虚拟节点进行比对,对相同的节点进行复用,对发生变化的节点进行patch。以下是vue源码中对虚拟节点的比对方式:

    function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
        let oldStartIdx = 0 // 旧头索引
        let newStartIdx = 0 // 新头索引
        let oldEndIdx = oldCh.length - 1 // 旧尾索引
        let newEndIdx = newCh.length - 1 // 新尾索引
        let oldStartVnode = oldCh[0] // oldVnode的第一个child
        let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
        let newStartVnode = newCh[0] // newVnode的第一个child
        let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
        let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    
        // removeOnly is a special flag used only by <transition-group>
        // to ensure removed elements stay in correct relative positions
        // during leaving transitions
        const canMove = !removeOnly
    
        // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          // 如果oldVnode的第一个child不存在
          if (isUndef(oldStartVnode)) {
            // oldStart索引右移
            oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    
          // 如果oldVnode的最后一个child不存在
          } else if (isUndef(oldEndVnode)) {
            // oldEnd索引左移
            oldEndVnode = oldCh[--oldEndIdx]
    
          // oldStartVnode和newStartVnode是同一个节点
          } else if (sameVnode(oldStartVnode, newStartVnode)) {
            // patch oldStartVnode和newStartVnode, 索引左移,继续循环
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
    
          // oldEndVnode和newEndVnode是同一个节点
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
            // patch oldEndVnode和newEndVnode,索引右移,继续循环
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
    
          // oldStartVnode和newEndVnode是同一个节点
          } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
            // patch oldStartVnode和newEndVnode
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
            // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
            // oldStart索引右移,newEnd索引左移
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
    
          // 如果oldEndVnode和newStartVnode是同一个节点
          } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
            // patch oldEndVnode和newStartVnode
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
            // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            // oldEnd索引左移,newStart索引右移
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
    
          // 如果都不匹配
          } else {
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
    
            // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
            idxInOld = isDef(newStartVnode.key)
              ? oldKeyToIdx[newStartVnode.key]
              : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
    
            // 如果未找到,说明newStartVnode是一个新的节点
            if (isUndef(idxInOld)) { // New element
              // 创建一个新Vnode
              createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
    
            // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
            } else {
              vnodeToMove = oldCh[idxInOld]
              /* istanbul ignore if */
              if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
                warn(
                  'It seems there are duplicate keys that is causing an update error. ' +
                  'Make sure each v-for item has a unique key.'
                )
              }
    
              // 比较两个具有相同的key的新节点是否是同一个节点
              //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
              if (sameVnode(vnodeToMove, newStartVnode)) {
                // patch vnodeToMove和newStartVnode
                patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
                // 清除
                oldCh[idxInOld] = undefined
                // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
                // 移动到oldStartVnode.elm之前
                canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
    
              // 如果key相同,但是节点不相同,则创建一个新的节点
              } else {
                // same key but different element. treat as new element
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
              }
            }
    
            // 右移
            newStartVnode = newCh[++newStartIdx]
          }
        }
    

    vue的渲染流程?

    从模板到真实dom节点还需要经过一些步骤

    把模板编译为render函数;

    实例进行挂载, 根据根节点render函数的调用,递归的生成虚拟dom;

    对比虚拟dom,渲染到真实dom;

    组件内部data发生变化,组件和子组件引用data作为props重新调用render函数,生成虚拟dom, 返回到步骤3。

    为何vue采用异步渲染?

    如果不采取异步更新,那么每次更新数据都会对当前组件进行重新渲染,为了性能考虑,Vue 会在本轮数据更新后,再去异步更新数据。

    原理:

    • dep.notify() 通知 watcher 进行更新操作
    • subs[i].update() 依次调用 watcher 的 update
    • queueWatcher将 watcher 重新放到队列中
    • nextTick(flushSchedulerQueue) 异步清空 watcher 队列

    谈一谈你对Vue性能优化的理解 ?

    主要包括:上线代码包打包、源码编写优化、用户体验优化。

    1.代码包优化

    • 屏蔽sourceMap
    • 对项目代码中的JS/CSS/SVG(*.ico)文件进行gzip压缩
    • 对路由组件进行懒加载

    2.源码优化

    • v-if 和 v-show选择调用
    • 为item设置唯一key值
    • 细分vuejs组件
    • 减少watch的数据
    • 内容类系统的图片资源按需加载
    • SSR(服务端渲染)

    3.用户体验优化

    • 菊花loading
    • 骨架屏加载
  • 相关阅读:
    Java 中String、StringBuffer、StringBuilder的差别
    [转载]StringBuffer versus String
    二维数组的连续子数组的最大和
    一位数组的最大和
    js中常见的去重方式
    魅族2016Java互联网方向其中一道笔试题--青蛙跳台阶问题
    美团在线编程2016--最大差值
    [转载]MySQL开发中常用的查询语句总结
    实现字符串全排列
    笔试题---最大子序列和
  • 原文地址:https://www.cnblogs.com/jiaoshou/p/14092523.html
Copyright © 2020-2023  润新知