关于前端:前端常考vue面试题必备

3次阅读

共计 21102 个字符,预计需要花费 53 分钟才能阅读完成。

computed 的实现原理

computed 实质是一个惰性求值的观察者。

computed 外部实现了一个惰性的 watcher, 也就是 computed watcher,computed watcher 不会立即求值, 同时持有一个 dep 实例。

其外部通过 this.dirty 属性标记计算属性是否须要从新求值。

当 computed 的依赖状态产生扭转时, 就会告诉这个惰性的 watcher,

computed watcher 通过 this.dep.subs.length 判断有没有订阅者,

有的话, 会从新计算, 而后比照新旧值, 如果变动了, 会从新渲染。(Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 从新渲染,实质上是一种优化。)

没有的话, 仅仅把 this.dirty = true。(当计算属性依赖于其余数据时,属性并不会立刻从新计算,只有之后其余中央须要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)个性。)

Computed 和 Methods 的区别

能够将同一函数定义为一个 method 或者一个计算属性。对于最终的后果,两种形式是雷同的

不同点:

  • computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相干依赖产生扭转时才会从新求值;
  • method 调用总会执行该函数。

Vue 生命周期钩子是如何实现的

  • vue的生命周期钩子就是回调函数而已,当创立组件实例的过程中会调用对应的钩子办法
  • 外部会对钩子函数进行解决,将钩子函数保护成数组的模式

Vue 的生命周期钩子外围实现是利用公布订阅模式先把用户传入的的生命周期钩子订阅好(外部采纳数组的形式存储)而后在创立组件实例的过程中会一次执行对应的钩子办法(公布)

<script>
    // Vue.options 中会寄存所有全局属性

    // 会用本身的 + Vue.options 中的属性进行合并
    // Vue.mixin({//     beforeCreate() {//         console.log('before 0')
    //     },
    // })
    debugger;
    const vm = new Vue({
        el: '#app',
        beforeCreate: [function() {console.log('before 1')
            },
            function() {console.log('before 2')
            }
        ]
    });
    console.log(vm);
</script>

相干代码如下

export function callHook(vm, hook) {
  // 顺次执行生命周期对应的办法
  const handlers = vm.$options[hook];
  if (handlers) {for (let i = 0; i < handlers.length; i++) {handlers[i].call(vm); // 生命周期外面的 this 指向以后实例
    }
  }
}

// 调用的时候
Vue.prototype._init = function (options) {
  const vm = this;
  vm.$options = mergeOptions(vm.constructor.options, options);
  callHook(vm, "beforeCreate"); // 初始化数据之前
  // 初始化状态
  initState(vm);
  callHook(vm, "created"); // 初始化数据之后
  if (vm.$options.el) {vm.$mount(vm.$options.el);
  }
};

// 销毁实例实现
Vue.prototype.$destory = function() {
     // 触发钩子
    callHook(vm, 'beforeDestory')
    // 本身及子节点
    remove() 
    // 删除依赖
    watcher.teardown() 
    // 删除监听
    vm.$off() 
    // 触发钩子
    callHook(vm, 'destoryed')
}

原理流程图

Class 与 Style 如何动静绑定

Class 能够通过对象语法和数组语法进行动静绑定

对象语法:

<div v-bind:class="{active: isActive,'text-danger': hasError}"></div>

data: {
  isActive: true,
  hasError: false
}

数组语法:

<div v-bind:class="[isActive ? activeClass :'', errorClass]"></div>

data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}

Style 也能够通过对象语法和数组语法进行动静绑定

对象语法:

<div v-bind:style="{color: activeColor, fontSize: fontSize +'px'}"></div>

data: {
  activeColor: 'red',
  fontSize: 30
}

数组语法:

<div v-bind:style="[styleColor, styleSize]"></div>

data: {
  styleColor: {color: 'red'},
  styleSize:{fontSize:'23px'}
}

extend 有什么作用

这个 API 很少用到,作用是扩大组件生成一个结构器,通常会与 $mount 一起应用。

// 创立组件结构器
let Component = Vue.extend({template: "<div>test</div>"});
// 挂载到 #app 上 new Component().$mount('#app')
// 除了下面的形式,还能够用来扩大已有的组件
let SuperComponent = Vue.extend(Component);
new SuperComponent({created() {console.log(1);
  },
});
new SuperComponent().$mount("#app");

vue-router 路由钩子函数是什么 执行程序是什么

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

残缺的导航解析流程:

  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 的回调函数,创立好的组件实例会作为回调函数的参数传入。

参考 前端进阶面试题具体解答

v-show 与 v-if 有什么区别?

