当前位置: 首页 > news >正文

Java面试题036:一文深入了解VUE(1)

1、vue简介

        Vue是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型。

 Vue 的两个核心功能:

  • 声明式渲染:Vue 基于标准 HTML 拓展了一套模板语法,可以声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。

  • 响应性:Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM。

Vue 2 发布于  2016 年,已于 2023 年 12 月 31 日停止维护。不再会有新增功能、更新或问题修复。自 2022 年 2 月 7 日起,Vue 3 已成为 Vue 的默认版本。

  • 更小的包大小和更快的渲染速度带来的更好的性能。
  • 增强的 TypeScript 支持,使大规模应用开发更轻松。
  • 基于 Proxy 的更高效的响应性系统。
  • 新的内置组件,如 Fragment、Teleport 和 Suspense。
  • 改进的构建工具支持和 Vue Devtools 体验。

2、选项式 API和组合式 API

选项式 API:可以用包含多个选项的对象来描述组件的逻辑,例如 datamethods 和 mounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。

<script>
export default {// data() 返回的属性将会成为响应式的状态// 并且暴露在 `this` 上data() {return {count: 0}},// methods 是一些用来更改状态与触发更新的函数// 它们可以在模板中作为事件处理器绑定methods: {increment() {this.count++}},// 生命周期钩子会在组件生命周期的各个不同阶段被调用// 例如这个函数就会在组件挂载完成后被调用mounted() {console.log(`The initial count is ${this.count}.`)}
}
</script><template><button @click="increment">Count is: {{ count }}</button>
</template>

组合式 API:使用导入的 API 函数来描述组件逻辑。在单文件组件中,组合式 API 通常会与 <script setup> 搭配使用。这个 setup attribute 是一个标识,告诉 Vue 需要在编译时进行一些处理。

<script setup>
import { ref, onMounted } from 'vue'// 响应式状态
const count = ref(0)// 用来修改状态、触发更新的函数
function increment() {count.value++
}// 生命周期钩子
onMounted(() => {console.log(`The initial count is ${count.value}.`)
})
</script><template><button @click="increment">Count is: {{ count }}</button>
</template>

两者对比:

(1)对初学者而言,选项式更为友好。

(2)组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,对 Vue 的响应式系统有更深的理解才能高效使用,它的灵活性也使得组织和重用逻辑的模式变得更加强大。

3、响应式原理

        也叫作数据双向绑定,是通过数据劫持侦测数据变化,发布订阅模式进行依赖收集与视图更新来实现的。

        Vue 利用 Object.defineProperty 创建一个 observe 来劫持监听所有的属性,把这些属性全部转为 getter 和 setter。Vue 中每个组件实例都会对应一个 watcher 实例,它会在组件渲染的过程中把使用过的数据属性通过 getter 收集为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

详细过程:

  1.  在 new Vue() 后, Vue 会调用 _init 函数进行初始化,也就是init 过程,在 这个过程Data通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行 getter 函数,而在当被赋值的时候会执行 setter函数。

  2. 当render function 执行的时候,因为会读取所需对象的值,所以会触发getter函数从而将Watcher添加到依赖中进行依赖收集。

  3. 在修改对象的值的时候,会触发对应的 setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新视图。

Watcher的作用:

        Vue 中定义一个 Watcher 类来表示观察订阅依赖,当属性发生变化后,我们要通知用到数据的地方,而使用这个数据的地方有很多,而且类型还不一样,既有可能是模板,也有可能是用户写的一个watch,这时需要抽象出一个能集中处理这些情况的类。然后,我们在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个,再由它负责通知其他地方。

        收集依赖需要为依赖找一个存储依赖的地方,为此创建了Dep,它用来收集依赖、删除依赖和向依赖发送消息等。它的主要作用是用来存放 Watcher 观察者对象。

        在Javascript中,如何侦测一个对象的变化?有两种办法可以侦测到变化:使用 Object.defineProperty和ES6的 Proxy,这就是进行数据劫持或数据代理。

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

