关于前端:前端一面常见vue面试题汇总

55次阅读

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

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

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 算法,它由 Snabbdo m 批改而来,虚构DOM 要想转化为实在 DOM 就须要通过 patch 办法转换
  2. 最后 Vue1.x 视图中每个依赖均有更新函数对应,能够做到精准更新,因而并不需要虚构 DOMpatching算法反对,然而这样粒度过细导致 Vue1.x 无奈承载较大利用;Vue 2.x中为了升高 Watcher 粒度,每个组件只有一个 Watcher 与之对应,此时就须要引入 patching 算法能力准确找到发生变化的中央并高效更新
  3. vuediff 执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行 render 函数取得最新的虚构 DOM,而后执行patc h 函数,并传入新旧两次虚构 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:155

function 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 各有两个头尾的变量 S tartIdxEndIdx,它们的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 时变成了全局的,能同时触发多个子模块中同名 mutation
store.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-L115
if (!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.ts
export 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.ts
export 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 这个钩子。执行成果依赖 n ext办法的调用参数。能够管制网页的跳转

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)

正文完
 0