v-if 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是 惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简略得多——不论初始条件是什么,元素总是会被渲染,并且只是简略地基于 CSS 的“display”属性进行切换。

所以,v-if 实用于在运行时很少扭转条件,不须要频繁切换条件的场景;v-show 则实用于须要十分频繁切换条件的场景。

MVVM、MVC、MVP 的区别

MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,次要通过拆散关注点的形式来组织代码构造,优化开发效率。

在开发单页面利用时,往往一个路由页面对应了一个脚本文件,所有的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应所有的应用逻辑都混合在一起,这样在开发简略我的项目时,可能看不出什么问题,如果我的项目变得复杂,那么整个文件就会变得简短、凌乱,这样对我的项目开发和前期的我的项目保护是十分不利的。

(1)MVC

MVC 通过拆散 Model、View 和 Controller 的形式来组织代码构造。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操作。并且 View 和 Model 利用了观察者模式,当 Model 层产生扭转的时候它会告诉无关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它次要负责用户与利用的响应操作,当用户与页面产生交互的时候,Controller 中的事件触发器就开始工作了,通过调用 Model 层,来实现对 Model 的批改,而后 Model 层再去告诉 View 层更新。

(2)MVVM

MVVM 分为 Model、View、ViewModel:

  • Model 代表数据模型,数据和业务逻辑都在 Model 层中定义;
  • View 代表 UI 视图,负责数据的展现;
  • ViewModel 负责监听 Model 中数据的扭转并且管制视图的更新,解决用户交互操作;

Model 和 View 并无间接关联,而是通过 ViewModel 来进行分割的,Model 和 ViewModel 之间有着双向数据绑定的分割。因而当 Model 中的数据扭转时会触发 View 层的刷新,View 中因为用户交互操作而扭转的数据也会在 Model 中同步。

这种模式实现了 Model 和 View 的数据主动同步,因而开发者只须要专一于数据的保护操作即可,而不须要本人操作 DOM。

(3)MVP

MVP 模式与 MVC 惟一不同的在于 Presenter 和 Controller。在 MVC 模式中应用观察者模式,来实现当 Model 层数据发生变化的时候,告诉 View 层的更新。这样 View 层和 Model 层耦合在一起,当我的项目逻辑变得复杂的时候,可能会造成代码的凌乱,并且可能会对代码的复用性造成一些问题。MVP 的模式通过应用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的 Controller 只晓得 Model 的接口,因而它没有方法管制 View 层的更新,MVP 模式中,View 层的接口裸露给了 Presenter 因而能够在 Presenter 中将 Model 的变动和 View 的变动绑定在一起,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还蕴含了其余的响应逻辑。

Vue 初始化页面闪动问题如何解决?

呈现该问题是因为在 Vue 代码尚未被解析之前,尚无法控制页面中 DOM 的显示,所以会看见模板字符串等代码。
解决方案是,在 css 代码中增加 v-cloak 规定,同时在待编译的标签上增加 v-cloak 属性:

[v-cloak] {display: none;}

<div v-cloak>
  {{message}}
</div>

Vue 中给 data 中的对象属性增加一个新的属性时会产生什么?如何解决?

<template> 
   <div>
      <ul>
         <li v-for="value in obj" :key="value"> {{value}} </li>       </ul>       <button @click="addObjB"> 增加 obj.b</button>    </div>
</template>

<script>
    export default {data () {return {               obj: {                   a: 'obj.a'}           }        },       methods: {addObjB () {this.obj.b = 'obj.b'               console.log(this.obj)           }       }   }
</script>

点击 button 会发现,obj.b 曾经胜利增加,然而视图并未刷新。这是因为在 Vue 实例创立时,obj.b 并未申明,因而就没有被 Vue 转换为响应式的属性,天然就不会触发视图的更新,这时就须要应用 Vue 的全局 api $set():

addObjB () (this.$set(this.obj, 'b', 'obj.b')
   console.log(this.obj)
}

$set()办法相当于手动的去把 obj.b 解决成一个响应式的属性,此时视图也会跟着扭转了。

常见的事件修饰符及其作用

  • .stop:等同于 JavaScript 中的 event.stopPropagation(),避免事件冒泡;
  • .prevent:等同于 JavaScript 中的 event.preventDefault(),避免执行预设的行为(如果事件可勾销,则勾销该事件,而不进行事件的进一步流传);
  • .capture:与事件冒泡的方向相同,事件捕捉由外到内;
  • .self:只会触发本人范畴内的事件,不蕴含子元素;
  • .once:只会触发一次。