例:使用Object.defineProperty()实现一个响应式功能

<div id="app"><input type="text" id="a"><span id="b"></span>
</div><script type="text/javascript">var data = {   //模拟vue里面的data属性a:1,b:2};var vm={}   //模拟vue实例function defineReactive(vm,key,val){   //defineReactive就是响应式//实现数据双向绑定的方法————数据劫持:当访问或者设置vm中的某一个成员时,做一些干预操作。Object.defineProperty(vm, key, { //参数1:vue实例,参数2:要劫持的属性,参数3:对象(用来获取和设置对象的属性值)//getterget: function() {console.log('get vm '+key+' val:'+ val);document.getElementById('a').value = val;document.getElementById('b').innerHTML = val;return val;},//setterset: function(newVal) {if(val===newVal){return ;}val = newVal;console.log('set vm '+key+' val:'+ val);document.getElementById('a').value = val;document.getElementById('b').innerHTML = val;}});}        document.addEventListener('keyup', function(e) {//触发事件的时机,从而执行相应的操作vm.a=e.target.value});Object.keys(data).forEach(k=>{   //data里面有多个属性时,遍历data的各个属性defineReactive(vm,k,data[k])})
</script>

Vue2存在的问题:

  • Vue2在使用Object.defineProperty()之前,是利用for循环,一个一个遍历data里面的属性,让每一个属性实现响应式,性能比较差。

  • Vue2对于数组数据的响应式(例如利用索引设置数组的值、修改数组长度)是有局限性的(直接通过下标修改数组, 界面不会自动更新 ),但是Vue3就可以。

方法2.Proxy
        Proxy 是 JavaScript 2015 的一个新特性。 Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性, Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,当然对于深层结构,递归还是需要进行的。此外 Proxy支持代理数组的变化。

Vue3通过ES6的代理对象Proxy进行响应式,代理data对象里面所有的属性及数组,访问属性时触发get(),改变属性值时触发set(),然后发布消息给订阅者,重新渲染页面。

<div id="app">
</div>
<script>let data={msg:'Hello',count:0,arr:[1,2,3,4]}//模拟vue实例const vm=new Proxy(data,{  //用Proxy,不用循环就可以遍历到data对象的所有属性,数组也可以更改数值,改变数组长度//执行代理行为的函数//当访问vm的成员会执行get(target,key){   //target就相当于是data对象,key是对象的属性console.log('get key:',key,target[key])document.querySelector('#app').textContent=target[key]return target[key]},//当设置vm的成员会执行set(target,key,newValue){console.log('set key',key,newValue)if(target[key]===newValue){return;}target[key]=newValuedocument.querySelector('#app').textContent=target[key]}})
</script>

Vue3中响应式是通过函数来实现的,包含ref函数和reactive函数。

  • ref():推荐使用接收的数据可以是基本类型也可以是对象类型

import { ref } from 'vue'const count = ref(0)console.log(count) // { value: 0 }
console.log(count.value) // 0

要在组件模板中访问 ref,需要在组件的 setup() 函数中声明并返回,在模板中使用 ref 时,不需要附加 .value

import { ref } from 'vue'export default {// `setup` 是一个特殊的钩子,专门用于组合式 API。setup() {const count = ref(0)// 将 ref 暴露给模板return {count}}
}

        在 setup() 函数中手动暴露大量的状态和方法非常繁琐。可以通过使用单文件组件来避免这种情况。使用 <script setup> 来大幅度地简化代码。

<script setup>
import { ref } from 'vue'const count = ref(0)function increment() {count.value++
}
</script><template><button @click="increment">{{ count }}</button>
</template>
  • reactive():接收一个数组或者对象,返回一个Proxy的实例对象,只能用于对象类型 (对象、数组和如 MapSet 这样的集合类型)。它不能持有如 stringnumber 或 boolean 这样的原始类型。

4、计算属性

        在 Vue.js 开发中,computed计算属性和watch侦听器是处理响应式数据的两个核心工具。

const author = reactive({name: 'John Doe',books: ['Vue 2 - Advanced Guide','Vue 3 - Basic Guide','Vue 4 - The Mystery']
})<p>Has published books:</p>
<span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span>

上述示例使用计算属性改造如下,可以简化模板中的代码:

<script setup>
import { reactive, computed } from 'vue'const author = reactive({name: 'John Doe',books: ['Vue 2 - Advanced Guide','Vue 3 - Basic Guide','Vue 4 - The Mystery']
})// 一个计算属性 ref
const publishedBooksMessage = computed(() => {return author.books.length > 0 ? 'Yes' : 'No'
})
</script><template><p>Has published books:</p><span>{{ publishedBooksMessage }}</span>
</template>

        若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。而方法调用总是会在重渲染发生时再次执行函数。

        下面的计算属性永远不会更新,因为 Date.now() 并不是一个响应式依赖:

const now = computed(() => Date.now())

        计算属性的 getter 应只做计算,不要改变其他状态、在 getter 中做异步请求或者更改 DOM。getter 的职责应该仅为计算和返回派生的值。

        避免直接修改计算属性值,计算属性返回的值是一个“临时快照”,每当源状态发生变化时,就会创建一个新的快照。更改快照是没有意义的,因此计算属性的返回值应该被视为只读的,并且永远不应该被更改——应该更新它所依赖的源状态以触发新的计算。

5、侦听器

        计算属性的 getter 只做计算而没有任何其他的副作用,不会改变其他状态、在 getter 中做异步请求或者更改 DOM。在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。

        Vue 提供了 watch 和 watchEffect 两个函数来创建侦听器。

watch 和 watchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:

  • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。

  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。代码往往更简洁,但有时其响应性依赖关系会不那么明确。

(1) watch 函数

        第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组

const x = ref(0)
const y = ref(0)// 单个 ref
watch(x, (newX) => {console.log(`x is ${newX}`)
})// getter 函数
watch(() => x.value + y.value,(sum) => {console.log(`sum of x + y is: ${sum}`)}
)// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {console.log(`x is ${newX} and y is ${newY}`)
})

