likes
comments
collection
share

常见Vue原理面试题,看你翅膀硬了没

作者站长头像
站长
· 阅读数 19

1. Vue的响应式原理是什么?请详细说明Object.defineProperty()和Proxy的区别和用法。

响应式原理:Vue中采用了数据劫持的方式,通过Object.defineProperty()函数来监听数据变化,并在数据变化时触发对应的更新函数。 Object.defineProperty()与Proxy的区别:前者只能监听属性的读取和修改,后者可以监听数组的变化等更多场景,且性能更高。


// 实现observe函数,对data对象中的所有属性进行数据劫持
function observe(data) {
  if (!data || typeof data !== 'object') {
    return
  }
  Object.keys(data).forEach(key => {
    defineReactive(data, key, data[key])
  })
}

// 定义defineReactive函数,通过Object.defineProperty()函数来监听数据变化
function defineReactive(obj, key, val) {
  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value:', val)
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('set value:', newVal)
      val = newVal
    }
  })
}

// 测试代码
const obj = { name: 'Tom', age: 18 }
observe(obj)
obj.age = 20
console.log(obj.age) // 输出:20

2. Vue的模板编译原理是什么?请说明它的优化策略和实现方式。并手动实现一个简单的模板编译器。

模板编译原理:Vue中将用户写好的模板转换成渲染函数,实际渲染时调用该函数进行渲染。模板编译主要包括三个阶段:解析、优化和生成。

优化策略:Vue在模板编译阶段会对模板进行静态节点标记和静态根节点标记,从而可以避免不必要的重复渲染和提升整体渲染性能。

实现方式:Vue通过将模板解析成抽象语法树(AST),再转换成render函数的方式来实现模板编译。最终生成的render函数就是一个虚拟DOM的描述对象,然后通过虚拟DOM的diff算法进行渲染更新。

// 简化版的 Vue 模板编译器
function compile(template) {
  var element = document.createElement('div');
  element.innerHTML = template;

  Array.from(element.childNodes).forEach(function(node) {
    if (node.nodeType === Node.TEXT_NODE) {
      var reg = /\{\{(.+)\}\}/;
      var match = node.textContent.match(reg);
      if (match) {
        var key = match[1].trim();
        node.textContent = '{{' + key + '}}';
        node._key = key;
      }
    } else if (node.nodeType === Node.ELEMENT_NODE) {
      Array.from(node.attributes).forEach(function(attr) {
        if (attr.name.startsWith('v-')) {
          if (attr.name === 'v-model') {
            node.value = '';
            node.addEventListener('input', function(event) {
              vm[attr.value] = event.target.value;
            });
          }
          node.removeAttribute(attr.name);
        }
      });
    }

    if (node.childNodes.length > 0) {
      compile(node.innerHTML);
    }
  });

  return element.innerHTML;
}

var vm = new Vue({
  el: '#app',
  data: {
    message: 'Hello, World!'
  },
  template: '<div><h1>{{message}}</h1></div>'
});

document.getElementById('app').innerHTML = compile(vm.template);

3. Vue中的虚拟DOM算法是什么?请说明其原理和优化策略。

虚拟DOM算法:Vue中通过比较新旧虚拟DOM树之间的差异来最小化更新操作,从而提高渲染效率。虚拟DOM算法主要包括两个步骤:Diff算法和更新操作。

原理:Vue将模板编译成虚拟DOM树,并将其与上一个虚拟DOM树进行比较,找出需要更新的节点并进行更新操作。

优化策略:Vue采用了一些策略来减少比较的次数,优化了虚拟DOM树的构建和比较的性能。


// 旧的虚拟DOM树
const prevVNode = {
  tag: 'div',
  props: { id: 'container' },
  children: [
    { tag: 'h1', children: 'Hello, World!' },
    { tag: 'p', children: 'Welcome to my website.' }
  ]
}

// 新的虚拟DOM树
const nextVNode = {
  tag: 'div',
  props: { id: 'container' },
  children: [
    { tag: 'h1', children: 'Hello, Vue!' },
    { tag: 'p', children: 'Welcome to the Vue world.' }
  ]
}

// 执行diff算法,得到需要更新的节点
function diff(prevVNode, nextVNode) {
  if (prevVNode.tag === nextVNode.tag) {
    const patchProps = {}
    const prevChildren = prevVNode.children
    const prevProps = prevVNode.props
    const nextChildren = nextVNode.children
    const nextProps = nextVNode.props

    // diff props
    for (const key in nextProps) {
      const prevVal = prevProps[key]
      const nextVal = nextProps[key]
      if (prevVal !== nextVal) {
        patchProps[key] = nextVal
      }
    }

    // diff children
    if (prevChildren.length !== nextChildren.length) {
      patchProps.children = nextChildren
    } else {
      const patchChildren = []
      for (let i = 0; i < nextChildren.length; i++) {
        const childPatch = diff(prevChildren[i], nextChildren[i])
        patchChildren.push(childPatch)
      }
      patchProps.children = patchChildren
    }

    return { type: 'update', props: patchProps }
  } else {
    return { type: 'replace', node: nextVNode }
  }
}

// 执行更新操作,将差异应用到真实DOM树上
function patch(node, patchProps) {
  switch (patchProps.type) {
    case 'replace':
      const newNode = createDOMElement(patchProps.node)
      node.parentNode.replaceChild(newNode, node)
      break
    case 'update':
      updateDOMProps(node, patchProps.props)
      for (const patchChild of patchProps.children) {
        patch(node.children[patchChild.index], patchChild.props)
      }
      break
  }
}

4. Vue中的组件通信有哪些方式?请分别说明它们的特点和应用场景。

组件通信方式:

  1. 父子组件通信:通过props传递数据和事件监听。 特点:简单易用,适用于父子组件之间的数据交互。 应用场景:父子组件之间的状态传递和操作。

  2. 子父组件通信:通过 $emit 触发自定义事件和 $on 监听事件。 特点:适用于子组件向父组件传递数据和状态变更。 应用场景:子组件向父组件传递用户输入或其他数据。

  3. 兄弟组件通信:通过$emit/ $on或vuex进行数据共享。 特点:适用于兄弟组件之间的状态共享和协同工作。 应用场景:多个组件之间需要共享状态或数据。

  4. 跨级组件通信:通过provide/ inject进行跨级数据传递。 特点:适用于祖先组件向后代组件传递数据。 应用场景:多级嵌套组件之间的状态共享和传递。


// 子组件
<template>
  <div>
    <h1>{{ title }}</h1>
    <button @click="handleClick">点击我</button>
  </div>
</template>
<script>
export default {
  props: ['title'],
  methods: {
    handleClick() {
      this.$emit('change', '子组件按钮被点击了')
    }
  }
}
</script>

// 父组件
<template>
  <div>
    <h2>{{ subtitle }}</h2>
    <child :title="title" @change="handleChange"></child>
  </div>
</template>
<script>
import Child from './Child.vue'
export default {
  components: { Child },
  data() {
    return {
      title: '父子组件通信示例',
      subtitle: ''
    }
  },
  methods: {
    handleChange(msg) {
      this.subtitle = msg
    }
  }
}
</script>

5. Vue中的v-model指令在表单元素上的使用方式有哪些?请分别说明它们的区别和注意事项。

v-model指令在表单元素上的使用方式:

  1. v-model="value":用于单选框、复选框和选择框的数据绑定,绑定的是选择的值。 区别:单选框和复选框绑定的是选中状态,而选择框绑定的是选中的值。 注意事项:当多个单选框或复选框绑定同一个数据时,需要为每个元素添加不同的value属性。

  2. v-model="value":用于输入框等表单元素的双向数据绑定,绑定的是输入框的值。 区别:v-model与value属性的实现方式不同,v-model实现了双向绑定的效果。 注意事项:需要为输入框添加type属性,并且该元素必须支持input事件或change事件。