v-if 和 v -show 的区别

  • 伎俩:v-if 是动静的向 DOM 树内增加或者删除 DOM 元素;v-show 是通过设置 DOM 元素的 display 款式属性管制显隐;
  • 编译过程:v-if 切换有一个部分编译 / 卸载的过程,切换过程中适合地销毁和重建外部的事件监听和子组件;v-show 只是简略的基于 css 切换;
  • 编译条件:v-if 是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始部分编译; v-show 是在任何条件下,无论首次条件是否为真,都被编译,而后被缓存,而且 DOM 元素保留;
  • 性能耗费:v-if 有更高的切换耗费;v-show 有更高的初始渲染耗费;
  • 应用场景:v-if 适宜经营条件不大可能扭转;v-show 适宜频繁切换。

组件通信

组件通信的形式如下:

(1)props / $emit

父组件通过 props 向子组件传递数据,子组件通过 $emit 和父组件通信

1. 父组件向子组件传值
  • props只能是父组件向子组件进行传值,props使得父子组件之间造成了一个单向上行绑定。子组件的数据会随着父组件不断更新。
  • props 能够显示定义一个或一个以上的数据,对于接管的数据,能够是各种数据类型,同样也能够传递一个函数。
  • props属性名规定:若在 props 中应用驼峰模式,模板中须要应用短横线的模式
// 父组件
<template>
  <div id="father">
    <son :msg="msgData" :fn="myFunction"></son>
  </div>
</template>

<script>
import son from "./son.vue";
export default {
  name: father,
  data() {msgData: "父组件数据";},
  methods: {myFunction() {console.log("vue");
    },
  },
  components: {son},
};
</script>
// 子组件
<template>
  <div id="son">
    <p>{{msg}}</p>
    <button @click="fn"> 按钮 </button>
  </div>
</template>
<script>
export default {name: "son", props: ["msg", "fn"] };
</script>
2. 子组件向父组件传值
  • $emit绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过 v-on 监听并接管参数。
// 父组件
<template>
  <div class="section">
    <com-article
      :articles="articleList"
      @onEmitIndex="onEmitIndex"
    ></com-article>
    <p>{{currentIndex}}</p>
  </div>
</template>

<script>
import comArticle from "./test/article.vue";
export default {
  name: "comArticle",
  components: {comArticle},
  data() {return { currentIndex: -1, articleList: ["红楼梦", "西游记", "三国演义"] };
  },
  methods: {onEmitIndex(idx) {this.currentIndex = idx;},
  },
};
</script>
// 子组件
<template>
  <div>
    <div
      v-for="(item, index) in articles"
      :key="index"
      @click="emitIndex(index)"
    >
      {{item}}
    </div>
  </div>
</template>

<script>
export default {props: ["articles"],
  methods: {emitIndex(index) {this.$emit("onEmitIndex", index); // 触发父组件的办法,并传递参数 index
    },
  },
};
</script>

