【vue】响应式(object.defineProperty)、可配置的参数、vue渲染机制

Vue 2的响应式原理主要是基于Object.defineProperty来实现的。

  1. 数据劫持

    • 当一个Vue实例被创建时,它会遍历data选项中的所有属性。对于每个属性,使用Object.defineProperty来进行数据劫持。这个方法允许精确地定义一个对象的属性,包括属性的值、可枚举性、可配置性和可写性。
    • 重点是定义了属性的getset函数。get函数用于获取属性的值,在这个函数内部可以进行一些依赖收集的操作。例如,当模板中使用了这个属性,就会在get被调用时,将这个模板中的使用点(watcher)收集起来。set函数用于设置属性的值,当属性的值被修改时,set函数会被触发。在set函数内部,会通知之前收集到的所有依赖(watcher)进行更新,从而实现数据变化到视图更新的过程。
  2. 依赖收集和通知更新(Watcher和Dep)

    • Dep(Dependency) :可以看作是一个依赖收集器,是一个发布者。它主要用于收集依赖(watcher),在数据劫持的get中,会将当前的watcher添加到Dep实例中。每个响应式数据都会有一个对应的Dep实例。
    • Watcher :是一个订阅者,主要用于观察数据的变化。它可以是一个组件渲染的观察者,也可以是一个用户自定义的计算属性的观察者等。当数据发生变化时,Dep会通知所有订阅了它的WatcherWatcher收到通知后,会执行相应的更新操作,如更新组件的DOM节点,重新计算计算属性的值等。

简单来说,当数据发生变化时,通过set触发Dep发布消息,Watcher收到消息后更新视图,而视图中的数据绑定表达式被解析时,会触发get进行依赖收集,这就实现了数据和视图之间的响应式绑定。

Object.defineProperty()方法中,用于定义对象属性时可以配置以下参数:

  1. value(可选)
    • 这是属性的值。例如:
javascript 复制代码
let obj = {};
Object.defineProperty(obj, 'name', {
    value: 'John'
});
console.log(obj.name); // 输出:John
  • 如果没有提供value参数,并且在对象中该属性不存在,那么这个属性的值将是undefined
  1. writable(可选)
    • 它是一个布尔值,用于确定属性的值是否可以被修改。默认值是false。例如:
javascript 复制代码
let obj = {};
Object.defineProperty(obj, 'age', {
    value: 30,
    writable: true
});
obj.age = 31;
console.log(obj.age); // 输出:31
  • 如果writablefalse,尝试修改属性的值将会在严格模式下抛出错误,在非严格模式下修改操作会被忽略。
  1. enumerable(可选)
    • 这也是一个布尔值,用于决定属性是否可以被枚举,比如在for...in循环或者Object.keys()方法中是否会出现。默认值是false。例如:
javascript 复制代码
let obj = {};
Object.defineProperty(obj, 'city', {
    value: 'New York',
    enumerable: true
});
for (let key in obj) {
    console.log(key); // 输出:city
}
  • 如果enumerablefalse,该属性在上述枚举操作中不会出现,但仍然可以通过直接访问属性名来获取其值。
  1. configurable(可选)
    • 同样是布尔值,它决定了这个属性是否可以被删除,以及是否可以重新配置属性的其他特性(除了valuewritable被设置为false后的情况)。默认值是false。例如:
javascript 复制代码
let obj = {};
Object.defineProperty(obj, 'country', {
    value: 'USA',
    configurable: true
});
delete obj.country;
console.log(obj.country); // 输出:undefined
  • 如果configurablefalse,尝试删除属性或者重新配置其特性(如改变enumerable等)将会在严格模式下抛出错误,在非严格模式下操作会被忽略。

以下是一些Vue上层相关的底层面试题:

一、关于Vue实例

  1. Vue实例的生命周期钩子函数
    • 问题:请简述Vue实例的生命周期钩子函数有哪些,以及它们分别在什么时候被调用?
    • 答案:
      • beforeCreate:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。此时,datamethodscomputed等都还不可用。
      • created:在实例创建完成后被调用。此时实例已完成以下的配置:数据观测 (data observer)、属性和方法的运算、watch/event 事件回调。然而,挂载阶段还没开始,$el 不可用。
      • beforeMount:在挂载开始之前被调用。相关的 render 函数首次被调用。
      • mountedel 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。一般在此进行DOM操作的初始化,例如获取元素的高度、宽度等操作,因为此时DOM已经渲染完成。
      • beforeUpdate:数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前。此时DOM还没更新。
      • updated:由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时DOM已经更新。
      • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。
      • destroyed:Vue实例销毁后调用。此时,Vue实例的所有指令都被解绑,所有的事件监听器被移除,子实例也被销毁。
  2. Vue实例的数据代理原理
    • 问题:请解释Vue是如何实现数据代理的?
    • 答案:
      • Vue通过Object.defineProperty()方法来实现数据代理。在Vue实例创建时,它会遍历data选项中的属性。
      • 对于每个属性,它会在Vue实例上定义一个同名的属性(如this.foo对应data中的foo属性)。
      • 这个同名属性的getset访问器函数被定义为读取和修改data中对应属性的值。当读取this.foo时,实际上是调用datafoo属性的get访问器;当修改this.foo时,是调用datafoo属性的set访问器。

二、关于Vue组件

  1. 组件通信方式
    • 问题:Vue组件有哪些通信方式?请举例说明。
    • 答案:
      • 父子组件通信
        • 父传子(props) :父组件通过props向子组件传递数据。例如,父组件有一个message属性,在子组件中通过props: ['message']接收,然后在子组件的模板中可以使用{``{message}}显示。
        • **子传父( e m i t ) ∗ ∗ :子组件通过 ' emit)**:子组件通过` emit)∗∗:子组件通过'emit触发事件向父组件传递数据。例如,子组件中有一个按钮,点击按钮时this.$emit('child - event', data),父组件通过@child - event="parentMethod"监听这个事件,并在parentMethod`方法中接收子组件传递的数据。
      • 非父子组件通信(兄弟组件或跨多层级组件通信)
        • 中央事件总线(Event Bus) :创建一个Vue实例作为事件总线。例如,var bus = new Vue()。一个组件可以通过bus.$emit('event - name', data)发送事件,其他组件通过bus.$on('event - name', callback)接收事件和数据。
        • Vuex :对于大型项目中复杂的状态管理和组件通信,使用Vuex。它有state(存储状态)、mutations(修改状态的方法)、actions(异步操作)和getters(获取状态的派生数据)。组件可以通过this.$store.state访问状态,通过this.$store.commit('mutation - name', data)提交修改状态的操作等。
  2. 组件的插槽(Slots)
    • 问题:请解释Vue组件中的插槽有哪些类型,以及它们的作用?
    • 答案:
      • 默认插槽(单个插槽)
        • 当在父组件中使用子组件时,如果在子组件标签内放置内容,这些内容会被渲染到子组件中的<slot>标签所在位置。例如,子组件ChildComponent中有一个<slot></slot>,父组件<ChildComponent>这是父组件传递的内容</ChildComponent>,"这是父组件传递的内容"就会被渲染到子组件的<slot>处。
      • 具名插槽
        • 当子组件有多个不同位置需要父组件填充内容时,可以使用具名插槽。在子组件中定义<slot name="header"></slot><slot name="footer"></slot>等。父组件使用<template v - slot:header><template v - slot:footer>来分别向对应的具名插槽传递内容。
      • 作用域插槽
        • 作用域插槽用于让父组件能够访问子组件中的数据。子组件在<slot>标签上绑定数据,如<slot :data="childData"></slot>。父组件通过<template v - slot="slotProps">slotProps是自定义的变量名)来接收子组件传递的数据,并在父组件中可以根据这些数据进行渲染,如{``{slotProps.childData}}

三、关于Vue的渲染机制(除了Diff算法相关)

  1. 模板编译过程
    • 问题:简述Vue模板的编译过程。
    • 答案:
      • 解析(parse) :将模板字符串解析成抽象语法树(AST)。这个过程会把模板中的HTML标签、指令、插值表达式等解析成JavaScript对象的形式。例如,模板<div>{``{message}}</div>会被解析成一个包含tag: 'div'text: nullchildren: [ { type: 2, expression:'message', text: '{``{message}}' } ]等属性的AST对象。
      • 优化(optimize):对解析后的AST进行优化,标记静态节点。静态节点是指在组件的生命周期内不会改变的节点,标记静态节点可以在后续的虚拟DOM更新过程中跳过这些节点的比较,提高性能。
      • 生成(generate) :将优化后的AST转换成渲染函数(render函数)。渲染函数返回虚拟DOM(VNode),例如with(this){return _c('div',[_v(_s(message))])},其中_c_v_s等是Vue内部的渲染函数助手,用于创建VNode、创建文本节点、将数据转换为文本等操作。
  2. 渲染函数(Render Function)与模板的区别和联系
    • 问题:请说明Vue中渲染函数与模板的区别和联系,以及在什么情况下会选择使用渲染函数?
    • 答案:
      • 区别
        • 语法形式 :模板是基于HTML的语法,通过指令(如v - ifv - for等)来添加逻辑;渲染函数是纯JavaScript函数,通过调用Vue内部的渲染函数助手来创建虚拟DOM。
        • 灵活性:渲染函数比模板更灵活。模板有一定的语法限制,而渲染函数可以实现更复杂的逻辑和渲染效果。例如,在模板中很难实现根据不同条件动态生成不同的HTML结构,但在渲染函数中可以通过JavaScript的条件判断来实现。
      • 联系
        • 模板最终会被编译成渲染函数。无论是使用模板还是直接编写渲染函数,目的都是生成虚拟DOM来渲染页面。
      • 选择使用渲染函数的情况
        • 当需要实现高度定制化的组件,模板的语法无法满足需求时,例如需要根据复杂的业务逻辑动态生成DOM结构。
        • 当需要对组件的渲染性能进行极致优化时,因为渲染函数可以更精确地控制虚拟DOM的生成和更新,通过优化渲染函数可以减少不必要的DOM操作。
相关推荐
GISer_Jing2 分钟前
Next.js数据获取演进史
java·开发语言·javascript
1024小神23 分钟前
uniapp+vue3+vite+ts+xr-frame实现ar+vr渲染踩坑记
前端
测试界清流26 分钟前
基于pytest的接口测试
前端·servlet
知识分享小能手1 小时前
微信小程序入门学习教程,从入门到精通,自定义组件与第三方 UI 组件库(以 Vant Weapp 为例) (16)
前端·学习·ui·微信小程序·小程序·vue·编程
trsoliu1 小时前
多仓库 Workspace 协作机制完整方案
前端
啦工作呢1 小时前
数据可视化 ECharts
前端·信息可视化·echarts
NoneSL1 小时前
Uniapp UTS插件开发实战:引入第三方SDK
前端·uni-app
trsoliu1 小时前
Claude Code Templates
前端·人工智能
wangpq1 小时前
使用rerender-spa-plugin在构建时预渲染静态HTML文件优化SEO
前端·javascript·vue.js
KongHen1 小时前
完美解决请求跨域问题
前端