computed 的实现原理
computed 本质是一个惰性求值的观察者。
computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。
其内部通过 this.dirty 属性标记计算属性是否需要重新求值。
当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,
computed watcher 通过 this.dep.subs.length 判断有没有订阅者,
有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)
没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)
Computed 和 Methods 的区别
可以将同一函数定义为一个 method 或者一个计算属性。对于最终的结果,两种方式是相同的
不同点:
- computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;
- method 调用总会执行该函数。
Vue生命周期钩子是如何实现的
vue
的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法- 内部会对钩子函数进行处理,将钩子函数维护成数组的形式
Vue
的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)
<script> | |
// Vue.options 中会存放所有全局属性 | |
// 会用自身的 + Vue.options 中的属性进行合并 | |
// Vue.mixin({ | |
// beforeCreate() { | |
// console.log('before 0') | |
// }, | |
// }) | |
debugger; | |
const vm = new Vue({ | |
el: '#app', | |
beforeCreate: [ | |
function() { | |
console.log('before 1') | |
}, | |
function() { | |
console.log('before 2') | |
} | |
] | |
}); | |
console.log(vm); | |
</script> |
相关代码如下
export function callHook(vm, hook) { | |
// 依次执行生命周期对应的方法 | |
const handlers = vm.$options[hook]; | |
if (handlers) { | |
for (let i = 0; i < handlers.length; i++) { | |
handlers[i].call(vm); //生命周期里面的this指向当前实例 | |
} | |
} | |
} | |
// 调用的时候 | |
Vue.prototype._init = function (options) { | |
const vm = this; | |
vm.$options = mergeOptions(vm.constructor.options, options); | |
callHook(vm, "beforeCreate"); //初始化数据之前 | |
// 初始化状态 | |
initState(vm); | |
callHook(vm, "created"); //初始化数据之后 | |
if (vm.$options.el) { | |
vm.$mount(vm.$options.el); | |
} | |
}; | |
// 销毁实例实现 | |
Vue.prototype.$destory = function() { | |
// 触发钩子 | |
callHook(vm, 'beforeDestory') | |
// 自身及子节点 | |
remove() | |
// 删除依赖 | |
watcher.teardown() | |
// 删除监听 | |
vm.$off() | |
// 触发钩子 | |
callHook(vm, 'destoryed') | |
} |
原理流程图
Class 与 Style 如何动态绑定
Class
可以通过对象语法和数组语法进行动态绑定
对象语法:
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div> | |
data: { | |
isActive: true, | |
hasError: false | |
} |
数组语法:
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div> | |
data: { | |
activeClass: 'active', | |
errorClass: 'text-danger' | |
} |
Style
也可以通过对象语法和数组语法进行动态绑定
对象语法:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div> | |
data: { | |
activeColor: 'red', | |
fontSize: 30 | |
} |
数组语法:
<div v-bind:style="[styleColor, styleSize]"></div> | |
data: { | |
styleColor: { | |
color: 'red' | |
}, | |
styleSize:{ | |
fontSize:'23px' | |
} | |
} |
extend 有什么作用
这个 API 很少用到,作用是扩展组件生成一个构造器,通常会与 $mount
一起使用。
// 创建组件构造器 | |
let Component = Vue.extend({ template: "<div>test</div>" }); | |
// 挂载到 #app 上new Component().$mount('#app') | |
// 除了上面的方式,还可以用来扩展已有的组件 | |
let SuperComponent = Vue.extend(Component); | |
new SuperComponent({ | |
created() { | |
console.log(1); | |
}, | |
}); | |
new SuperComponent().$mount("#app"); |
vue-router 路由钩子函数是什么 执行顺序是什么
路由钩子的执行流程, 钩子函数种类有:全局守卫、路由守卫、组件守卫
完整的导航解析流程:
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫 (2.5+)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
参考 前端进阶面试题详细解答
v-show 与 v-if 有什么区别?
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
MVVM、MVC、MVP的区别
MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化开发效率。
在开发单页面应用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简单项目时,可能看不出什么问题,如果项目变得复杂,那么整个文件就会变得冗长、混乱,这样对项目开发和后期的项目维护是非常不利的。
(1)MVC
MVC 通过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来完成对 Model 的修改,然后 Model 层再去通知 View 层更新。
(2)MVVM
MVVM 分为 Model、View、ViewModel:
- Model代表数据模型,数据和业务逻辑都在Model层中定义;
- View代表UI视图,负责数据的展示;
- ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。
这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。
(3)MVP
MVP 模式与 MVC 唯一不同的在于 Presenter 和 Controller。在 MVC 模式中使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一起,当项目逻辑变得复杂的时候,可能会造成代码的混乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的Controller 只知道 Model 的接口,因此它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 因此可以在 Presenter 中将 Model 的变化和 View 的变化绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其他的响应逻辑。
Vue 初始化页面闪动问题如何解决?
出现该问题是因为在 Vue 代码尚未被解析之前,尚无法控制页面中 DOM 的显示,所以会看见模板字符串等代码。
解决方案是,在 css 代码中添加 v-cloak 规则,同时在待编译的标签上添加 v-cloak 属性:
[v-cloak] { display: none; } | |
<div v-cloak> | |
{{ message }} | |
</div> |
Vue 中给 data 中的对象属性添加一个新的属性时会发生什么?如何解决?
<template> | |
<div> | |
<ul> | |
<li v-for="value in obj" :key="value"> {{value}} </li> </ul> <button @click="addObjB">添加 obj.b</button> </div> | |
</template> | |
<script> | |
export default { data () { return { obj: { a: 'obj.a' } } }, methods: { addObjB () { this.obj.b = 'obj.b' console.log(this.obj) } } } | |
</script> |
点击 button 会发现,obj.b 已经成功添加,但是视图并未刷新。这是因为在Vue实例创建时,obj.b并未声明,因此就没有被Vue转换为响应式的属性,自然就不会触发视图的更新,这时就需要使用Vue的全局 api $set():
addObjB () ( | |
this.$set(this.obj, 'b', 'obj.b') | |
console.log(this.obj) | |
} |
$set()方法相当于手动的去把obj.b处理成一个响应式的属性,此时视图也会跟着改变了。
常见的事件修饰符及其作用
.stop
:等同于 JavaScript 中的event.stopPropagation()
,防止事件冒泡;.prevent
:等同于 JavaScript 中的event.preventDefault()
,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播);.capture
:与事件冒泡的方向相反,事件捕获由外到内;.self
:只会触发自己范围内的事件,不包含子元素;.once
:只会触发一次。
v-if和v-show的区别
- 手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;
- 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;
- 编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM元素保留;
- 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;
- 使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。
组件通信
组件通信的方式如下:
(1) props / $emit
父组件通过props
向子组件传递数据,子组件通过$emit
和父组件通信
1. 父组件向子组件传值
props
只能是父组件向子组件进行传值,props
使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。props
可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。props
属性名规则:若在props
中使用驼峰形式,模板中需要使用短横线的形式
// 父组件 | |
<template> | |
<div id="father"> | |
<son :msg="msgData" :fn="myFunction"></son> | |
</div> | |
</template> | |
<script> | |
import son from "./son.vue"; | |
export default { | |
name: father, | |
data() { | |
msgData: "父组件数据"; | |
}, | |
methods: { | |
myFunction() { | |
console.log("vue"); | |
}, | |
}, | |
components: { son }, | |
}; | |
</script> | |
// 子组件 | |
<template> | |
<div id="son"> | |
<p>{{ msg }}</p> | |
<button @click="fn">按钮</button> | |
</div> | |
</template> | |
<script> | |
export default { name: "son", props: ["msg", "fn"] }; | |
</script> |
2. 子组件向父组件传值
$emit
绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过v-on
监听并接收参数。
// 父组件 | |
<template> | |
<div class="section"> | |
<com-article | |
:articles="articleList" | |
@onEmitIndex="onEmitIndex" | |
></com-article> | |
<p>{{ currentIndex }}</p> | |
</div> | |
</template> | |
<script> | |
import comArticle from "./test/article.vue"; | |
export default { | |
name: "comArticle", | |
components: { comArticle }, | |
data() { | |
return { currentIndex: -1, articleList: ["红楼梦", "西游记", "三国演义"] }; | |
}, | |
methods: { | |
onEmitIndex(idx) { | |
this.currentIndex = idx; | |
}, | |
}, | |
}; | |
</script> | |
//子组件 | |
<template> | |
<div> | |
<div | |
v-for="(item, index) in articles" | |
:key="index" | |
@click="emitIndex(index)" | |
> | |
{{ item }} | |
</div> | |
</div> | |
</template> | |
<script> | |
export default { | |
props: ["articles"], | |
methods: { | |
emitIndex(index) { | |
this.$emit("onEmitIndex", index); // 触发父组件的方法,并传递参数index | |
}, | |
}, | |
}; | |
</script> |
(2)eventBus事件总线($emit / $on
)
eventBus
事件总线适用于父子组件、非父子组件等之间的通信,使用步骤如下: (1)创建事件中心管理组件之间的通信
// event-bus.js | |
import Vue from 'vue' | |
export const EventBus = new Vue() |
(2)发送事件 假设有两个兄弟组件firstCom
和secondCom
:
<template> | |
<div> | |
<first-com></first-com> | |
<second-com></second-com> | |
</div> | |
</template> | |
<script> | |
import firstCom from "./firstCom.vue"; | |
import secondCom from "./secondCom.vue"; | |
export default { components: { firstCom, secondCom } }; | |
</script> |
在firstCom
组件中发送事件:
<template> | |
<div> | |
<button @click="add">加法</button> | |
</div> | |
</template> | |
<script> | |
import { EventBus } from "./event-bus.js"; // 引入事件中心 | |
export default { | |
data() { | |
return { num: 0 }; | |
}, | |
methods: { | |
add() { | |
EventBus.$emit("addition", { num: this.num++ }); | |
}, | |
}, | |
}; | |
</script> |
(3)接收事件 在secondCom
组件中发送事件:
<template> | |
<div>求和: {{ count }}</div> | |
</template> | |
<script> | |
import { EventBus } from "./event-bus.js"; | |
export default { | |
data() { | |
return { count: 0 }; | |
}, | |
mounted() { | |
EventBus.$on("addition", (param) => { | |
this.count = this.count + param.num; | |
}); | |
}, | |
}; | |
</script> |
在上述代码中,这就相当于将num
值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。
虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。
(3)依赖注入(provide / inject)
这种方式就是Vue中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。
provide / inject
是Vue提供的两个钩子,和data
、methods
是同级的。并且provide
的书写形式和data
一样。
provide
钩子用来发送数据或方法inject
钩子用来接收数据或方法
在父组件中:
provide() { | |
return { | |
num: this.num | |
}; | |
} |
在子组件中:
inject: ['num']
还可以这样写,这样写就可以访问父组件中的所有属性:
provide() { | |
return { | |
app: this | |
}; | |
} | |
data() { | |
return { | |
num: 1 | |
}; | |
} | |
inject: ['app'] | |
console.log(this.app.num) |
注意: 依赖注入所提供的属性是非响应式的。
(3)ref / $refs
这种方式也是实现父子组件之间的通信。
ref
: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。
在子组件中:
export default { | |
data () { | |
return { | |
name: 'JavaScript' | |
} | |
}, | |
methods: { | |
sayHello () { | |
console.log('hello') | |
} | |
} | |
} |
在父组件中:
<template> | |
<child ref="child"></component-a> | |
</template> | |
<script> | |
import child from "./child.vue"; | |
export default { | |
components: { child }, | |
mounted() { | |
console.log(this.$refs.child.name); // JavaScript | |
this.$refs.child.sayHello(); // hello | |
}, | |
}; | |
</script> |
(4)$parent / $children
- 使用
$parent
可以让组件访问父组件的实例(访问的是上一级父组件的属性和方法) - 使用
$children
可以让组件访问子组件的实例,但是,$children
并不能保证顺序,并且访问的数据也不是响应式的。
在子组件中:
<template> | |
<div> | |
<span>{{ message }}</span> | |
<p>获取父组件的值为: {{ parentVal }}</p> | |
</div> | |
</template> | |
<script> | |
export default { | |
data() { | |
return { message: "Vue" }; | |
}, | |
computed: { | |
parentVal() { | |
return this.$parent.msg; | |
}, | |
}, | |
}; | |
</script> |
在父组件中:
// 父组件中 | |
<template> | |
<div class="hello_world"> | |
<div>{{ msg }}</div> | |
<child></child> | |
<button @click="change">点击改变子组件值</button> | |
</div> | |
</template> | |
<script> | |
import child from "./child.vue"; | |
export default { | |
components: { child }, | |
data() { | |
return { msg: "Welcome" }; | |
}, | |
methods: { | |
change() { | |
// 获取到子组件 | |
this.$children[0].message = "JavaScript"; | |
}, | |
}, | |
}; | |
</script> |
在上面的代码中,子组件获取到了父组件的parentVal
值,父组件改变了子组件中message
的值。 需要注意:
- 通过
$parent
访问到的是上一级父组件的实例,可以使用$root
来访问根组件的实例 - 在组件中使用
$children
拿到的是所有的子组件的实例,它是一个数组,并且是无序的 - 在根组件
#app
上拿$parent
得到的是new Vue()
的实例,在这实例上再拿$parent
得到的是undefined
,而在最底层的子组件拿$children
是个空数组 $children
的值是数组,而$parent
是个对象
(5)$attrs / $listeners
考虑一种场景,如果A是B组件的父组件,B是C组件的父组件。如果想要组件A给组件C传递数据,这种隔代的数据,该使用哪种方式呢?
如果是用props/$emit
来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。
针对上述情况,Vue引入了$attrs / $listeners
,实现组件之间的跨代通信。
先来看一下inheritAttrs
,它的默认值true,继承所有的父组件属性除props
之外的所有属性;inheritAttrs:false
只继承class属性 。
$attrs
:继承所有的父组件属性(除了prop传递的属性、class 和 style ),一般用在子组件的子元素上$listeners
:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)
A组件(APP.vue
):
<template> | |
<div id="app"> | |
//此处监听了两个事件,可以在B组件或者C组件中直接触发 | |
<child1 | |
:p-child1="child1" | |
:p-child2="child2" | |
@test1="onTest1" | |
@test2="onTest2" | |
></child1> | |
</div> | |
</template> | |
<script> | |
import Child1 from "./Child1.vue"; | |
export default { | |
components: { Child1 }, | |
methods: { | |
onTest1() { | |
console.log("test1 running"); | |
}, | |
onTest2() { | |
console.log("test2 running"); | |
}, | |
}, | |
}; | |
</script> |
B组件(Child1.vue
):
<template> | |
<div class="child-1"> | |
<p>props: {{ pChild1 }}</p> | |
<p>$attrs: {{ $attrs }}</p> | |
<child2 v-bind="$attrs" v-on="$listeners"></child2> | |
</div> | |
</template> | |
<script> | |
import Child2 from "./Child2.vue"; | |
export default { | |
props: ["pChild1"], | |
components: { Child2 }, | |
inheritAttrs: false, | |
mounted() { | |
this.$emit("test1"); // 触发APP.vue中的test1方法 | |
}, | |
}; | |
</script> |
C 组件 (Child2.vue
):
<template> | |
<div class="child-2"> | |
<p>props: {{ pChild2 }}</p> | |
<p>$attrs: {{ $attrs }}</p> | |
</div> | |
</template> | |
<script> | |
export default { | |
props: ["pChild2"], | |
inheritAttrs: false, | |
mounted() { | |
this.$emit("test2"); // 触发APP.vue中的test2方法 | |
}, | |
}; | |
</script> |
在上述代码中:
- C组件中能直接触发test的原因在于 B组件调用C组件时 使用 v-on 绑定了
$listeners
属性 - 在B组件中通过v-bind 绑定
$attrs
属性,C组件可以直接获取到A组件中传递下来的props(除了B组件中props声明的)
(6)总结
(1)父子组件间通信
- 子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
- 通过 ref 属性给子组件设置一个名字。父组件通过
$refs
组件名来获得子组件,子组件通过$parent
获得父组件,这样也可以实现通信。 - 使用 provide/inject,在父组件中通过 provide提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide中的数据。
(2)兄弟组件间通信
- 使用 eventBus 的方法,它的本质是通过创建一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现消息的传递。
- 通过
$parent/$refs
来获取到兄弟组件,也可以进行通信。
(3)任意组件之间
- 使用 eventBus ,其实就是创建一个事件中心,相当于中转站,可以用它来传递事件和接收事件。
如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。
如何从真实DOM到虚拟DOM
涉及到Vue中的模板编译原理,主要过程:
- 将模板转换成
ast
树,ast
用对象来描述真实的JS语法(将真实DOM转换成虚拟DOM) - 优化树
- 将
ast
树生成代码
Vue3.0 和 2.0 的响应式原理区别
Vue3.x 改用 Proxy 替代 Object.defineProperty。因为 Proxy 可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。
相关代码如下
import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑 | |
import { isObject } from "./util"; // 工具方法 | |
export function reactive(target) { | |
// 根据不同参数创建不同响应式对象 | |
return createReactiveObject(target, mutableHandlers); | |
} | |
function createReactiveObject(target, baseHandler) { | |
if (!isObject(target)) { | |
return target; | |
} | |
const observed = new Proxy(target, baseHandler); | |
return observed; | |
} | |
const get = createGetter(); | |
const set = createSetter(); | |
function createGetter() { | |
return function get(target, key, receiver) { | |
// 对获取的值进行放射 | |
const res = Reflect.get(target, key, receiver); | |
console.log("属性获取", key); | |
if (isObject(res)) { | |
// 如果获取的值是对象类型,则返回当前对象的代理对象 | |
return reactive(res); | |
} | |
return res; | |
}; | |
} | |
function createSetter() { | |
return function set(target, key, value, receiver) { | |
const oldValue = target[key]; | |
const hadKey = hasOwn(target, key); | |
const result = Reflect.set(target, key, value, receiver); | |
if (!hadKey) { | |
console.log("属性新增", key, value); | |
} else if (hasChanged(value, oldValue)) { | |
console.log("属性值被修改", key, value); | |
} | |
return result; | |
}; | |
} | |
export const mutableHandlers = { | |
get, // 当获取属性时调用此方法 | |
set, // 当修改属性时调用此方法 | |
}; |
Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?
1)Vue为什么要用vm.$set() 解决对象新增属性不能响应的问题
- Vue使用了Object.defineProperty实现双向数据绑定
- 在初始化实例时对属性执行 getter/setter 转化
- 属性必须在data对象上存在才能让Vue将它转换为响应式的(这也就造成了Vue无法检测到对象属性的添加或删除)
所以Vue提供了Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
2)接下来我们看看框架本身是如何实现的呢?
Vue 源码位置:vue/src/core/instance/index.js
export function set (target: Array<any> | Object, key: any, val: any): any { | |
// target 为数组 | |
if (Array.isArray(target) && isValidArrayIndex(key)) { | |
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误 | |
target.length = Math.max(target.length, key) | |
// 利用数组的splice变异方法触发响应式 | |
target.splice(key, 1, val) | |
return val | |
} | |
// key 已经存在,直接修改属性值 | |
if (key in target && !(key in Object.prototype)) { | |
target[key] = val | |
return val | |
} | |
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 方法进行响应式处理
defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法
nextTick在哪里使用?原理是?
nextTick
中的回调是在下次DOM
更新循环结束之后执行延迟回调,用于获得更新后的DOM
- 在修改数据之后立即使用这个方法,获取更新后的
DOM
- 主要思路就是采用
微任务优先
的方式调用异步方法去执行nextTick
包装的方法
nextTick
方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用nextTick
会将方法存入队列中,通过这个异步方法清空当前队列。所以这个nextTick
方法就是异步方法
根据执行环境分别尝试采用
- 先采用
Promise
Promise
不支持,再采用MutationObserver
MutationObserver
不支持,再采用setImmediate
- 如果以上都不行则采用
setTimeout
- 最后执行
flushCallbacks
,把callbacks
里面的数据依次执行
回答范例
nextTick
中的回调是在下次DOM
更新循环结束之后执行延迟回调,用于获得更新后的DOM
Vue
有个异步更新策略,意思是如果数据变化,Vue
不会立刻更新DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick
- 开发时,有两个场景我们会用到
nextTick
created
中想要获取DOM
时
- 响应式数据变化后获取
DOM
更新后的状态,比如希望获取列表更新后的高度 nextTick
签名如下:function nextTick(callback?: () => void): Promise<void>
所以我们只需要在传入的回调函数中访问最新DOM状态即可,或者我们可以await nextTick()
方法返回的Promise
之后做这件事
- 在
Vue
内部,nextTick
之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback
会被添加到队列刷新函数(flushSchedulerQueue
)的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback
自然能够获取到最新的DOM值
基本使用
const vm = new Vue({ | |
el: '#app', | |
data() { | |
return { a: 1 } | |
} | |
}); | |
// vm.$nextTick(() => {// [nextTick回调函数fn,内部更新flushSchedulerQueue] | |
// console.log(vm.$el.innerHTML) | |
// }) | |
// 是将内容维护到一个数组里,最终按照顺序顺序。 第一次会开启一个异步任务 | |
vm.a = 'test'; // 修改了数据后并不会马上更新视图 | |
vm.$nextTick(() => {// [nextTick回调函数fn,内部更新flushSchedulerQueue] | |
console.log(vm.$el.innerHTML) | |
}) | |
// nextTick中的方法会被放到 更新页面watcher的后面去 |
相关代码如下
// src/core/utils/nextTick | |
let callbacks = []; | |
let pending = false; | |
function flushCallbacks() { | |
pending = false; //把标志还原为false | |
// 依次执行回调 | |
for (let i = 0; i < callbacks.length; i++) { | |
callbacks[i](); | |
} | |
} | |
let timerFunc; //定义异步方法 采用优雅降级 | |
if (typeof Promise !== "undefined") { | |
// 如果支持promise | |
const p = Promise.resolve(); | |
timerFunc = () => { | |
p.then(flushCallbacks); | |
}; | |
} else if (typeof MutationObserver !== "undefined") { | |
// MutationObserver 主要是监听dom变化 也是一个异步方法 | |
let counter = 1; | |
const observer = new MutationObserver(flushCallbacks); | |
const textNode = document.createTextNode(String(counter)); | |
observer.observe(textNode, { | |
characterData: true, | |
}); | |
timerFunc = () => { | |
counter = (counter + 1) % 2; | |
textNode.data = String(counter); | |
}; | |
} else if (typeof setImmediate !== "undefined") { | |
// 如果前面都不支持 判断setImmediate | |
timerFunc = () => { | |
setImmediate(flushCallbacks); | |
}; | |
} else { | |
// 最后降级采用setTimeout | |
timerFunc = () => { | |
setTimeout(flushCallbacks, 0); | |
}; | |
} | |
export function nextTick(cb) { | |
// 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组 | |
callbacks.push(cb); | |
if (!pending) { | |
// 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false | |
pending = true; | |
timerFunc(); | |
} | |
} |
数据更新的时候内部会调用nextTick
// src/core/observer/scheduler.js | |
export function queueWatcher (watcher: Watcher) { | |
const id = watcher.id | |
if (has[id] == null) { | |
has[id] = true | |
if (!flushing) { | |
queue.push(watcher) | |
} else { | |
// if already flushing, splice the watcher based on its id | |
// if already past its id, it will be run next immediately. | |
let i = queue.length - 1 | |
while (i > index && queue[i].id > watcher.id) { | |
i-- | |
} | |
queue.splice(i + 1, 0, watcher) | |
} | |
// queue the flush | |
if (!waiting) { | |
waiting = true | |
if (process.env.NODE_ENV !== 'production' && !config.async) { | |
flushSchedulerQueue() | |
return | |
} | |
// 把更新方法放到数组中维护[nextTick回调函数,更新函数flushSchedulerQueue] | |
/** | |
* vm.a = 'test'; // 修改了数据后并不会马上更新视图 | |
vm.$nextTick(() => {// [fn,更新] | |
console.log(vm.$el.innerHTML) | |
}) | |
*/ | |
nextTick(flushSchedulerQueue) | |
} | |
} | |
} |
Vue 的生命周期方法有哪些 一般在哪一步发请求
beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问
created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el,如果非要想与 Dom 进行交互,可以通过 vm.$nextTick 来访问 Dom
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点
beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程
updated 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。
destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
activated keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
异步请求在哪一步发起?
可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面 loading 时间;
- ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;
computed和watch区别
- 当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性
computed
Computed
本质是一个具备缓存的watcher
,依赖的属性发生变化就会更新视图。 适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理
<template>{{fullName}}</template> | |
export default { | |
data(){ | |
return { | |
firstName: 'zhang', | |
lastName: 'san', | |
} | |
}, | |
computed:{ | |
fullName: function(){ | |
return this.firstName + ' ' + this.lastName | |
} | |
} | |
} |
watch
用于观察和监听页面上的vue实例,如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch
为最佳选择
Watch
没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true
选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch
手动注销
<template>{{fullName}}</template> | |
export default { | |
data(){ | |
return { | |
firstName: 'zhang', | |
lastName: 'san', | |
fullName: 'zhang san' | |
} | |
}, | |
watch:{ | |
firstName(val) { | |
this.fullName = val + ' ' + this.lastName | |
}, | |
lastName(val) { | |
this.fullName = this.firstName + ' ' + val | |
} | |
} | |
} |
computed:
computed
是计算属性,也就是计算值,它更多用于计算值的场景computed
具有缓存性,computed
的值在getter
执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed
的值时才会重新调用对应的getter
来计算computed
适用于计算比较消耗性能的计算场景
watch:
- 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察
props
$emit
或者本组件的值,当数据变化时来执行回调进行后续操作 - 无缓存性,页面重新渲染时值不变化也会执行
小结:
computed
和watch
都是基于watcher
来实现的computed
属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行watch
是监控值的变化,当值发生变化时调用其对应的回调函数- 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为
computed
- 如果你需要在某个数据变化时做一些事情,使用
watch
来观察这个数据变化
回答范例
思路分析
- 先看
computed
,watch
两者定义,列举使用上的差异 - 列举使用场景上的差异,如何选择
- 使用细节、注意事项
vue3
变化
computed
特点:具有响应式的返回值
const count = ref(1) | |
const plusOne = computed(() => count.value + 1) |
watch
特点:侦测变化,执行回调
const state = reactive({ count: 0 }) | |
watch( | |
() => state.count, | |
(count, prevCount) => { | |
/* ... */ | |
} | |
) |
回答范例
- 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,
computed
和methods
的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch没有返回值,但可以执行异步操作等复杂逻辑 - 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的DOM操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性.
- 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。
watch
可以传递对象,设置deep
、immediate
等选项 vue3
中watch
选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式;reactivity API
中新出现了watch
、watchEffect
可以完全替代目前的watch
选项,且功能更加强大
基本使用
// src/core/observer:45; | |
// 渲染watcher / computed watcher / watch | |
const vm = new Vue({ | |
el: '#app', | |
data: { | |
firstname:'张', | |
lastname:'三' | |
}, | |
computed:{ // watcher => firstname lastname | |
// computed 只有取值时才执行 | |
// Object.defineProperty .get | |
fullName(){ // firstName lastName 会收集fullName计算属性 | |
return this.firstname + this.lastname | |
} | |
}, | |
watch:{ | |
firstname(newVal,oldVal){ | |
console.log(newVal) | |
} | |
} | |
}); | |
setTimeout(() => { | |
debugger; | |
vm.firstname = '赵' | |
}, 1000); |
相关源码
// 初始化state | |
function initState (vm: Component) { | |
vm._watchers = [] | |
const opts = vm.$options | |
if (opts.props) initProps(vm, opts.props) | |
if (opts.methods) initMethods(vm, opts.methods) | |
if (opts.data) { | |
initData(vm) | |
} else { | |
observe(vm._data = {}, true /* asRootData */) | |
} | |
// 初始化计算属性 | |
if (opts.computed) initComputed(vm, opts.computed) | |
// 初始化watch | |
if (opts.watch && opts.watch !== nativeWatch) { | |
initWatch(vm, opts.watch) | |
} | |
} | |
// 计算属性取值函数 | |
function createComputedGetter (key) { | |
return function computedGetter () { | |
const watcher = this._computedWatchers && this._computedWatchers[key] | |
if (watcher) { | |
if (watcher.dirty) { // 如果值依赖的值发生变化,就会进行重新求值 | |
watcher.evaluate(); // this.firstname lastname | |
} | |
if (Dep.target) { // 让计算属性所依赖的属性 收集渲染watcher | |
watcher.depend() | |
} | |
return watcher.value | |
} | |
} | |
} | |
// watch的实现 | |
Vue.prototype.$watch = function ( | |
expOrFn: string | Function, | |
cb: any, | |
options?: Object | |
): Function { | |
const vm: Component = this | |
debugger; | |
if (isPlainObject(cb)) { | |
return createWatcher(vm, expOrFn, cb, options) | |
} | |
options = options || {} | |
options.user = true | |
const watcher = new Watcher(vm, expOrFn, cb, options) // 创建watcher,数据更新调用cb | |
if (options.immediate) { | |
try { | |
cb.call(vm, watcher.value) | |
} catch (error) { | |
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) | |
} | |
} | |
return function unwatchFn () { | |
watcher.teardown() | |
} | |
} |
v-show 与 v-if 有什么区别?
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。