(2)eventBus 事件总线($emit / $on

eventBus事件总线实用于 父子组件 非父子组件 等之间的通信,应用步骤如下:(1)创立事件核心治理组件之间的通信

// event-bus.js

import Vue from 'vue'
export const EventBus = new Vue()

(2)发送事件 假如有两个兄弟组件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 的思维就是将这一些公共的数据抽离进去,将它作为一个全局的变量来治理,而后其余组件就能够对这个公共数据进行读写操作,这样达到理解耦的目标。

如何从实在 DOM 到虚构 DOM

波及到 Vue 中的模板编译原理,次要过程:

  1. 将模板转换成 ast 树, ast 用对象来形容实在的 JS 语法(将实在 DOM 转换成虚构 DOM)
  2. 优化树
  3. ast 树生成代码

Vue3.0 和 2.0 的响应式原理区别

Vue3.x 改用 Proxy 代替 Object.defineProperty。因为 Proxy 能够间接监听对象和数组的变动,并且有多达 13 种拦挡办法。

相干代码如下

import {mutableHandlers} from "./baseHandlers"; // 代理相干逻辑
import {isObject} from "./util"; // 工具办法

export function reactive(target) {
  // 依据不同参数创立不同响应式对象
  return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {if (!isObject(target)) {return target;}
  const observed = new Proxy(target, baseHandler);
  return observed;
}

const get = createGetter();
const set = createSetter();

function createGetter() {return function get(target, key, receiver) {
    // 对获取的值进行喷射
    const res = Reflect.get(target, key, receiver);
    console.log("属性获取", key);
    if (isObject(res)) {
      // 如果获取的值是对象类型,则返回以后对象的代理对象
      return reactive(res);
    }
    return res;
  };
}
function createSetter() {return function set(target, key, value, receiver) {const oldValue = target[key];
    const hadKey = hasOwn(target, key);
    const result = Reflect.set(target, key, value, receiver);
    if (!hadKey) {console.log("属性新增", key, value);
    } else if (hasChanged(value, oldValue)) {console.log("属性值被批改", key, value);
    }
    return result;
  };
}
export const mutableHandlers = {
  get, // 当获取属性时调用此办法
  set, // 当批改属性时调用此办法
};

Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题?你能说说如下代码的实现原理么?

1)Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题

  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 在哪里应用?原理是?

  • nextTick 中的回调是在下次 DOM 更新循环完结之后执行提早回调,用于取得更新后的 DOM
  • 在批改数据之后立刻应用这个办法,获取更新后的 DOM
  • 次要思路就是采纳 微工作优先 的形式调用异步办法去执行 nextTick 包装的办法

nextTick 办法次要是应用了宏工作和微工作, 定义了一个异步办法. 屡次调用 nextTick 会将办法存入队列中,通过这个异步办法清空以后队列。所以这个 nextTick 办法就是异步办法

依据执行环境别离尝试采纳

  • 先采纳Promise
  • Promise不反对,再采纳MutationObserver
  • MutationObserver不反对,再采纳setImmediate
  • 如果以上都不行则采纳setTimeout
  • 最初执行 flushCallbacks,把callbacks 外面的数据顺次执行

答复范例

  1. nextTick 中的回调是在下次 DOM 更新循环完结之后执行提早回调,用于取得更新后的 DOM
  2. Vue有个异步更新策略,意思是如果数据变动,Vue不会立即更新 DOM,而是开启一个队列,把组件更新函数保留在队列中,在同一事件循环中产生的所有数据变更会异步的批量更新。这一策略导致咱们对数据的批改不会立即体现在 DOM 上,此时如果想要获取更新后的 DOM 状态,就须要应用nextTick
  3. 开发时,有两个场景咱们会用到nextTick
  4. created中想要获取 DOM
  • 响应式数据变动后获取 DOM 更新后的状态,比方心愿获取列表更新后的高度
  • nextTick签名如下:function nextTick(callback?: () => void): Promise<void>

所以咱们只须要在传入的回调函数中拜访最新 DOM 状态即可,或者咱们能够 await nextTick() 办法返回的 Promise 之后做这件事

  1. Vue 外部,nextTick之所以可能让咱们看到 DOM 更新后的后果,是因为咱们传入的 callback 会被增加到队列刷新函数 (flushSchedulerQueue) 的前面,这样等队列外部的更新函数都执行结束,所有 DOM 操作也就完结了,callback天然可能获取到最新的 DOM 值

根本应用

const vm = new Vue({
    el: '#app',
    data() {return { a: 1}
    }
}); 

// vm.$nextTick(() => {// [nextTick 回调函数 fn, 外部更新 flushSchedulerQueue]
//     console.log(vm.$el.innerHTML)
// })

// 是将内容保护到一个数组里,最终依照程序程序。第一次会开启一个异步工作

vm.a = 'test'; // 批改了数据后并不会马上更新视图
vm.$nextTick(() => {// [nextTick 回调函数 fn, 外部更新 flushSchedulerQueue]
    console.log(vm.$el.innerHTML)
})

// nextTick 中的办法会被放到 更新页面 watcher 的前面去

相干代码如下

// src/core/utils/nextTick
let callbacks = [];
let pending = false;
function flushCallbacks() {
  pending = false; // 把标记还原为 false
  // 顺次执行回调
  for (let i = 0; i < callbacks.length; i++) {callbacks[i]();}
}
let timerFunc; // 定义异步办法  采纳优雅降级
if (typeof Promise !== "undefined") {
  // 如果反对 promise
  const p = Promise.resolve();
  timerFunc = () => {p.then(flushCallbacks);
  };
} else if (typeof MutationObserver !== "undefined") {
  // MutationObserver 次要是监听 dom 变动 也是一个异步办法
  let counter = 1;
  const observer = new MutationObserver(flushCallbacks);
  const textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {characterData: true,});
  timerFunc = () => {counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
} else if (typeof setImmediate !== "undefined") {
  // 如果后面都不反对 判断 setImmediate
  timerFunc = () => {setImmediate(flushCallbacks);
  };
} else {
  // 最初降级采纳 setTimeout
  timerFunc = () => {setTimeout(flushCallbacks, 0);
  };
}

export function nextTick(cb) {
  // 除了渲染 watcher  还有用户本人手动调用的 nextTick 一起被收集到数组
  callbacks.push(cb);
  if (!pending) {
    // 如果屡次调用 nextTick  只会执行一次异步 等异步队列清空之后再把标记变为 false
    pending = true;
    timerFunc();}
}

数据更新的时候外部会调用nextTick

// src/core/observer/scheduler.js

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {has[id] = true
    if (!flushing) {queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {i--}
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {flushSchedulerQueue()
        return
      }
      // 把更新办法放到数组中保护[nextTick 回调函数, 更新函数 flushSchedulerQueue]
      /**
       * vm.a = 'test'; // 批改了数据后并不会马上更新视图
        vm.$nextTick(() => {// [fn, 更新]
            console.log(vm.$el.innerHTML)
        })
       */
      nextTick(flushSchedulerQueue)
    }
  }
}

