组件通信
组件通信的形式如下:
(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.jsimport 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.jsexport 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 / watchconst 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);
相干源码
// 初始化statefunction 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') }