结构相关
❗Vue中为什么要设计一个data来处理数据?
实例和组件定义data的区别
vue
实例的时候定义data
属性既可以是一个对象,也可以是一个函数
const app = new Vue({
el:"#app",
// 对象格式
data:{
foo:"foo"
},
// 函数格式
data(){
return {
foo:"foo"
}
}
})
组件中定义data
属性,只能是一个函数
如果为组件data
直接定义为一个对象
Vue.component('component1',{
template:`<div>组件</div>`,
data:{
foo:"foo"
}
})
则会得到警告信息:返回的data
应该是一个函数在每一个组件实例中
代码示例:
let makeCounter = function() {
let num = 0;
function changeBy(val){
num += val
}
//在函数外部是无法取得num这个变量的,因为其定义在私有域中。只能取到return中方法
return {
add: function(){
changeBy(1)
},
reduce:function(){
changeBy(-2)
},
value: function(){
return num
}
}
}
let counter1 = makeCounter()
let counter2 = makeCounter()
counter1.add()
console.log(counter1.value()) //1 二者不受影响,虽然都拥有相同的方法,但有各自的独立的词法作用域
console.log(counter2.value()) //0
组件data定义函数与对象的区别
上面讲到组件data
必须是一个函数,这是因为在我们定义好一个组件的时候,vue
最终都会通过Vue.extend()
构成组件实例。
这里我们模仿组件构造函数,定义data
属性,采用对象的形式:
function Component(){
}
Component.prototype.data = {
count : 0
}
创建两个组件实例
const componentA = new Component()
const componentB = new Component()
修改componentA
组件data
属性的值,componentB
中的值也发生了改变
console.log(componentB.data.count) // 0
componentA.data.count = 1
console.log(componentB.data.count) // 1
产生这样的原因这是两者共用了同一个内存地址,componentA
修改的内容,同样对componentB
产生了影响。
如果我们采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同)
function Component(){
this.data = this.data()
}
Component.prototype.data = function (){
return {
count : 0
}
}
修改componentA
组件data
属性的值,componentB
中的值不受影响
console.log(componentB.data.count) // 0
componentA.data.count = 1
console.log(componentB.data.count) // 0
vue
组件可能会有很多个实例,采用函数返回一个全新data
形式,使每个实例对象的数据不会受到其他实例对象数据的污染。
原理分析
首先可以看看vue
初始化data
的代码,data
的定义可以是函数也可以是对象
源码位置:/vue-dev/src/core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
...
}
data
既能是object
也能是function
,那为什么还会出现上文警告呢?
别急,继续看下文
组件在创建的时候,会进行选项的合并
源码位置:/vue-dev/src/core/util/options.js
自定义组件会进入mergeOptions
进行选项合并
Vue.prototype._init = function (options?: Object) {
...
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
...
}
定义data
会进行数据校验
源码位置:/vue-dev/src/core/instance/init.js
这时候vm
实例为undefined
,进入if
判断,若data
类型不是function
,则出现警告提示
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) {
if (childVal && typeof childVal !== "function") {
process.env.NODE_ENV !== "production" &&
warn(
'The "data" option should be a function ' +
"that returns a per-instance value in component " +
"definitions.",
vm
);
return parentVal;
}
return mergeDataOrFn(parentVal, childVal);
}
return mergeDataOrFn(parentVal, childVal, vm);
};
- 根实例对象
data
可以是对象也可以是函数(根实例是单例),不会产生数据污染情况 - 组件实例对象
data
必须为函数,目的是为了防止多个组件实例对象之间共用一个data
,产生数据污染。采用函数的形式,initData
时会将其作为工厂函数都会返回全新data
对象 - 如果组件中 data 选项是一个函数(闭包),那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data 属性值不会相互影响;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
Vue中组件和插件有什么区别?
组件是什么
回顾以前对组件的定义:
组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue
中每一个.vue
文件都可以视为一个组件
组件的优势
- 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现
- 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单
- 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级
插件是什么
插件通常用来为 Vue
添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:
- 添加全局方法或者属性。如:
vue-custom-element
- 添加全局资源:指令/过滤器/过渡等。如
vue-touch
- 通过全局混入来添加一些组件选项。如
vue-router
- 添加
Vue
实例方法,通过把它们添加到Vue.prototype
上实现。 - 一个库,提供自己的
API
,同时提供上面提到的一个或多个功能。如vue-router
两者的区别
两者的区别主要表现在以下几个方面:
- 编写形式
- 注册形式
- 使用场景
编写形式
编写组件
编写一个组件,可以有很多方式,我们最常见的就是vue
单文件的这种格式,每一个.vue
文件我们都可以看成是一个组件
vue
文件标准格式
<template>
</template>
<script>
export default{
...
}
</script>
<style>
</style>
我们还可以通过template
属性来编写一个组件,如果组件内容多,我们可以在外部定义template
组件内容,如果组件内容并不多,我们可直接写在template
属性上
<template id="testComponent"> // 组件显示的内容
<div>component!</div>
</template>
Vue.component('componentA',{
template: '#testComponent'
template: `<div>component</div>` // 组件内容少可以通过这种形式
})
编写插件
vue
插件的实现应该暴露一个 install
方法。这个方法的第一个参数是 Vue
构造器,第二个参数是一个可选的选项对象
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
注册形式
组件注册
vue
组件注册主要分为全局注册与局部注册
全局注册通过Vue.component
方法,第一个参数为组件的名称,第二个参数为传入的配置项
Vue.component('my-component-name', { /* ... */ })
局部注册只需在用到的地方通过components
属性注册一个组件
const component1 = {...} // 定义一个组件
export default {
components:{
component1 // 局部注册
}
}
插件注册
插件的注册通过Vue.use()
的方式进行注册(安装),第一个参数为插件的名字,第二个参数是可选择的配置项
Vue.use(插件名字,{ /* ... */} )
注意的是:
注册插件的时候,需要在调用 new Vue()
启动应用之前完成
Vue.use
会自动阻止多次注册相同插件,只会注册一次
使用场景
组件 (Component)
是用来构成你的 App
的业务模块,它的目标是 App.vue
插件 (Plugin)
是用来增强你的技术栈的功能模块,它的目标是 Vue
本身
简单来说,插件就是指对
Vue
的功能的增强或补充
v-model 的原理?
我们在 vue 项目中只要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
- text 和 textarea 元素使用 value 属性和 input 事件;
- checkbox 和 radio 使用 checked 属性和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件。
<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
如果在自定义组件中,v-model 默认会利用名为 value 的 prop 和名为 input 的事件,如下所示:
// 父组件
<ModelChild v-model="message"></ModelChild>
//子组件
<div>{{value}}</div>
props:{
value: String
},
methods: {
test1(){
this.$emit('input', '小红')
}
}
你使用过 Vuex 吗?
Vuex状态管理:
- Vuex的状态存储是响应式的。当 Vue 组件从
store
中读取状态的时候,若store
中的状态发生变化,那么相应的组件也会相应地得到高效更新。 - 改变
store
中的状态的唯一途径就是显式地提交(commit
)mutation
。这样使得我们可以方便地跟踪每一个状态的变化。- State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
- Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter映射到局部计算属性。
- Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
- Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
使用过Vue SSR吗?说说 SSR?
SSR(服务器端渲染):SSR是指将Vue应用程序在服务器端渲染成HTML,并将其发送到客户端,然后再在客户端进行激活。
CSR(客户端渲染):Vue应用程序的初始渲染是在浏览器中完成的。浏览器会下载Vue应用程序的JavaScript文件,并在客户端执行,然后生成DOM并渲染出页面。
SSR优点:
- 更好的 SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
- 更快的内容到达时间:SPA 会等待所有 Vue 编译后的JavaScript文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载JavaScript文件及再去渲染等,所以 SSR 有更快的内容到达时间(首屏加载更快);
SSR缺点:
- 更多的开发条件限制:例如服务端渲染只支持
beforCreate
和created
两个钩子函数,这会导致一些外部扩展库需要特殊处理, 才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于Node.js server运行环境; - 更多的服务器负载:在Node.js中渲染完整的应用程序,显然会比仅仅提供静态文件的server 更加大量占用CPU资源(CPU intensive-CPU密集),因此如果你预料在高流量环境(high traffic)下使用,请准备相应的服务器负载,并明智地采用缓存策略。
在Vue SSR中,我们可以使用Vue的 createRenderer
方法创建一个渲染器,并将其绑定到一个Express或Koa等服务器框架上。渲染器会将Vue应用程序的组件渲染成HTML,并将其返回给客户端。客户端会接收到服务器渲染的HTML,并在其上进行激活,使其成为一个可交互的Vue应用程序。
vue-router 路由模式有几种?
vue-router 有 3 种路由模式:hash、history、abstract,对应的源码如下所示:
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production'){
assert(false, `invalid mode: ${mode}`)
}
}
- hash:使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;
- history:依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;
- abstract:支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。
能说下 vue-router 中常用的 hash 和 history 路由模式实现原理吗?
早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search': https://www.word.com#search
- URL中hash值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash部分不会被发送;
- hash值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash的切换;
- 可以通过a标签,并设置href属性,当用户点击这个标签后 URL的hash值会发生改变;或者使用JavaScript来对 loaction..hash进行赋值,改变URL的hash值;
- 我们可以使用hashchange事件来监听hash值的变化,从而对页面进行跳转(渲染)
History:使用了HTML5提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState()
和history.repalceState()
。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。 唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的 历史记录,如下所示:
window.history.pushstate(null,null,path);
window.history.replacestate(null,null,path);
history路由模式的实现主要基于存在下面几个特性:
pushState
和repalceState
两个 API 来操作实现 URL 的变化,而不会引起页面的刷新;- 当用户点击路由链接或调用
router.push
或router.replace
方法时,Vue Router 会使用 History API 将新的路由添加到浏览器的历史记录中。 - 当浏览器的 URL 发生变化时,Vue Router 会监听
popstate
事件,并根据新的 URL 解析出对应的路由。 history.pushState()
或history.replaceState()
不会触发popstate
事件,这时我们需要手动触发页面跳转(渲染)。
Vue中的$nextTick有什么作用?
详见:Vue核心 $nextTick 过渡与动画 | QT-7274 (qblog.top)
说说你对vue的mixin的理解,有什么应用场景?
详见:Vue CLI中的属性 | QT-7274 (qblog.top)
说说你对slot的理解?slot使用场景有哪些?
❗Vue 中组件通信
详见:Vue组件间通信方式详解 | QT-7274 (qblog.top)
Vue.observable你有了解过吗?说说看
详见:Vue组件间通信方式详解 | QT-7274 (qblog.top)
数据响应相关
Vue的响应式原理
答案:Vue3新特性 | QT-7274 (qblog.top)
❗Vue 是如何实现数据双向绑定的?
Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据。
双向绑定由三个重要部分构成:
- 数据层(Model):应用的数据及业务逻辑
- 视图层(View):应用的展示效果,各类UI组件
- 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM
这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理
ViewModel
它的主要职责就是:
- 数据变化后更新视图
- 视图变化后更新数据
以及两个主要部分组成:
- 监听器(Observer):对所有数据的属性进行监听
- 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数。一旦数据有变动,这些函数能够在数据变化时更新DOM元素。
实现双向绑定
我们还是以Vue
为例,先来看看Vue
中的双向绑定流程是什么的
new Vue()
首先执行初始化,对data
执行响应化处理,这个过程发生Observe
中- 同时对模板执行编译,找到其中动态绑定的数据,从
data
中获取并初始化视图,这个过程发生在Compile
中 - 同时定义⼀个更新函数和
Watcher
,将来对应数据变化时Watcher
会调用更新函数 - 由于
data
的某个key
在⼀个视图中可能出现多次,所以每个key
都需要⼀个管家Dep
来管理多个Watcher
- 将来data中数据⼀旦发生变化,会首先找到对应的
Dep
,通知所有Watcher
执行更新函数
实现
先来一个构造函数:执行初始化,对data
执行响应化处理
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 对data选项做响应式处理
observe(this.$data);
// 代理data到vm上
proxy(this);
// 执行编译
new Compile(options.el, this);
}
}
对data
选项执行响应化具体操作
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
return;
}
new Observer(obj);
}
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
编译Compile
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 获取dom
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍历子元素
if (this.isElement(node)) { // 判断是否为节点
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素
this.compile(node); // 对子元素进行递归遍历
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
依赖收集
视图中会用到data
中某key
,这称为依赖。同⼀个key
可能出现多次,每次都需要收集出来用⼀个Watcher
来维护它们,此过程称为依赖收集多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知
实现思路
defineReactive
时为每⼀个key
创建⼀个Dep
实例- 初始化视图时读取某个
key
,例如name1
,创建⼀个watcher1
- 由于触发
name1
的getter
方法,便将watcher1
添加到name1
对应的Dep中 - 当
name1
更新,setter
触发时,便可通过对应Dep
通知其管理所有Watcher
更新
// 负责更新视图
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this
// 读一下key,触发get
vm[key]
// 置空
Dep.target = null
}
// 未来执行dom更新函数,由dep调用的
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
声明Dep
class Dep {
constructor() {
this.deps = []; // 依赖管理
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
创建watcher
时触发getter
class Watcher {
constructor(vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
依赖收集,创建Dep
实例
function defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例
return val;
},
set(newVal) {
if (newVal === val) return;
dep.notify(); // 通知dep执行更新方法
},
});
}
更多详细源码,详见0 到 1 掌握:Vue核心之数据双向绑定
动态给vue的data添加一个新的属性时会发生什么?怎样解决?
直接添加属性的问题
我们从一个例子开始
定义一个p
标签,通过v-for
指令进行遍历
然后给botton
标签绑定点击事件,我们预期点击按钮时,数据新增一个属性,界面也 新增一行
<p v-for="(value,key) in item" :key="key">
{{ value }}
</p>
<button @click="addProperty">动态添加新属性</button>
实例化一个vue
实例,定义data
属性和methods
方法
const app = new Vue({
el:"#app",
data:()=>{
item:{
oldProperty:"旧属性"
}
},
methods:{
addProperty(){
this.items.newProperty = "新属性" // 为items添加新属性
console.log(this.items) // 输出带有newProperty的items
}
}
})
点击按钮,发现结果不及预期,数据虽然更新了(console
打印出了新属性),但页面并没有更新。
原理分析
为什么产生上面的情况呢?
下面来分析一下
vue2
是用过Object.defineProperty
实现数据响应式
const obj = {}
Object.defineProperty(obj, 'foo', {
get() {
console.log(`get foo:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
console.log(`set foo:${newVal}`);
val = newVal
}
}
})
}
当我们访问foo
属性或者设置foo
值的时候都能够触发setter
与getter
obj.foo
obj.foo = 'new'
但是我们为obj
添加新属性的时候,却无法触发事件属性的拦截
obj.bar = '新属性'
原因是一开始obj
的foo
属性被设成了响应式数据,而bar
是后面新增的属性,并没有通过Object.defineProperty
设置成响应式数据。这个在后面章节也反复提到。
解决方案
Vue
不允许在已经创建的实例上动态添加新的响应式属性
若想实现数据与视图同步更新,可采取下面三种解决方案:
- Vue.set()
- Object.assign()
- $forcecUpdated()
Vue.set()
Vue.set( target, propertyName/index, value )
参数
{Object | Array} target
{string | number} propertyName/index
{any} value
返回值:设置的值
通过Vue.set
向响应式对象中添加一个property
,并确保这个新 property
同样是响应式的,且触发视图更新
关于Vue.set
源码,详见后续章节:[Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题?]
Object.assign()
直接使用Object.assign()
添加到对象的新属性不会触发更新
应创建一个新的对象,合并原对象和混入对象的属性
this.someObject = Object.assign({},this.someObject,{newProperty1:1,n
注意:
Object.assign()
执行的是浅拷贝,即只会拷贝对象的引用而不会递归地拷贝对象的属性。如果源对象中的属性值是对象或数组,拷贝后的对象仍然会共享这些属性,导致可能意外修改原始对象。- 不支持拷贝继承属性和不可枚举属性。
- 无法拷贝特殊对象:
Object.assign()
无法正确拷贝某些特殊对象,如Date对象、RegExp对象和Error对象等。- 不支持拷贝Symbol属性
- 可能会抛出错误:当目标对象为null或undefined时,
Object.assign()
会抛出TypeError错误。此外,如果源对象包含不可写属性或setter方法,拷贝过程中可能会抛出错误。
$forceUpdate
如果你发现你自己需要在 Vue
中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。
$forceUpdate
迫使Vue
实例重新渲染
PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
小结
- 如果为对象添加少量的新属性,可以直接采用
Vue.set()
- 如果需要为新对象添加大量的新属性,则通过
Object.assign()
创建新对象 - 如果你实在不知道怎么操作时,可采取
$forceUpdate()
进行强制刷新 (不建议)
PS:vue3
是用过proxy
实现数据响应式的,直接动态添加新属性仍可以实现数据响应式
怎样理解 Vue 的单向数据流
所有的 prop
都使得其父子 prop
之间形成了一个单向下行绑定:父级 prop
的更新会向下流动到子组件中,但是反过来则不行。
这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生更新时,子组件中所有的 prop
都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop
。如果你这样做了, Vue 会在浏览器的控制台中发出警告。
子组件想修改时,只能通过 $emit
派发一个自定义事件,父组件接收到后,由父组件修改。
这个
prop
用来传递一个初始值;这个子组件接下来希望将其作为一个本地的prop
数据来使用。在这种情况下,最好定义一个本地的data
属性并将这个prop
用作其初始值:props: ['initialCounter'], data: function () { return { counter: this.initialCounter } }
这个
prop
以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个prop
的值来定义一个计算属性:props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
直接给一个数组项赋值,Vue 能检测到变化吗?
由于 JavaScript 的限制,Vue 不能检测到以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set, Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
// Array.prototype.splice
vm.items.splice(newLength)
注意:在Vue 3中,当你直接通过索引设置数组项时,它是响应式的。这是因为Vue 3采用了 Proxy 作为其响应式系统的实现方式,可以捕获对数组的直接修改操作。
如果你需要对数组进行这些操作,可以使用Vue提供的一些特定方法,如
push
、pop
、splice
等,这些方法会被Vue重写以保证响应式更新的正确性。
Vue 怎么用 vm.$set() 解决对象新增属性不能响应的问题?
受现代JavaScript的限制,Vue 无法检测到对象属性的添加或删除。 由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。 但是 Vue 提供了 Vue.set(object, propertyName, value) / vm.$set(object, propertyName, value)
来实现为对象添加响应式属性,那框架本身是如何实现的呢?
我们查看对应的 Vue 源码:vue/src/core/instance/index.js
export function set (target: Array<any> | Object, key: any, val: any):
// 如果目标是数组,并且索引是有效的数组索引
if (Array.isArray(target) && isValidArrayIndex(key)){
// 修改数组的长度,避免索引大于数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 这行代码的作用是在目标数组 target 中的指定位置 key ,删除一个元素,并用新的元素 val 进行替换。这样可以实现在数组中修改指定位置的元素,同时触发响应式更新,以便 Vue中更新相关的视图。
target.splice(key, 1, val)
return val
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)){
target[key] = val
return val
}
// __ob__ 是一个特殊的属性,它是 Observer(观察者)的缩写。Observer 是用于实现数据响应式的核心对象之一。
// 通过 Observer 对象,我们可以进行依赖追踪、通知更新等操作,以实现数据的响应式特性。
const ob = (target: any).__ob__
// target 本身就不是响应式数据,直接赋值
if(!ob){
target[key] = val
return val
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
我们阅读以上源码可知,vm.$set 的实现原理是:
- 如果目标是数组,直接使用数组的 splice 方法触发响应式;
- 针对已存在的属性,直接修改属性值。
- 对于非响应式数据,直接赋值。
- 对属性进行响应式处理,通过调用defineReactive函数来定义属性的getter和setter,并通知相关依赖进行更新。 (defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法)。
生命周期相关
❗谈谈你对Vue生命周期的理解?
Vue2:
详见Vue核心 Vue生命周期 | QT-7274 (qblog.top)
beforeCreate | 组件实例被创建之初,组件的属性生效之前 |
created | 组件实例已经瓦全创建 |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 |
mounted | el 被新创建的 vm.$el 替换,并挂载到实例上之后调用该函数 |
beforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 |
updated | 组件数据更新之后 |
activited | keep-alive 专属,组件被激活时调用 |
deactivated | keep-alive 专属,组件被销毁时调用 |
beforeDestroy | 组件销毁前调用 |
destroyed | 组件销毁后调用 |
Vue 的父组件和子组件生命周期钩子函数执行顺序?
Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下4部分:
加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
父组件更新过程
父 beforeUpdate -> 父 updated
销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
在哪个生命周期内调用异步请求?
可以在钩子函数 created、beforeMount、 mounted 中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
但是本人推荐在 created 钩子函数中调用异步请求,因为 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面loading时间;
- ssr不支持
beforeMount
、mounted
钩子函数,所以放created中有助于一致性;
在什么阶段才能访问操作DOM?
在钩子函数 mounted 被调用前,Vue 已经将编译好的模板挂载到页面上,所以在 mounted 中可以访问操作 DOM。
详见Vue核心 Vue生命周期 | QT-7274 (qblog.top)
父组件可以监听到子组件的生命周期吗?
比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted
就做一些逻辑处理,可以通过以下写法实现:
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted(){
this.$emit('mounted');
}
以上需要手动通过 $emit
触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook
来监听即可,如下所示:
// Parent.vue
<Child @hook:mounted="doSomething"/>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...')
}
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...')
}
当然 @hook
方法不仅仅是可以监听 mounted
,其它的生命周期事件,例如:created
,updated
等都可以监听。
其他问题
v-for
遍历必须为 item 添加 key
,且避免同时使用 v-if
?
详见:Vue性能优化详解 | QT-7274 (qblog.top)
computed
和 watch
的区别和运用场景?
computed
:是计算属性,依赖其它属性值,并且 computed
的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed
的值时才会重新计算 computed
的值;
watch
:更多的是「观察」的作用,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作;
- 当我们需要进行数值计算,并且依赖于其它数据时,应该使用
computed
,因为可以利用computed
的缓存特性,避免每次获取值时,都要重新计算; - 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用
watch
,使用watch
选项允许我们执行异步操作(访问一个API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
使用 Vue 框架踩过最大的坑是什么?如何解决?
- 异步更新问题:由于 Vue 的响应式更新是异步的,有时候可能会导致数据更新不及时或不符合预期。这可能会导致一些意外的bug,特别是在涉及到计算属性、侦听属性或组件生命周期钩子函数时。解决这个问题可以使用 Vue 提供的
$nextTick
方法来确保在DOM更新后再执行相关操作。 - 对象和数组的变更检测:Vue 的响应式系统可以追踪对象和数组的变化,但是对于新增的属性或直接修改数组的索引项时,Vue 无法自动检测到变化。为了确保对象和数组的变更能够被 Vue 正确追踪,可以使用Vue提供的特定方法来进行修改,如
Vue.set
、this.$set
、splice
等。 - 事件处理函数的作用域:在 Vue 中,事件处理函数默认情况下会自动绑定到组件实例上,这样可以访问组件的数据和方法。然而,如果在循环中使用事件处理函数,可能会遇到作用域问题,导致事件处理函数无法正确访问循环中的数据。可以通过使用箭头函数或
bind
方法来绑定正确的作用域。 - 无限循环的 Watcher :在使用 Vue 的计算属性或侦听属性时,如果存在循环依赖关系,可能会导致无限循环的 Watcher 。这会导致性能问题和页面卡顿。为了避免这种情况,需要仔细检查计算属性和侦听属性之间的依赖关系,确保它们之间没有循环引用。
虚拟 DOM 的优缺点?
- 保证性能下限:框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
- 无需手动操作 DOM:我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以预期的方式更新视图,极大提高我们的开发效率;
- 跨平台:虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex开发等等。
- 无法进行极致优化:虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
虚拟 DOM 实现原理?
- 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
- diff 算法 —— 比较两棵虚拟 DOM 树的差异;
- pach 算法 —— 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。
怎么缓存当前的组件?缓存后怎么更新?说说你对keep-alive的理解是什么?
详见:Vue核心 路由 | QT-7274 (qblog.top)
Vue常用的修饰符有哪些?有什么应用场景?
详见:和题目一致
说下你的vue项目的目录结构,如果是大型项目你该怎么划分结构和划分组
详见:
Vue 中的 key 有什么作用?你知道vue中key的原理吗?
详见:和题目一致
Vue要做权限管理该怎么做?如果控制到按钮级别的权限怎么做?
详见: