乐趣区

关于javascript:一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
  • 骨架屏加载
退出移动版