Vue 的生命周期办法有哪些 个别在哪一步发申请

beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在以后阶段 data、methods、computed 以及 watch 上的数据和办法都不能被拜访

created 实例曾经创立实现之后被调用。在这一步,实例已实现以下的配置:数据观测(data observer),属性和办法的运算,watch/event 事件回调。这里没有 $el, 如果非要想与 Dom 进行交互,能够通过 vm.$nextTick 来拜访 Dom

beforeMount 在挂载开始之前被调用:相干的 render 函数首次被调用。

mounted 在挂载实现后产生,在以后阶段,实在的 Dom 挂载结束,数据实现双向绑定,能够拜访到 Dom 节点

beforeUpdate 数据更新时调用,产生在虚构 DOM 从新渲染和打补丁(patch)之前。能够在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程

updated 产生在更新实现之后,以后阶段组件 Dom 已实现更新。要留神的是防止在此期间更改数据,因为这可能会导致有限循环的更新,该钩子在服务器端渲染期间不被调用。

beforeDestroy 实例销毁之前调用。在这一步,实例依然齐全可用。咱们能够在这时进行善后收尾工作,比方革除计时器。

destroyed Vue 实例销毁后调用。调用后,Vue 实例批示的所有货色都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。

activated keep-alive 专属,组件被激活时调用

deactivated keep-alive 专属,组件被销毁时调用

异步申请在哪一步发动?

能够在钩子函数 created、beforeMount、mounted 中进行异步申请,因为在这三个钩子函数中,data 曾经创立,能够将服务端端返回的数据进行赋值。

如果异步申请不须要依赖 Dom 举荐在 created 钩子函数中调用异步申请,因为在 created 钩子函数中调用异步申请有以下长处:

  • 能更快获取到服务端数据,缩小页面 loading 工夫;
  • ssr 不反对 beforeMount、mounted 钩子函数,所以放在 created 中有助于一致性;

computed 和 watch 区别

  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  /  watch
const vm = new Vue({
    el: '#app',
    data: {
        firstname:'张',
        lastname:'三'
    },
    computed:{ // watcher  =>   firstname lastname
        // computed 只有取值时才执行

        // Object.defineProperty .get
        fullName(){ // firstName lastName 会收集 fullName 计算属性
            return this.firstname + this.lastname
        }
    },
    watch:{firstname(newVal,oldVal){console.log(newVal)
        }
    }
});

setTimeout(() => {
    debugger;
    vm.firstname = '赵'
}, 1000);

相干源码

// 初始化 state
function initState (vm: Component) {vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {initData(vm)
  } else {observe(vm._data = {}, true /* asRootData */)
  }

  // 初始化计算属性
  if (opts.computed) initComputed(vm, opts.computed) 

  // 初始化 watch
  if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)
  }
}

// 计算属性取值函数
function createComputedGetter (key) {return function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {if (watcher.dirty) { // 如果值依赖的值发生变化,就会进行从新求值
        watcher.evaluate(); // this.firstname lastname}
      if (Dep.target) { // 让计算属性所依赖的属性 收集渲染 watcher
        watcher.depend()}
      return watcher.value
    }
  }
}

// watch 的实现
Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    debugger;
    if (isPlainObject(cb)) {return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options) // 创立 watcher,数据更新调用 cb
    if (options.immediate) {
      try {cb.call(vm, watcher.value)
      } catch (error) {handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    return function unwatchFn () {watcher.teardown()
    }
}

v-show 与 v-if 有什么区别?

v-if 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是 惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。

v-show 就简略得多——不论初始条件是什么,元素总是会被渲染,并且只是简略地基于 CSS 的“display”属性进行切换。

所以,v-if 实用于在运行时很少扭转条件,不须要频繁切换条件的场景;v-show 则实用于须要十分频繁切换条件的场景。

正文完
 0