共计 21896 个字符,预计需要花费 55 分钟才能阅读完成。
组件通信
组件通信的形式如下:
(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')
}