组件通信

组件通信的形式如下:

(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)发送事件 假如有两个兄弟组件firstComsecondCom

<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提供的两个钩子,和datamethods是同级的。并且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 种拦挡方:不限于applyownKeysdeletePropertyhas等等是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 路由钩子函数是什么 执行程序是什么

路由钩子的执行流程, 钩子函数品种有:全局守卫、路由守卫、组件守卫

残缺的导航解析流程:

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创立好的组件实例会作为回调函数的参数传入。

参考:前端vue面试题具体解答

diff算法

工夫复杂度: 个树的齐全 diff 算法是一个工夫复杂度为 O(n*3) ,vue进行优化转化成 O(n)

了解:

  • 最小量更新, key 很重要。这个能够是这个节点的惟一标识,通知 diff 算法,在更改前后它们是同一个DOM节点

    • 扩大 v-for 为什么要有 key ,没有 key 会暴力复用,举例子的话轻易说一个比方挪动节点或者减少节点(批改DOM),加 key 只会挪动缩小操作DOM。
  • 只有是同一个虚构节点才会进行精细化比拟,否则就是暴力删除旧的,插入新的。
  • 只进行同层比拟,不会进行跨层比拟。

diff算法的优化策略:四种命中查找,四个指针

  1. 旧前与新前(先比结尾,后插入和删除节点的这种状况)
  2. 旧后与新后(比结尾,前插入或删除的状况)
  3. 旧前与新后(头与尾比,此种产生了,波及挪动节点,那么新前指向的节点,挪动到旧后之后)
  4. 旧后与新前(尾与头比,此种产生了,波及挪动节点,那么新前指向的节点,挪动到旧前之前)

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() 解决对象新增属性不能响应的问题

  1. Vue应用了Object.defineProperty实现双向数据绑定
  2. 在初始化实例时对属性执行 getter/setter 转化
  3. 属性必须在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 的实现原理是:

  1. 如果指标是数组,间接应用数组的 splice 办法触发相应式;
  2. 如果指标是对象,会先判读属性是否存在、对象是否是响应式,
  3. 最终如果要对属性进行响应式解决,则是通过调用 defineReactive 办法进行响应式解决
defineReactive 办法就是 Vue 在初始化对象时,给对象属性采纳 Object.defineProperty 动静增加 getter 和 setter 的性能所调用的办法

理解nextTick吗?

异步办法,异步渲染最初一步,与JS事件循环分割严密。次要应用了宏工作微工作(setTimeoutpromise那些),定义了一个异步办法,屡次调用nextTick会将办法存入队列,通过异步办法清空以后队列。

computed和watch区别

  1. 当页面中有某些数据依赖其余数据进行变动的时候,能够应用计算属性computed
Computed实质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。 实用于计算比拟耗费性能的计算场景。当表达式过于简单时,在模板中放入过多逻辑会让模板难以保护,能够将简单的逻辑放入计算属性中解决

<template>{{fullName}}</template>export default {    data(){        return {            firstName: 'zhang',            lastName: 'san',        }    },    computed:{        fullName: function(){            return this.firstName + ' ' + this.lastName        }    }}
  1. 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或者本组件的值,当数据变动时来执行回调进行后续操作
  • 无缓存性,页面从新渲染时值不变动也会执行

小结:

  • computedwatch都是基于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) => {    /* ... */  })

答复范例

  1. 计算属性能够从组件数据派生出新数据,最常见的应用形式是设置一个函数,返回计算之后的后果,computedmethods的差别是它具备缓存性,如果依赖项不变时不会从新计算。侦听器能够侦测某个响应式数据的变动并执行副作用,常见用法是传递一个函数,执行副作用,watch没有返回值,但能够执行异步操作等简单逻辑
  2. 计算属性罕用场景是简化行内模板中的简单表达式,模板中呈现太多逻辑会是模板变得臃肿不易保护。侦听器罕用场景是状态变动之后做一些额定的DOM操作或者异步操作。抉择采纳何用计划时首先看是否须要派生出新值,根本能用计算属性实现的形式首选计算属性.
  3. 应用过程中有一些细节,比方计算属性也是能够传递对象,成为既可读又可写的计算属性。watch能够传递对象,设置deepimmediate等选项
  4. vue3watch选项产生了一些变动,例如不再能侦测一个点操作符之外的字符串模式的表达式; reactivity API中新呈现了watchwatchEffect能够齐全代替目前的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') }