说说你对 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

组件通信

组件通信的形式如下:

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

mixin 和 mixins 区别

mixin 用于全局混入,会影响到每个组件实例,通常插件都是这样做初始化的。

Vue.mixin({  beforeCreate() {    // ...逻辑        // 这种形式会影响到每个组件的 beforeCreate 钩子函数  },});

尽管文档不倡议在利用中间接应用 mixin,然而如果不滥用的话也是很有帮忙的,比方能够全局混入封装好的 ajax 或者一些工具函数等等。

mixins 应该是最常应用的扩大组件的形式了。如果多个组件中有雷同的业务逻辑,就能够将这些逻辑剥离进去,通过 mixins 混入代码,比方上拉下拉加载数据这种逻辑等等。
另外须要留神的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并。

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') }

在Vue中应用插件的步骤

  • 采纳ES6import ... from ...语法或CommonJSrequire()办法引入插件
  • 应用全局办法Vue.use( plugin )应用插件,能够传入一个选项对象Vue.use(MyPlugin, { someOption: true })

Vue中diff算法原理

DOM操作是十分低廉的,因而咱们须要尽量地缩小DOM操作。这就须要找出本次DOM必须更新的节点来更新,其余的不更新,这个找出的过程,就须要利用diff算法

vuediff算法是平级比拟,不思考跨级比拟的状况。外部采纳深度递归的形式+双指针(头尾都加指针)的形式进行比拟。

简略来说,Diff算法有以下过程

  • 同级比拟,再比拟子节点(依据keytag标签名判断)
  • 先判断一方有子节点和一方没有子节点的状况(如果新的children没有子节点,将旧的子节点移除)
  • 比拟都有子节点的状况(外围diff)
  • 递归比拟子节点
  • 失常Diff两个树的工夫复杂度是O(n^3),但理论状况下咱们很少会进行跨层级的挪动DOM,所以VueDiff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才须要用外围的Diff算法进行同层级比拟。
  • Vue2的外围Diff算法采纳了双端比拟的算法,同时从新旧children的两端开始进行比拟,借助key值找到可复用的节点,再进行相干操作。相比ReactDiff算法,同样状况下能够缩小挪动节点次数,缩小不必要的性能损耗,更加的优雅
  • 在创立VNode时就确定其类型,以及在mount/patch的过程中采纳位运算来判断一个VNode的类型,在这个根底之上再配合外围的Diff算法,使得性能上较Vue2.x有了晋升

vue3中采纳最长递增子序列来实现diff优化

答复范例

思路

  • diff算法是干什么的
  • 它的必要性
  • 它何时执行
  • 具体执行形式
  • 拔高:说一下vue3中的优化

答复范例

  1. Vue中的diff算法称为patching算法,它由Snabbdom批改而来,虚构DOM要想转化为实在DOM就须要通过patch办法转换
  2. 最后Vue1.x视图中每个依赖均有更新函数对应,能够做到精准更新,因而并不需要虚构DOMpatching算法反对,然而这样粒度过细导致Vue1.x无奈承载较大利用;Vue 2.x中为了升高Watcher粒度,每个组件只有一个Watcher与之对应,此时就须要引入patching算法能力准确找到发生变化的中央并高效更新
  3. vuediff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数取得最新的虚构DOM,而后执行patch函数,并传入新旧两次虚构DOM,通过比对两者找到变动的中央,最初将其转化为对应的DOM操作
  4. patch过程是一个递归过程,遵循深度优先、同层比拟的策略;以vue3patch为例
  5. 首先判断两个节点是否为雷同同类节点,不同则删除从新创立
  6. 如果单方都是文本则更新文本内容
  7. 如果单方都是元素节点则递归更新子元素,同时更新元素属性
  8. 更新子节点时又分了几种状况

    • 新的子节点是文本,老的子节点是数组则清空,并设置文本;
    • 新的子节点是文本,老的子节点是文本则间接更新文本;
    • 新的子节点是数组,老的子节点是文本则清空文本,并创立新子节点数组中的子元素;
    • 新的子节点是数组,老的子节点也是数组,那么比拟两组子节点,更新细节blabla
  9. vue3中引入的更新策略:动态节点标记等

vdom中diff算法的繁难实现

以下代码只是帮忙大家了解diff算法的原理和流程

  1. vdom转化为实在dom
const createElement = (vnode) => {  let tag = vnode.tag;  let attrs = vnode.attrs || {};  let children = vnode.children || [];  if(!tag) {    return null;  }  //创立元素  let elem = document.createElement(tag);  //属性  let attrName;  for (attrName in attrs) {    if(attrs.hasOwnProperty(attrName)) {      elem.setAttribute(attrName, attrs[attrName]);    }  }  //子元素  children.forEach(childVnode => {    //给elem增加子元素    elem.appendChild(createElement(childVnode));  })  //返回实在的dom元素  return elem;}
  1. 用繁难diff算法做更新操作
function updateChildren(vnode, newVnode) {  let children = vnode.children || [];  let newChildren = newVnode.children || [];  children.forEach((childVnode, index) => {    let newChildVNode = newChildren[index];    if(childVnode.tag === newChildVNode.tag) {      //深层次比照, 递归过程      updateChildren(childVnode, newChildVNode);    } else {      //替换      replaceNode(childVnode, newChildVNode);    }  })}

</details>

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

v-model实现原理

咱们在 vue 我的项目中次要应用 v-model 指令在表单 inputtextareaselect 等元素上创立双向数据绑定,咱们晓得 v-model 实质上不过是语法糖(能够看成是value + input办法的语法糖),v-model 在外部为不同的输出元素应用不同的属性并抛出不同的事件:
  • texttextarea 元素应用 value 属性和 input 事件
  • checkboxradio 应用 checked 属性和 change 事件
  • select 字段将 value 作为 prop 并将 change 作为事件

所以咱们能够v-model进行如下改写:

<input v-model="sth" /><!-- 等同于 --><input :value="sth" @input="sth = $event.target.value" />
当在input元素中应用v-model实现双数据绑定,其实就是在输出的时候触发元素的input事件,通过这个语法糖,实现了数据的双向绑定
  • 这个语法糖必须是固定的,也就是说属性必须为value,办法名必须为:input
  • 晓得了v-model的原理,咱们能够在自定义组件上实现v-model
//Parent<template>  {{num}}  <Child v-model="num"></template>export default {  data(){    return {      num: 0    }  }}//Child<template>  <div @click="add">Add</div></template>export default {  props: ['value'], // 属性必须为value  methods:{    add(){      // 办法名为input      this.$emit('input', this.value + 1)    }  }}

原理

会将组件的 v-model 默认转化成value+input

const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>'); // 察看输入的渲染函数:// with(this) { //     return _c('el-checkbox', { //         model: { //             value: (check), //             callback: function ($$v) { check = $$v }, //             expression: "check" //         } //     }) // }
// 源码地位 core/vdom/create-component.js line:155function transformModel (options, data: any) {     const prop = (options.model && options.model.prop) || 'value'     const event = (options.model && options.model.event) || 'input'     ;(data.attrs || (data.attrs = {}))[prop] = data.model.value     const on = data.on || (data.on = {})     const existing = on[event]     const callback = data.model.callback     if (isDef(existing)) {         if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) {            on[event] = [callback].concat(existing)         }     } else {         on[event] = callback     } }

原生的 v-model,会依据标签的不同生成不同的事件和属性

const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('<input v-model="value"/>');// with(this) { //     return _c('input', { //         directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }], //         domProps: { "value": (value) },//         on: {"input": function ($event) { //             if ($event.target.composing) return;//             value = $event.target.value//         }//         }//     })// }
编译时:不同的标签解析出的内容不一样 platforms/web/compiler/directives/model.js
if (el.component) {     genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime     return false } else if (tag === 'select') {     genSelect(el, value, modifiers) } else if (tag === 'input' && type === 'checkbox') {     genCheckboxModel(el, value, modifiers) } else if (tag === 'input' && type === 'radio') {     genRadioModel(el, value, modifiers) } else if (tag === 'input' || tag === 'textarea') {     genDefaultModel(el, value, modifiers) } else if (!config.isReservedTag(tag)) {     genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime     return false }
运行时:会对元素解决一些对于输入法的问题 platforms/web/runtime/directives/model.js
inserted (el, binding, vnode, oldVnode) {     if (vnode.tag === 'select') { // #6903     if (oldVnode.elm && !oldVnode.elm._vOptions) {         mergeVNodeHook(vnode, 'postpatch', () => {             directive.componentUpdated(el, binding, vnode)         })     } else {         setSelected(el, binding, vnode.context)     }    el._vOptions = [].map.call(el.options, getValue)     } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {         el._vModifiers = binding.modifiers         if (!binding.modifiers.lazy) {             el.addEventListener('compositionstart', onCompositionStart)             el.addEventListener('compositionend', onCompositionEnd)             // Safari < 10.2 & UIWebView doesn't fire compositionend when             // switching focus before confirming composition choice             // this also fixes the issue where some browsers e.g. iOS Chrome            // fires "change" instead of "input" on autocomplete.             el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */             if (isIE9) {                 el.vmodel = true             }        }    }}

请阐明Vue中key的作用和原理,谈谈你对它的了解

  • key是为Vue中的VNode标记的惟一id,在patch过程中通过key能够判断两个虚构节点是否是雷同节点,通过这个key,咱们的diff操作能够更精确、更疾速
  • diff算法的过程中,先会进行新旧节点的首尾穿插比照,当无奈匹配的时候会用新节点的key与旧节点进行比对,而后检出差别
  • 尽量不要采纳索引作为key
  • 如果不加key,那么vue会抉择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug
  • 更精确 :因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 比照中能够防止就地复用的状况。所以会更加精确。
  • 更疾速key的唯一性能够被Map数据结构充分利用,相比于遍历查找的工夫复杂度O(n)Map的工夫复杂度仅仅为O(1),比遍历形式更快。

源码如下:

function createKeyToOldIdx (children, beginIdx, endIdx) {  let i, key  const map = {}  for (i = beginIdx; i <= endIdx; ++i) {    key = children[i].key    if (isDef(key)) map[key] = i  }  return map}

答复范例

剖析

这是一道特地常见的问题,次要考查大家对虚构DOMpatch细节的把握水平,可能反映面试者了解档次

思路剖析:

  • 给出论断,key的作用是用于优化patch性能
  • key的必要性
  • 理论应用形式
  • 总结:可从源码层面形容一下vue如何判断两个节点是否雷同

答复范例:

  1. key的作用次要是为了更高效的更新虚构DOM
  2. vuepatch过程中 判断两个节点是否是雷同节点是key是一个必要条件 ,渲染一组列表时,key往往是惟一标识,所以如果不定义key的话,vue只能认为比拟的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比拟低效,影响性能
  3. 理论应用中在渲染一组列表时key必须设置,而且必须是惟一标识,应该防止应用数组索引作为key,这可能导致一些荫蔽的bugvue中在应用雷同标签元素过渡切换时,也会应用key属性,其目标也是为了让vue能够辨别它们,否则vue只会替换其外部属性而不会触发过渡成果
  4. 从源码中能够晓得,vue判断两个节点是否雷同时次要判断两者的key标签类型(如div)等,因而如果不设置key,它的值就是undefined,则可能永远认为这是两个雷同节点,只能去做更新操作,这造成了大量的dom更新操作,显著是不可取的
如果不应用 keyVue 会应用一种最大限度缩小动静元素并且尽可能的尝试就地批改/复用雷同类型元素的算法。key 是为 Vuevnode 的惟一标记,通过这个 key,咱们的 diff 操作能够更精确、更疾速

diff程能够概括为:oldChnewCh各有两个头尾的变量StartIdxEndIdx,它们的2个变量互相比拟,一共有4种比拟形式。如果4种比拟都没匹配,如果设置了key,就会用key进行比拟,在比拟的过程中,变量会往两头靠,一旦StartIdx>EndIdx表明oldChnewCh至多有一个曾经遍历完了,就会完结比拟,这四种比拟形式就是旧尾新头旧头新尾

相干代码如下

// 判断两个vnode的标签和key是否雷同 如果雷同 就能够认为是同一节点就地复用function isSameVnode(oldVnode, newVnode) {  return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;}// 依据key来创立老的儿子的index映射表  相似 {'a':0,'b':1} 代表key为'a'的节点在第一个地位 key为'b'的节点在第二个地位function makeIndexByKey(children) {  let map = {};  children.forEach((item, index) => {    map[item.key] = index;  });  return map;}// 生成的映射表let map = makeIndexByKey(oldCh);

Vuex中actions和mutations有什么区别

题目剖析

  • mutationsactionsvuex带来的两个独特的概念。老手程序员容易混同,所以面试官喜爱问。
  • 咱们只需记住批改状态只能是mutationsactions只能通过提交mutation批改状态即可

答复范例

  1. 更改 Vuexstore 中的状态的惟一办法是提交 mutationmutation 十分相似于事件:每个 mutation 都有一个字符串的类型 (type)和一个 回调函数 (handler) 。Action 相似于 mutation,不同在于:Action能够蕴含任意异步操作,但它不能批改状态, 须要提交mutation能力变更状态
  2. 开发时,蕴含异步操作或者简单业务组合时应用action;须要间接批改状态则提交mutation。但因为dispatchcommit是两个API,容易引起混同,实际中也会采纳对立应用dispatch action的形式。调用dispatchcommit两个API时简直齐全一样,然而定义两者时却不甚雷同,mutation的回调函数接管参数是state对象。action则是与Store实例具备雷同办法和属性的上下文context对象,因而个别会解构它为{commit, dispatch, state},从而不便编码。另外dispatch会返回Promise实例便于解决外部异步后果
  3. 实现上commit(type)办法相当于调用options.mutations[type](state)dispatch(type)办法相当于调用options.actions[type](store),这样就很容易了解两者应用上的不同了

实现

咱们能够像上面这样简略实现commitdispatch,从而分别两者不同

class Store {    constructor(options) {        this.state = reactive(options.state)        this.options = options    }    commit(type, payload) {        // 传入上下文和参数1都是state对象        this.options.mutations[type].call(this.state, this.state, payload)    }    dispatch(type, payload) {        // 传入上下文和参数1都是store自身        this.options.actions[type].call(this, this, payload)    }}

对Vue SSR的了解

Vue.js 是构建客户端应用程序的框架。默认状况下,能够在浏览器中输入 Vue 组件,进行生成 DOM 和操作 DOM。然而,也能够将同一个组件渲染为服务端的 HTML 字符串,将它们间接发送到浏览器,最初将这些动态标记"激活"为客户端上齐全可交互的应用程序。

SSR也就是服务端渲染,也就是将 Vue 在客户端把标签渲染成 HTML 的工作放在服务端实现,而后再把 html 间接返回给客户端

  • 长处SSR 有着更好的 SEO、并且首屏加载速度更快

    • 因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会期待 Ajax 异步实现后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax获取到的内容;而 SSR 是间接由服务端返回曾经渲染好的页面(数据曾经蕴含在页面中),所以搜索引擎爬取工具能够抓取渲染好的页面
    • 更快的内容达到工夫(首屏加载更快): SPA 会期待所有 Vue 编译后的 js 文件都下载实现后,才开始进行页面的渲染,文件下载等须要肯定的工夫等,所以首屏渲染须要肯定的工夫;SSR 间接由服务端渲染好页面间接返回显示,无需期待下载 js 文件及再去渲染等,所以 SSR 有更快的内容达到工夫
  • 毛病 : 开发条件会受到限制,服务器端渲染只反对 beforeCreatecreated 两个钩子,当咱们须要一些内部扩大库时须要非凡解决,服务端渲染应用程序也须要处于 Node.js 的运行环境。服务器会有更大的负载需要

    • 在 Node.js 中渲染残缺的应用程序,显然会比仅仅提供动态文件的 server 更加大量占用CPU资源 (CPU-intensive - CPU 密集),因而如果你意料在高流量环境 ( high traffic ) 下应用,请筹备相应的服务器负载,并明智地采纳缓存策略

其根本实现原理

  • app.js 作为客户端与服务端的专用入口,导出 Vue 根实例,供客户端 entry 与服务端 entry 应用。客户端 entry 次要作用挂载到 DOM 上,服务端 entry 除了创立和返回实例,还进行路由匹配与数据预获取。
  • webpack 为客服端打包一个 Client Bundle ,为服务端打包一个 Server Bundle
  • 服务器接管申请时,会依据 url,加载相应组件,获取和解析异步数据,创立一个读取 Server BundleBundleRenderer,而后生成 html 发送给客户端。
  • 客户端混合,客户端收到从服务端传来的 DOM 与本人的生成的 DOM 进行比照,把不雷同的 DOM 激活,使其能够可能响应后续变动,这个过程称为客户端激活 。为确保混合胜利,客户端与服务器端须要共享同一套数据。在服务端,能够在渲染之前获取数据,填充到 stroe 里,这样,在客户端挂载到 DOM 之前,能够间接从 store里取数据。首屏的动态数据通过 window.__INITIAL_STATE__发送到客户端
Vue SSR 的实现,次要就是把 Vue 的组件输入成一个残缺 HTML, vue-server-renderer 就是干这事的

Vue SSR须要做的事多点(输入残缺 HTML),除了complier -> vnode,还需如数据获取填充至 HTML、客户端混合(hydration)、缓存等等。相比于其余模板引擎(ejs, jade 等),最终要实现的目标是一样的,性能上可能要差点

怎么实现路由懒加载呢

这是一道应用题。当打包利用时,JavaScript 包会变得十分大,影响页面加载。如果咱们能把不同路由对应的组件宰割成不同的代码块,而后当路由被拜访时才加载对应组件,这样就会更加高效

// 将// import UserDetails from './views/UserDetails'// 替换为const UserDetails = () => import('./views/UserDetails')const router = createRouter({  // ...  routes: [{ path: '/users/:id', component: UserDetails }],})

答复范例

  1. 当打包构建利用时,JavaScript 包会变得十分大,影响页面加载。利用路由懒加载咱们能把不同路由对应的组件宰割成不同的代码块,而后当路由被拜访的时候才加载对应组件,这样会更加高效,是一种优化伎俩
  2. 一般来说,对所有的路由都应用动静导入是个好主见
  3. component选项配置一个返回 Promise 组件的函数就能够定义懒加载路由。例如:{ path: '/users/:id', component: () => import('./views/UserDetails') }
  4. 联合正文 () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue') 能够做webpack代码分块

你感觉vuex有什么毛病

剖析

相较于reduxvuex曾经相当简便好用了。但模块的应用比拟繁琐,对ts反对也不好。

体验

应用模块:用起来比拟繁琐,应用模式也不对立,基本上得不到类型零碎的任何反对

const store = createStore({  modules: {    a: moduleA  }})store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState应用store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的store.getters['a/c'] // -> 有namespaced时要加path,应用模式又和state不一样store.commit('d') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutationstore.commit('a/d') // -> 有namespaced时要加path,配合mapMutations应用感觉也没简化

答复范例

  1. vuex利用响应式,应用起来曾经相当方便快捷了。然而在应用过程中感觉模块化这一块做的过于简单,用的时候容易出错,还要常常查看文档
  2. 比方:拜访state时要带上模块key,内嵌模块的话会很长,不得不配合mapState应用,加不加namespaced区别也很大,gettersmutationsactions这些默认是全局,加上之后必须用字符串类型的path来匹配,应用模式不对立,容易出错;对ts的反对也不敌对,在应用模块时没有代码提醒。
  3. 之前Vue2我的项目中用过vuex-module-decorators的解决方案,尽管类型反对上有所改善,但又要学一套新货色,减少了学习老本。pinia呈现之后应用体验好了很多,Vue3 + pinia会是更好的组合

原理

上面咱们来看看vuexstore.state.x.y这种嵌套的门路是怎么搞进去的

首先是子模块装置过程:父模块状态parentState下面设置了子模块名称moduleName,值为以后模块state对象。放在下面的例子中相当于:store.state['x'] = moduleX.state。此过程是递归的,那么store.state.x.y装置时就是:store.state['x']['y'] = moduleY.state
//源码地位 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115if (!isRoot && !hot) {    // 获取父模块state    const parentState = getNestedState(rootState, path.slice(0, -1))    // 获取子模块名称    const moduleName = path[path.length - 1]    store._withCommit(() => {        // 把子模块state设置到父模块上        parentState[moduleName] = module.state    })}

v-if和v-show区别

  • v-show暗藏则是为该元素增加css--display:nonedom元素仍旧还在。v-if显示暗藏是将dom元素整个增加或删除
  • 编译过程:v-if切换有一个部分编译/卸载的过程,切换过程中适合地销毁和重建外部的事件监听和子组件;v-show只是简略的基于css切换
  • 编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染
  • v-showfalse变为true的时候不会触发组件的生命周期
  • v-iffalse变为true的时候,触发组件的beforeCreatecreatebeforeMountmounted钩子,由true变为false的时候触发组件的beforeDestorydestoryed办法
  • 性能耗费:v-if有更高的切换耗费;v-show有更高的初始渲染耗费

v-show与v-if的应用场景

  • v-ifv-show 都能管制dom元素在页面的显示
  • v-if 相比 v-show 开销更大的(间接操作dom节点减少与删除)
  • 如果须要十分频繁地切换,则应用 v-show 较好
  • 如果在运行时条件很少扭转,则应用 v-if 较好

v-show与v-if原理剖析

  1. v-show原理

不论初始条件是什么,元素总是会被渲染

咱们看一下在vue中是如何实现的

代码很好了解,有transition就执行transition,没有就间接设置display属性

// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.tsexport const vShow: ObjectDirective<VShowElement> = {  beforeMount(el, { value }, { transition }) {    el._vod = el.style.display === 'none' ? '' : el.style.display    if (transition && value) {      transition.beforeEnter(el)    } else {      setDisplay(el, value)    }  },  mounted(el, { value }, { transition }) {    if (transition && value) {      transition.enter(el)    }  },  updated(el, { value, oldValue }, { transition }) {    // ...  },  beforeUnmount(el, { value }) {    setDisplay(el, value)  }}
  1. v-if原理

v-if在实现上比v-show要简单的多,因为还有else else-if 等条件须要解决,这里咱们也只摘抄源码中解决 v-if 的一小部分

返回一个node节点,render函数通过表达式的值来决定是否生成DOM

// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.tsexport const transformIf = createStructuralDirectiveTransform(  /^(if|else|else-if)$/,  (node, dir, context) => {    return processIf(node, dir, context, (ifNode, branch, isRoot) => {      // ...      return () => {        if (isRoot) {          ifNode.codegenNode = createCodegenNodeForBranch(            branch,            key,            context          ) as IfConditionalExpression        } else {          // attach this branch's codegen node to the v-if root.          const parentCondition = getParentCondition(ifNode.codegenNode!)          parentCondition.alternate = createCodegenNodeForBranch(            branch,            key + ifNode.branches.length - 1,            context          )        }      }    })  })

Vue路由的钩子函数

首页能够管制导航跳转,beforeEachafterEach等,个别用于页面title的批改。一些须要登录能力调整页面的重定向性能。
  • beforeEach次要有3个参数tofromnext
  • toroute行将进入的指标路由对象。
  • fromroute以后导航正要来到的路由。
  • nextfunction肯定要调用该办法resolve这个钩子。执行成果依赖next办法的调用参数。能够管制网页的跳转

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

Composition API 与 Options API 有什么不同

剖析

Vue3最重要更新之一就是Composition API,它具备一些列长处,其中不少是针对Options API裸露的一些问题量身打造。是Vue3举荐的写法,因而把握好Composition API利用对把握好Vue3至关重要

What is Composition API?(opens new window)

  • Composition API呈现就是为了解决Options API导致雷同性能代码扩散的景象

体验

Composition API能更好的组织代码,上面用composition api能够提取为useCount(),用于组合、复用

compositon api提供了以下几个函数:

  • setup
  • ref
  • reactive
  • watchEffect
  • watch
  • computed
  • toRefs
  • 生命周期的hooks

答复范例

  1. Composition API是一组API,包含:Reactivity API生命周期钩子依赖注入,使用户能够通过导入函数形式编写vue组件。而Options API则通过申明组件选项的对象模式编写组件
  2. Composition API最次要作用是可能简洁、高效复用逻辑。解决了过来Options APImixins的各种毛病;另外Composition API具备更加麻利的代码组织能力,很多用户喜爱Options API,认为所有货色都有固定地位的选项搁置代码,然而单个组件增长过大之后这反而成为限度,一个逻辑关注点扩散在组件各处,造成代码碎片,保护时须要重复横跳,Composition API则能够将它们无效组织在一起。最初Composition API领有更好的类型推断,对ts反对更敌对,Options API在设计之初并未思考类型推断因素,尽管官网为此做了很多简单的类型体操,确保用户能够在应用Options API时取得类型推断,然而还是没方法用在mixinsprovide/inject
  3. Vue3首推Composition API,然而这会让咱们在代码组织上多花点心理,因而在抉择上,如果咱们我的项目属于中低复杂度的场景,Options API仍是一个好抉择。对于那些大型,高扩大,强保护的我的项目上,Composition API会取得更大收益

可能的诘问

  1. Composition API是否和Options API一起应用?

能够在同一个组件中应用两个script标签,一个应用vue3,一个应用vue2写法,一起应用没有问题

<!-- vue3 --><script setup>  // vue3写法</script><!-- 降级vue2 --><script>  export default {    data() {},    methods: {}  }</script>

子组件能够间接扭转父组件的数据么,阐明起因

这是一个实际知识点,组件化开发过程中有个单项数据流准则,不在子组件中批改父组件是个常识问题

思路

  • 讲讲单项数据流准则,表明为何不能这么做
  • 举几个常见场景的例子说说解决方案
  • 联合实际讲讲如果须要批改父组件状态应该如何做

答复范例

  1. 所有的 prop 都使得其父子之间造成了一个单向上行绑定:父级 prop 的更新会向下流动到子组件中,然而反过来则不行。这样会避免从子组件意外变更父级组件的状态,从而导致你的利用的数据流向难以了解。另外,每次父级组件产生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件外部扭转 prop。如果你这样做了,Vue 会在浏览器控制台中收回正告
const props = defineProps(['foo'])// ❌ 上面行为会被正告, props是只读的!props.foo = 'bar'
  1. 理论开发过程中有两个场景会想要批改一个属性:

这个 prop 用来传递一个初始值;这个子组件接下来心愿将其作为一个本地的 prop 数据来应用。 在这种状况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:

const props = defineProps(['initialCounter'])const counter = ref(props.initialCounter)

这个 prop 以一种原始的值传入且须要进行转换。 在这种状况下,最好应用这个 prop 的值来定义一个计算属性:

const props = defineProps(['size'])// prop变动,计算属性自动更新const normalizedSize = computed(() => props.size.trim().toLowerCase())
  1. 实际中如果的确想要扭转父组件属性应该emit一个事件让父组件去做这个变更。留神尽管咱们不能间接批改一个传入的对象或者数组类型的prop,然而咱们还是可能间接改内嵌的对象或属性

既然Vue通过数据劫持能够精准探测数据变动,为什么还须要虚构DOM进行diff检测差别

  • 响应式数据变动,Vue的确能够在数据变动时,响应式零碎能够立即得悉。然而如果给每个属性都增加watcher用于更新的话,会产生大量的watcher从而升高性能
  • 而且粒度过细也得导致更新不精确的问题,所以vue采纳了组件级的watcher配合diff来检测差别

Vue为什么须要虚构DOM?优缺点有哪些

因为在浏览器中操作 DOM是很低廉的。频繁的操作 DOM,会产生肯定的性能问题。这就是虚构 Dom 的产生起因。Vue2Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 实质就是用一个原生的 JS 对象去形容一个 DOM 节点,是对实在 DOM 的一层形象

长处:

  • 保障性能上限 : 框架的虚构 DOM 须要适配任何下层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;然而比起粗犷的 DOM 操作性能要好很多,因而框架的虚构 DOM 至多能够保障在你不须要手动优化的状况下,仍然能够提供还不错的性能,即保障性能的上限;
  • 无需手动操作 DOM : 咱们不再须要手动去操作 DOM,只须要写好 View-Model 的代码逻辑,框架会依据虚构 DOM 和 数据双向绑定,帮咱们以可预期的形式更新视图,极大进步咱们的开发效率;
  • 跨平台 : 虚构 DOM 实质上是 JavaScript 对象,而 DOM 与平台强相干,相比之下虚构 DOM 能够进行更不便地跨平台操作,例如服务器渲染、weex 开发等等。

毛病:

  • 无奈进行极致优化:尽管虚构 DOM + 正当的优化,足以应答绝大部分利用的性能需求,但在一些性能要求极高的利用中虚构 DOM 无奈进行针对性的极致优化。
  • 首次渲染大量DOM时,因为多了一层虚构 DOM 的计算,会比 innerHTML 插入慢。

虚构 DOM 实现原理?

虚构 DOM 的实现原理次要包含以下 3 局部:

  • JavaScript 对象模仿实在 DOM 树,对实在 DOM 进行形象;
  • diff 算法 — 比拟两棵虚构 DOM 树的差别;
  • pach 算法 — 将两个虚构 DOM 对象的差别利用到真正的 DOM 树。

说说你对虚构 DOM 的了解?答复范例

思路

  • vdom是什么
  • 引入vdom的益处
  • vdom如何生成,又如何成为dom
  • 在后续的diff中的作用

答复范例

  1. 虚构dom顾名思义就是虚构的dom对象,它自身就是一个 JavaScript 对象,只不过它是通过不同的属性去形容一个视图构造
  2. 通过引入vdom咱们能够取得如下益处:
  3. 将实在元素节点形象成 VNode,无效缩小间接操作 dom 次数,从而进步程序性能

    • 间接操作 dom 是有限度的,比方:diffclone 等操作,一个实在元素上有许多的内容,如果间接对其进行 diff 操作,会去额定 diff 一些没有必要的内容;同样的,如果须要进行 clone 那么须要将其全部内容进行复制,这也是没必要的。然而,如果将这些操作转移到 JavaScript 对象上,那么就会变得简略了
    • 操作 dom 是比拟低廉的操作,频繁的dom操作容易引起页面的重绘和回流,然而通过形象 VNode 进行两头解决,能够无效缩小间接操作dom的次数,从而缩小页面重绘和回流
  • 不便实现跨平台

    • 同一 VNode 节点能够渲染成不同平台上的对应的内容,比方:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android)变为对应的控件、能够实现 SSR 、渲染到 WebGL 中等等
    • Vue3 中容许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染
  • vdom如何生成?在vue中咱们经常会为组件编写模板 - template, 这个模板会被编译器 - compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚构dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom

  1. 挂载过程完结后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件从新render,此时就会生成新的vdom,和上一次的渲染后果diff就能失去变动的中央,从而转换为最小量的dom操作,高效更新视图

为什么要用vdom?案例解析

当初有一个场景,实现以下需要:

[      { name: "张三", age: "20", address: "北京"},      { name: "李四", age: "21", address: "武汉"},      { name: "王五", age: "22", address: "杭州"},]

将该数据展现成一个表格,并且轻易批改一个信息,表格也跟着批改。 用jQuery实现如下:

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <meta http-equiv="X-UA-Compatible" content="ie=edge">  <title>Document</title></head><body>  <div id="container"></div>  <button id="btn-change">扭转</button>  <script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>  <script>    const data = [{        name: "张三",        age: "20",        address: "北京"      },      {        name: "李四",        age: "21",        address: "武汉"      },      {        name: "王五",        age: "22",        address: "杭州"      },    ];    //渲染函数    function render(data) {      const $container = $('#container');      $container.html('');      const $table = $('<table>');      // 重绘一次      $table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'));      data.forEach(item => {        //每次进入都重绘        $table.append($(`<tr><td>${item.name}</td><td>${item.age}</td><td>${item.address}</td></tr>`))      })      $container.append($table);    }    $('#btn-change').click(function () {      data[1].age = 30;      data[2].address = '深圳';      render(data);    });  </script></body></html>
  • 这样点击按钮,会有相应的视图变动,然而你审查以下元素,每次改变之后,table标签都得从新创立,也就是说table上面的每一个栏目,不论是数据是否和原来一样,都得从新渲染,这并不是现实中的状况,当其中的一栏数据和原来一样,咱们心愿这一栏不要从新渲染,因为DOM重绘相当耗费浏览器性能。
  • 因而咱们采纳JS对象模仿的办法,将DOM的比对操作放在JS层,缩小浏览器不必要的重绘,提高效率。
  • 当然有人说虚构DOM并不比实在的DOM快,其实也是有情理的。当上述table中的每一条数据都扭转时,显然实在的DOM操作更快,因为虚构DOM还存在jsdiff算法的比对过程。所以,上述性能劣势仅仅实用于大量数据的渲染并且扭转的数据只是一小部分的状况。

如下DOM构造:

<ul id="list">    <li class="item">Item1</li>    <li class="item">Item2</li></ul>

映射成虚构DOM就是这样:

{  tag: "ul",  attrs: {    id: "list"  },  children: [    {      tag: "li",      attrs: { className: "item" },      children: ["Item1"]    }, {      tag: "li",      attrs: { className: "item" },      children: ["Item2"]    }  ]} 

应用snabbdom实现vdom

这是一个繁难的实现vdom性能的库,相比vuereact,对于vdom这块更加繁难,适宜咱们学习vdomvdom外面有两个外围的api,一个是h函数,一个是patch函数,前者用来生成vdom对象,后者的性能在于做虚构dom的比对和将vdom挂载到实在DOM

简略介绍一下这两个函数的用法:

h('标签名', {属性}, [子元素])h('标签名', {属性}, [文本])patch(container, vnode) // container为容器DOM元素patch(vnode, newVnode)

当初咱们就来用snabbdom重写一下方才的例子:

<!DOCTYPE html><html lang="en"><head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <meta http-equiv="X-UA-Compatible" content="ie=edge">  <title>Document</title></head><body>  <div id="container"></div>  <button id="btn-change">扭转</button>  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script>  <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>  <script>    let snabbdom = window.snabbdom;    // 定义patch    let patch = snabbdom.init([      snabbdom_class,      snabbdom_props,      snabbdom_style,      snabbdom_eventlisteners    ]);    //定义h    let h = snabbdom.h;    const data = [{        name: "张三",        age: "20",        address: "北京"      },      {        name: "李四",        age: "21",        address: "武汉"      },      {        name: "王五",        age: "22",        address: "杭州"      },    ];    data.unshift({name: "姓名", age: "年龄", address: "地址"});    let container = document.getElementById('container');    let vnode;    const render = (data) => {      let newVnode = h('table', {}, data.map(item => {         let tds = [];        for(let i in item) {          if(item.hasOwnProperty(i)) {            tds.push(h('td', {}, item[i] + ''));          }        }        return h('tr', {}, tds);      }));      if(vnode) {          patch(vnode, newVnode);      } else {          patch(container, newVnode);      }      vnode = newVnode;    }    render(data);    let btnChnage = document.getElementById('btn-change');    btnChnage.addEventListener('click', function() {      data[1].age = 30;      data[2].address = "深圳";      //re-render      render(data);    })  </script></body></html>

你会发现, 只有扭转的栏目才闪动,也就是进行重绘 ,数据没有扭转的栏目还是放弃原样,这样就大大节俭了浏览器从新渲染的开销

vue中应用h函数生成虚构DOM返回
const vm = new Vue({  el: '#app',  data: {    user: {name:'poetry'}  },  render(h){    // h()    // h(App)    // h('div',[])    let vnode = h('div',{},'hello world');    return vnode  }});

</details>

vue-router中如何爱护路由

剖析

路由爱护在利用开发过程中十分重要,简直每个利用都要做各种路由权限治理,因而相当考查使用者基本功。

体验

全局守卫:

const router = createRouter({ ... })router.beforeEach((to, from) => {  // ...  // 返回 false 以勾销导航  return false})

路由独享守卫:

const routes = [  {    path: '/users/:id',    component: UserDetails,    beforeEnter: (to, from) => {      // reject the navigation      return false    },  },]

组件内的守卫:

const UserDetails = {  template: `...`,  beforeRouteEnter(to, from) {    // 在渲染该组件的对应路由被验证前调用  },  beforeRouteUpdate(to, from) {    // 在以后路由扭转,然而该组件被复用时调用  },  beforeRouteLeave(to, from) {    // 在导航来到渲染该组件的对应路由时调用  },}

答复

  • vue-router中爱护路由的办法叫做路由守卫,次要用来通过跳转或勾销的形式守卫导航。
  • 路由守卫有三个级别:全局路由独享组件级。影响范畴由大到小,例如全局的router.beforeEach(),能够注册一个全局前置守卫,每次路由导航都会通过这个守卫,因而在其外部能够退出管制逻辑决定用户是否能够导航到指标路由;在路由注册的时候能够退出单路由独享的守卫,例如beforeEnter,守卫只在进入路由时触发,因而只会影响这个路由,管制更准确;咱们还能够为路由组件增加守卫配置,例如beforeRouteEnter,会在渲染该组件的对应路由被验证前调用,管制的范畴更准确了。
  • 用户的任何导航行为都会走navigate办法,外部有个guards队列按程序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会勾销原有的导航。

原理

runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则持续下一个级别的守卫,不通过进入catch流程勾销本来导航

// 源码runGuardQueue(guards)  .then(() => {    // check global guards beforeEach    guards = []    for (const guard of beforeGuards.list()) {      guards.push(guardToPromiseFn(guard, to, from))    }    guards.push(canceledNavigationCheck)    return runGuardQueue(guards)  })  .then(() => {    // check in components beforeRouteUpdate    guards = extractComponentsGuards(      updatingRecords,      'beforeRouteUpdate',      to,      from    )    for (const record of updatingRecords) {      record.updateGuards.forEach(guard => {        guards.push(guardToPromiseFn(guard, to, from))      })    }    guards.push(canceledNavigationCheck)    // run the queue of per route beforeEnter guards    return runGuardQueue(guards)  })  .then(() => {    // check the route beforeEnter    guards = []    for (const record of to.matched) {      // do not trigger beforeEnter on reused views      if (record.beforeEnter && !from.matched.includes(record)) {        if (isArray(record.beforeEnter)) {          for (const beforeEnter of record.beforeEnter)            guards.push(guardToPromiseFn(beforeEnter, to, from))        } else {          guards.push(guardToPromiseFn(record.beforeEnter, to, from))        }      }    }    guards.push(canceledNavigationCheck)    // run the queue of per route beforeEnter guards    return runGuardQueue(guards)  })  .then(() => {    // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>    // clear existing enterCallbacks, these are added by extractComponentsGuards    to.matched.forEach(record => (record.enterCallbacks = {}))    // check in-component beforeRouteEnter    guards = extractComponentsGuards(      enteringRecords,      'beforeRouteEnter',      to,      from    )    guards.push(canceledNavigationCheck)    // run the queue of per route beforeEnter guards    return runGuardQueue(guards)  })  .then(() => {    // check global guards beforeResolve    guards = []    for (const guard of beforeResolveGuards.list()) {      guards.push(guardToPromiseFn(guard, to, from))    }    guards.push(canceledNavigationCheck)    return runGuardQueue(guards)  })  // catch any navigation canceled  .catch(err =>    isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)      ? err      : Promise.reject(err)  )

源码地位(opens new window)