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:可以用包含多个选项的对象来描述组件的逻辑,例如 data
、methods
和 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,从而使它关联的组件重新渲染。
详细过程:
-
在 new Vue() 后, Vue 会调用 _init 函数进行初始化,也就是init 过程,在 这个过程Data通过Observer转换成了getter/setter的形式,来对数据追踪变化,当被设置的对象被读取的时候会执行 getter 函数,而在当被赋值的时候会执行 setter函数。
-
当render function 执行的时候,因为会读取所需对象的值,所以会触发getter函数从而将Watcher添加到依赖中进行依赖收集。
-
在修改对象的值的时候,会触发对应的 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的实例对象,只能用于对象类型 (对象、数组和如Map
、Set
这样的集合类型)。它不能持有如string
、number
或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>