// 单选框
<template>
  <div>
    <input type="radio" v-model="gender" value="male">
    <input type="radio" v-model="gender" value="female">
  </div>
</template>
<script>
export default {
  data() {
    return {
      gender: ''
    }
  }
}
</script>

// 输入框
<template>
  <div>
    <input type="text" v-model="message">
    <p>输入的内容是:{{ message }}</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      message: ''
    }
  }
}
</script>

6. Vue中的计算属性computed和侦听器watch有什么区别?请举例说明它们的使用场景。

computed和watch的区别:

  1. computed是基于依赖缓存的,只有当依赖的数据发生变化时才会重新计算;watch则是通过监听数据变化来触发回调函数。
  2. computed适用于多个值之间的计算和处理,而watch则适用于单独的数据变化后执行异步或复杂的操作。

// 计算属性
<template>
  <div>
    <p>商品数量:{{ quantity }}</p>
    <p>商品总价:{{ totalPrice }}</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      price: 10,
      quantity: 3
    }
  },
  computed: {
    totalPrice() {
      return this.price * this.quantity
    }
  }
}
</script>

// 侦听器
<template>
  <div>
    <input type="text" v-model="message">
    <p>输入的消息是:{{ message }}</p>
  </div>
</template>
<script>
export default {
  data() {
    return {
      message: ''
    }
  },
  watch: {
    message(newVal, oldVal) {
      console.log('输入的消息发生了变化:', newVal, oldVal)
      // 执行异步操作
      this.$http.get('/api/messages', { params: { message: newVal } }).then(res => {
        console.log(res.data)
      })
    }
  }
}
</script>

7. 简述 Vue 中的异步更新队列原理。

Vue 在更新视图时会将所有数据变化的 watcher(观察者)加入到一个异步更新队列中,等到下一个事件循环时才执行更新操作。这样做的好处是避免频繁的更新视图,提高性能。

  1. 在 watcher 对象构造函数中,将当前 watcher 对象 push 到全局的异步更新队列 queue 中;
  2. 在下一个事件循环方式中,通过 flushQueue 函数逐个遍历 queue 数组中的 watcher,调用其 run 方法进行更新;
  3. 清空 queue 数组,以待下一次更新。
// 定义全局异步更新队列
var queue = [];

// 异步更新队列处理函数
function flushQueue() {
  queue.forEach(function(watcher) {
    watcher.run();
  });
  queue = [];
}

// 定义 Watcher 类
class Watcher {
  constructor() {
    queue.push(this); // 将当前 watcher 加入到异步更新队列中
  }

  run() {
    console.log('更新视图!'); // 执行更新操作
  }
}

// 创建多个 Watcher 对象
var watcher1 = new Watcher();
var watcher2 = new Watcher();

// 延迟一定时间,再执行异步更新操作
setTimeout(function() {
  flushQueue(); // 清空异步更新队列,逐个执行更新
}, 0);

8. 简述 Vue 中的事件机制原理,并手动实现一个简单的事件总线。

Vue 中的事件机制是利用事件总线 EventBus 进行封装实现的。事件总线将事件的发送和接收解耦,通过一个中心化的事件分发器(Event Bus)来管理,使得多个组件间的通信变得简单而灵活。

// 定义 EventBus 类
class EventBus {
  constructor() {
    this.events = {};
  }

  $on(name, callback) {
    if (!this.events[name]) {
      this.events[name] = [];
    }
    this.events[name].push(callback);
  }

  $emit(name, ...args) {
    if (this.events[name]) {
      this.events[name].forEach(function(callback) {
        callback(...args);
      });
    }
  }
}

// 创建 EventBus 实例
var bus = new EventBus();

// 定义事件处理函数
function handleMessage(message) {
  console.log(`收到消息:${message}`);
}

// 绑定 message 事件处理函数
bus.$on('message', handleMessage);

// 触发 message 事件,并传递参数
bus.$emit('message', 'Hello, World!');

常见Vue原理面试题,看你翅膀硬了没