组件通信
组件通信的方式如下:
(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 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。
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 Ref的作用
- 获取
dom
元素this.$refs.box
- 获取子组件中的
datathis.$refs.box.msg
- 调用子组件中的方法
this.$refs.box.open()
Vue组件data为什么必须是个函数?
- 根实例对象
data
可以是对象也可以是函数 (根实例是单例),不会产生数据污染情况 - 组件实例对象
data
必须为函数 一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data
是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data
不冲突,data
必须是一个函数,
简版理解
// 1.组件的渲染流程 调用Vue.component -> Vue.extend -> 子类 -> new 子类
// Vue.extend 根据用户定义产生一个新的类
function Vue() {}
function Sub() { // 会将data存起来
this.data = this.constructor.options.data();
}
Vue.extend = function(options) {
Sub.options = options; // 静态属性
return Sub;
}
let Child = Vue.extend({
data:()=>( { name: 'zf' })
});
// 两个组件就是两个实例, 希望数据互不感染
let child1 = new Child();
let child2 = new Child();
console.log(child1.data.name);
child1.data.name = 'poetry';
console.log(child2.data.name);
// 根不需要 任何的合并操作 根才有vm属性 所以他可以是函数和对象 但是组件mixin他们都没有vm 所以我就可以判断 当前data是不是个函数
相关源码
// 源码位置 src/core/global-api/extend.js
export function initExtend (Vue: GlobalAPI) {
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
const Sub = function VueComponent (options) {
this._init(options)
}
// 子类继承大Vue父类的原型
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub // 记录自己 在组件中递归自己 -> jsx
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}
}
Vue 模板编译原理
Vue 的编译过程就是将 template 转化为 render 函数的过程 分为以下三步
第一步是将 模板字符串 转换成 element ASTs(解析器)
第二步是对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
第三步是 使用 element ASTs 生成 render 函数代码字符串(代码生成器)
说说你对 proxy 的理解,Proxy 相比于 defineProperty 的优势
Object.defineProperty()
的问题主要有三个:
- 不能监听数组的变化 :无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应
- 必须遍历对象的每个属性 :只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果属性值是对象,还需要深度遍历。
Proxy
可以劫持整个对象,并返回一个新的对象 - 必须深层遍历嵌套的对象
Proxy的优势如下:
- 针对对象: 针对整个对象,而不是对象的某个属性 ,所以也就不需要对
keys
进行遍历 - 支持数组:
Proxy
不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的 Proxy
的第二个参数可以有13
种拦截方:不限于apply
、ownKeys
、deleteProperty
、has
等等是Object.defineProperty
不具备的Proxy
返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty
只能遍历对象属性直接修改Proxy
作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利
proxy详细使用点击查看(opens new window)
Object.defineProperty的优势如下:
兼容性好,支持IE9
,而Proxy
的存在浏览器兼容性问题,而且无法用polyfill
磨平
defineProperty的属性值有哪些
Object.defineProperty(obj, prop, descriptor)
// obj 要定义属性的对象
// prop 要定义或修改的属性的名称
// descriptor 要定义或修改的属性描述符
Object.defineProperty(obj,"name",{
value:"poetry", // 初始值
writable:true, // 该属性是否可写入
enumerable:true, // 该属性是否可被遍历得到(for...in, Object.keys等)
configurable:true, // 定该属性是否可被删除,且除writable外的其他描述符是否可被修改
get: function() {},
set: function(newVal) {}
})
相关代码如下
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, // 当修改属性时调用此方法
};
Proxy
只会代理对象的第一层,那么Vue3
又是怎样处理这个问题的呢?
判断当前Reflect.get的
返回值是否为Object
,如果是则再通过reactive
方法做代理, 这样就实现了深度观测。
监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?
我们可以判断key
是否为当前被代理对象target
自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger
vue-router 路由钩子函数是什么 执行顺序是什么
路由钩子的执行流程, 钩子函数种类有:全局守卫、路由守卫、组件守卫
完整的导航解析流程:
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫 (2.5+)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
参考:前端vue面试题详细解答
diff算法
时间复杂度: 个树的完全 diff
算法是一个时间复杂度为 O(n*3)
,vue进行优化转化成 O(n)
。
理解:
最小量更新,
key
很重要。这个可以是这个节点的唯一标识,告诉diff
算法,在更改前后它们是同一个DOM节点- 扩展
v-for
为什么要有key
,没有key
会暴力复用,举例子的话随便说一个比如移动节点或者增加节点(修改DOM),加key
只会移动减少操作DOM。
- 扩展
- 只有是同一个虚拟节点才会进行精细化比较,否则就是暴力删除旧的,插入新的。
- 只进行同层比较,不会进行跨层比较。
diff算法的优化策略:四种命中查找,四个指针
- 旧前与新前(先比开头,后插入和删除节点的这种情况)
- 旧后与新后(比结尾,前插入或删除的情况)
- 旧前与新后(头与尾比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧后之后)
- 旧后与新前(尾与头比,此种发生了,涉及移动节点,那么新前指向的节点,移动到旧前之前)
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 还包含了其他的响应逻辑。
谈谈对keep-alive的了解
keep-alive 可以实现组件的缓存,当组件切换时不会对当前组件进行卸载。常用的2个属性
include/exclude ,2个生命周期
activated ,
deactivated
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适合频繁切换。
Vue 初始化页面闪动问题如何解决?
出现该问题是因为在 Vue 代码尚未被解析之前,尚无法控制页面中 DOM 的显示,所以会看见模板字符串等代码。
解决方案是,在 css 代码中添加 v-cloak 规则,同时在待编译的标签上添加 v-cloak 属性:
[v-cloak] { display: none; }
<div v-cloak>
{{ message }}
</div>
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 表单元素为例:
<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', '小红')
},
},
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吗?
异步方法,异步渲染最后一步,与JS事件循环联系紧密。主要使用了宏任务微任务(setTimeout
、promise
那些),定义了一个异步方法,多次调用nextTick
会将方法存入队列,通过异步方法清空当前队列。
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()
}
}
谈一下对 vuex 的个人理解
vuex 是专门为 vue 提供的全局状态管理系统,用于多个组件中数据共享、数据缓存等。(无法持久化、内部核心原理是通过创造一个全局实例 new Vue)
主要包括以下几个模块:
- State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
- Getter:允许组件从 Store 中获取数据,mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
- Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
- Action:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
- Module:允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
vue是如何实现响应式数据的呢?(响应式数据原理)
Vue2: Object.defineProperty
重新定义 data
中所有的属性, Object.defineProperty
可以使数据的获取与设置增加一个拦截的功能,拦截属性的获取,进行依赖收集。拦截属性的更新操作,进行通知。
具体的过程:首先Vue使用 initData
初始化用户传入的参数,然后使用 new Observer
对数据进行观测,如果数据是一个对象类型就会调用 this.walk(value)
对对象进行处理,内部使用 defineeReactive
循环对象属性定义响应式变化,核心就是使用 Object.defineProperty
重新定义数据。
说说Vue的生命周期吧
什么时候被调用?
- beforeCreate :实例初始化之后,数据观测之前调用
- created:实例创建万之后调用。实例完成:数据观测、属性和方法的运算、
watch/event
事件回调。无$el
. - beforeMount:在挂载之前调用,相关
render
函数首次被调用 - mounted:了被新创建的
vm.$el
替换,并挂载到实例上去之后调用改钩子。 - beforeUpdate:数据更新前调用,发生在虚拟DOM重新渲染和打补丁,在这之后会调用改钩子。
- updated:由于数据更改导致的虚拟DOM重新渲染和打补丁,在这之后会调用改钩子。
- beforeDestroy:实例销毁前调用,实例仍然可用。
- destroyed:实例销毁之后调用,调用后,Vue实例指示的所有东西都会解绑,所有事件监听器和所有子实例都会被移除
每个生命周期内部可以做什么?
- created:实例已经创建完成,因为他是最早触发的,所以可以进行一些数据、资源的请求。
- mounted:实例已经挂载完成,可以进行一些DOM操作。
- beforeUpdate:可以在这个钩子中进一步的更改状态,不会触发重渲染。
- updated:可以执行依赖于DOM的操作,但是要避免更改状态,可能会导致更新无线循环。
- destroyed:可以执行一些优化操作,清空计时器,解除绑定事件。
ajax放在哪个生命周期?:一般放在 mounted
中,保证逻辑统一性,因为生命周期是同步执行的, ajax
是异步执行的。单数服务端渲染 ssr
同一放在 created
中,因为服务端渲染不支持 mounted
方法。 什么时候使用beforeDestroy?:当前页面使用 $on
,需要解绑事件。清楚定时器。解除事件绑定, scroll mousemove
。
Vue 的父子组件生命周期钩子函数执行顺序
- 渲染顺序 :先父后子,完成顺序:先子后父
- 更新顺序 :父更新导致子更新,子更新完成后父
- 销毁顺序 :先父后子,完成顺序:先子后父
加载渲染过程
父 beforeCreate
->父 created
->父 beforeMount
->子 beforeCreate
->子 created
->子 beforeMount
->子 mounted
->父 mounted
。子组件先挂载,然后到父组件
子组件更新过程
父 beforeUpdate
->子 beforeUpdate
->子 updated
->父 updated
父组件更新过程
父 beforeUpdate
->父 updated
销毁过程
父 beforeDestroy
->子 beforeDestroy
->子 destroyed
->父 destroyed
之所以会这样是因为Vue
创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加mounted
钩子到队列,等到patch
结束再执行它们,可见子组件的mounted
钩子是先进入到队列中的,因此等到patch
结束执行这些钩子时也先执行。
function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return
}
let isInitialPatch = false
const insertedVnodeQueue = [] // 定义收集所有组件的insert hook方法的数组 // somthing ...
createElm(
vnode,
insertedVnodeQueue, oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)// somthing...
// 最终会依次调用收集的insert hook
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
return vnode.elm
}
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) {
// createChildren 会递归创建儿子组件
createChildren(vnode, children, insertedVnodeQueue) // something...
}
// 将组件的vnode插入到数组中
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
// insert方法中会依次调用mounted方法
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
}
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the // element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i]); // 调用insert方法
}
}
}
Vue.prototype.$destroy = function () {
callHook(vm, 'beforeDestroy')
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null) // 先销毁儿子
// fire destroyed hook
callHook(vm, 'destroyed')
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。