不能直接侦听响应式对象的属性值,例如:

const obj = reactive({ count: 0 })// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {console.log(`Count is: ${count}`)
})//需要用一个返回该属性的 getter 函数
// 提供一个 getter 函数
watch(() => obj.count,(count) => {console.log(`Count is: ${count}`)}
)

一次性侦听器

        如果希望回调只在源变化时触发一次,使用 once: true 选项。

watch(source,(newValue, oldValue) => {// 当 `source` 变化时,仅触发一次},{ once: true }
)

即时回调的侦听器

  watch 默认是懒执行的:仅当数据源变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。可以通过传入 immediate: true 选项来强制侦听器的回调立即执行。

watch(source,(newValue, oldValue) => {// 立即执行,且当 `source` 改变时再次执行},{ immediate: true }
)

(2)watchEffect()

  watchEffect 会自动追踪其回调函数中使用的所有响应式数据。watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。

例子:下面的例子中两次使用 todoId ,一次是作为源,另一次是在回调中。

const todoId = ref(1)
const data = ref(null)watch(todoId,async () => {const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)data.value = await response.json()},{ immediate: true }
)

        我们可以用 watchEffect 函数 来简化上面的代码:回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。当 todoId.value 变化时,回调会再次执行。有了 watchEffect(),我们不再需要明确传递 todoId 作为源值。

watchEffect(async () => {const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${todoId.value}`)data.value = await response.json()
})

另一个完整案例:通过 GitHub 的 API 获取最新的 Vue Core 提交信息并将其展示为列表。
可以在两个分支之间切换。

<script setup>
import { ref, watchEffect } from 'vue'const API_URL = `https://api.github.com/repos/vuejs/core/commits?per_page=3&sha=`
const branches = ['main', 'minor']const currentBranch = ref(branches[0])
const commits = ref([])watchEffect(async () => {// 该 effect 会立即运行,// 并且在 currentBranch.value 改变时重新运行const url = `${API_URL}${currentBranch.value}`commits.value = await (await fetch(url)).json()
})function truncate(v) {const newline = v.indexOf('\n')return newline > 0 ? v.slice(0, newline) : v
}function formatDate(v) {return v.replace(/T|Z/g, ' ')
}
</script><template><h1>Latest Vue Core Commits</h1><template v-for="branch in branches"><input type="radio":id="branch":value="branch"name="branch"v-model="currentBranch"><label :for="branch">{{ branch }}</label></template><p>vuejs/core@{{ currentBranch }}</p><ul v-if="commits.length > 0"><li v-for="{ html_url, sha, author, commit } in commits" :key="sha"><a :href="html_url" target="_blank" class="commit">{{ sha.slice(0, 7) }}</a>- <span class="message">{{ truncate(commit.message) }}</span><br>by <span class="author"><a :href="author.html_url" target="_blank">{{ commit.author.name }}</a></span>at <span class="date">{{ formatDate(commit.author.date) }}</span></li></ul>
</template><style>
a {text-decoration: none;color: #42b883;
}
li {line-height: 1.5em;margin-bottom: 20px;
}
.author,
.date {font-weight: bold;
}
</style>

默认情况下,侦听器回调会在父组件更新 (如有) 之后、所属组件的 DOM 更新之前被调用。

如果你尝试在侦听器回调中访问所属组件的 DOM,那么 DOM 将处于更新前的状态。

在 setup() 或 <script setup> 中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。因此,在大多数情况下,无需关心怎么停止一个侦听器。如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。需要异步创建侦听器的情况很少,请尽可能选择同步创建。

<script setup>
import { watchEffect } from 'vue'// 它会自动停止
watchEffect(() => {})// ...这个则不会!
setTimeout(() => {watchEffect(() => {})
}, 100)
</script>
http://www.dtcms.com/a/318071.html

相关文章:

  • 批量提问程序开发方案:基于Python的百度文小言接口实现
  • 学习嵌入式之硬件——ARM体系
  • vue margin与padding对比
  • 用户体验设计中微投入设计:用户不知不觉付出的 3 种方式
  • 【24】C++实战篇——【 C++ 外部变量】 C++多个文件共用一个枚举变量,外部变量 extern,枚举外部变量 enum
  • Kaggle 经典竞赛泰坦尼克号:超级无敌爆炸详细基础逐行讲解Pytorch实现代码,看完保证你也会!!!
  • 直播间自动发言工具的开发
  • OpenAI/gpt-oss开源模型部署与使用全指南
  • 三维偏序 -- cdq 套 cdq
  • 蓝桥杯----锁存器、LED、蜂鸣器、继电器、Motor
  • 【YOLOv8改进 - C2f融合】C2f融合EBlock(Encoder Block):低光增强编码器块,利用傅里叶信息增强图像的低光条件
  • 分类数据集 - 水稻叶病虫害分类数据集下载
  • Java面试宝典:Java内存模型与对象可达性判定原理
  • Python爬虫实战:研究spiderfoot工具,构建网络情报收集系统
  • java 之 继承
  • jdk动态代理如何实现
  • 【补题】Codeforces Round 779 (Div. 2) C. Shinju and the Lost Permutation
  • 【补题】CodeTON Round 1 (Div. 1 + Div. 2, Rated, Prizes!) D. K-good
  • 大数据之HBase
  • 深度学习-卷积神经网络CNN-多输入输出通道
  • MySQL数据库索引及底层数据结构
  • 宝塔部署go 项目
  • Maven--打包方式详解 (pom、war、jar)
  • 各类排序算法
  • FastAPI(未结束)
  • 【React 插件】@uiw/react-md-editor 使用教程:从基础使用到自定义扩展
  • STM32串口通信指南
  • 基于RPR模型的机械臂手写器simulink建模与仿真
  • easyExcel 读取有合并单元格数据
  • 对接钉钉审批过程记录(C#版本)