共计 34560 个字符,预计需要花费 87 分钟才能阅读完成。
说说你对 proxy 的了解,Proxy 相比于 defineProperty 的劣势
Object.defineProperty()
的问题次要有三个:
- 不能监听数组的变动:无奈监控到数组下标的变动,导致通过数组下标增加元素,不能实时响应
- 必须遍历对象的每个属性:只能劫持对象的属性,从而须要对每个对象,每个属性进行遍历,如果属性值是对象,还须要深度遍历。
Proxy
能够劫持整个对象,并返回一个新的对象 - 必须深层遍历嵌套的对象
Proxy 的劣势如下:
- 针对对象:针对整个对象,而不是对象的某个属性,所以也就不须要对
keys
进行遍历 - 反对数组:
Proxy
不须要对数组的办法进行重载,省去了泛滥 hack,缩小代码量等于缩小了保护老本,而且规范的就是最好的 Proxy
的第二个参数能够有13
种拦挡方:不限于apply
、ownKeys
、deleteProperty
、has
等等是Object.defineProperty
不具备的Proxy
返回的是一个新对象, 咱们能够只操作新的对象达到目标, 而Object.defineProperty
只能遍历对象属性间接批改Proxy
作为新规范将受到浏览器厂商重点继续的性能优化,也就是传说中的新规范的性能红利
proxy 具体应用点击查看(opens new window)
Object.defineProperty 的劣势如下:
兼容性好,反对
IE9
,而Proxy
的存在浏览器兼容性问题, 而且无奈用polyfill
磨平
defineProperty 的属性值有哪些
Object.defineProperty(obj, prop, descriptor)
// obj 要定义属性的对象
// prop 要定义或批改的属性的名称
// descriptor 要定义或批改的属性描述符
Object.defineProperty(obj,"name",{
value:"poetry", // 初始值
writable:true, // 该属性是否可写入
enumerable:true, // 该属性是否可被遍历失去(for...in,Object.keys 等)configurable:true, // 定该属性是否可被删除,且除 writable 外的其余描述符是否可被批改
get: function() {},
set: function(newVal) {}})
相干代码如下
import {mutableHandlers} from "./baseHandlers"; // 代理相干逻辑
import {isObject} from "./util"; // 工具办法
export function reactive(target) {
// 依据不同参数创立不同响应式对象
return createReactiveObject(target, mutableHandlers);
}
function createReactiveObject(target, baseHandler) {if (!isObject(target)) {return target;}
const observed = new Proxy(target, baseHandler);
return observed;
}
const get = createGetter();
const set = createSetter();
function createGetter() {return function get(target, key, receiver) {
// 对获取的值进行喷射
const res = Reflect.get(target, key, receiver);
console.log("属性获取", key);
if (isObject(res)) {
// 如果获取的值是对象类型,则返回以后对象的代理对象
return reactive(res);
}
return res;
};
}
function createSetter() {return function set(target, key, value, receiver) {const oldValue = target[key];
const hadKey = hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
if (!hadKey) {console.log("属性新增", key, value);
} else if (hasChanged(value, oldValue)) {console.log("属性值被批改", key, value);
}
return result;
};
}
export const mutableHandlers = {
get, // 当获取属性时调用此办法
set, // 当批改属性时调用此办法
};
Proxy
只会代理对象的第一层,那么 Vue3
又是怎么解决这个问题的呢?
判断以后
Reflect.get 的
返回值是否为Object
,如果是则再通过reactive
办法做代理,这样就实现了深度观测。
监测数组的时候可能触发屡次 get/set,那么如何避免触发屡次呢?
咱们能够判断
key
是否为以后被代理对象target
本身属性,也能够判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger
组件通信
组件通信的形式如下:
(1)props / $emit
父组件通过 props
向子组件传递数据,子组件通过 $emit
和父组件通信
1. 父组件向子组件传值
props
只能是父组件向子组件进行传值,props
使得父子组件之间造成了一个单向上行绑定。子组件的数据会随着父组件不断更新。props
能够显示定义一个或一个以上的数据,对于接管的数据,能够是各种数据类型,同样也能够传递一个函数。props
属性名规定:若在props
中应用驼峰模式,模板中须要应用短横线的模式
// 父组件
<template>
<div id="father">
<son :msg="msgData" :fn="myFunction"></son>
</div>
</template>
<script>
import son from "./son.vue";
export default {
name: father,
data() {msgData: "父组件数据";},
methods: {myFunction() {console.log("vue");
},
},
components: {son},
};
</script>
// 子组件
<template>
<div id="son">
<p>{{msg}}</p>
<button @click="fn"> 按钮 </button>
</div>
</template>
<script>
export default {name: "son", props: ["msg", "fn"] };
</script>
2. 子组件向父组件传值
$emit
绑定一个自定义事件,当这个事件被执行的时就会将参数传递给父组件,而父组件通过v-on
监听并接管参数。
// 父组件
<template>
<div class="section">
<com-article
:articles="articleList"
@onEmitIndex="onEmitIndex"
></com-article>
<p>{{currentIndex}}</p>
</div>
</template>
<script>
import comArticle from "./test/article.vue";
export default {
name: "comArticle",
components: {comArticle},
data() {return { currentIndex: -1, articleList: ["红楼梦", "西游记", "三国演义"] };
},
methods: {onEmitIndex(idx) {this.currentIndex = idx;},
},
};
</script>
// 子组件
<template>
<div>
<div
v-for="(item, index) in articles"
:key="index"
@click="emitIndex(index)"
>
{{item}}
</div>
</div>
</template>
<script>
export default {props: ["articles"],
methods: {emitIndex(index) {this.$emit("onEmitIndex", index); // 触发父组件的办法,并传递参数 index
},
},
};
</script>
(2)eventBus 事件总线($emit / $on
)
eventBus
事件总线实用于 父子组件 、 非父子组件 等之间的通信,应用步骤如下:(1)创立事件核心治理组件之间的通信
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
(2)发送事件 假如有两个兄弟组件firstCom
和secondCom
:
<template>
<div>
<first-com></first-com>
<second-com></second-com>
</div>
</template>
<script>
import firstCom from "./firstCom.vue";
import secondCom from "./secondCom.vue";
export default {components: { firstCom, secondCom} };
</script>
在 firstCom
组件中发送事件:
<template>
<div>
<button @click="add"> 加法 </button>
</div>
</template>
<script>
import {EventBus} from "./event-bus.js"; // 引入事件核心
export default {data() {return { num: 0};
},
methods: {add() {EventBus.$emit("addition", { num: this.num++});
},
},
};
</script>
(3)接管事件 在secondCom
组件中发送事件:
<template>
<div> 求和: {{count}}</div>
</template>
<script>
import {EventBus} from "./event-bus.js";
export default {data() {return { count: 0};
},
mounted() {EventBus.$on("addition", (param) => {this.count = this.count + param.num;});
},
};
</script>
在上述代码中,这就相当于将 num
值存贮在了事件总线中,在其余组件中能够间接拜访。事件总线就相当于一个桥梁,不必组件通过它来通信。
尽管看起来比较简单,然而这种办法也有不变之处,如果我的项目过大,应用这种形式进行通信,前期保护起来会很艰难。
(3)依赖注入(provide / inject)
这种形式就是 Vue 中的 依赖注入 ,该办法用于 父子组件之间的通信。当然这里所说的父子不肯定是真正的父子,也能够是祖孙组件,在层数很深的状况下,能够应用这种办法来进行传值。就不必一层一层的传递了。
provide / inject
是 Vue 提供的两个钩子,和 data
、methods
是同级的。并且 provide
的书写模式和 data
一样。
provide
钩子用来发送数据或办法inject
钩子用来接收数据或办法
在父组件中:
provide() {
return {num: this.num};
}
在子组件中:
inject: ['num']
还能够这样写,这样写就能够拜访父组件中的所有属性:
provide() {
return {app: this};
}
data() {
return {num: 1};
}
inject: ['app']
console.log(this.app.num)
留神: 依赖注入所提供的属性是 非响应式 的。
(3)ref / $refs
这种形式也是实现 父子组件 之间的通信。
ref
:这个属性用在子组件上,它的援用就指向了子组件的实例。能够通过实例来拜访组件的数据和办法。
在子组件中:
export default {data () {
return {name: 'JavaScript'}
},
methods: {sayHello () {console.log('hello')
}
}
}
在父组件中:
<template>
<child ref="child"></component-a>
</template>
<script>
import child from "./child.vue";
export default {components: { child},
mounted() {console.log(this.$refs.child.name); // JavaScript
this.$refs.child.sayHello(); // hello},
};
</script>
(4)$parent / $children
- 应用
$parent
能够让组件拜访父组件的实例(拜访的是上一级父组件的属性和办法) - 应用
$children
能够让组件拜访子组件的实例,然而,$children
并不能保障程序,并且拜访的数据也不是响应式的。
在子组件中:
<template>
<div>
<span>{{message}}</span>
<p> 获取父组件的值为: {{parentVal}}</p>
</div>
</template>
<script>
export default {data() {return { message: "Vue"};
},
computed: {parentVal() {return this.$parent.msg;},
},
};
</script>
在父组件中:
// 父组件中
<template>
<div class="hello_world">
<div>{{msg}}</div>
<child></child>
<button @click="change"> 点击扭转子组件值 </button>
</div>
</template>
<script>
import child from "./child.vue";
export default {components: { child},
data() {return { msg: "Welcome"};
},
methods: {change() {
// 获取到子组件
this.$children[0].message = "JavaScript";
},
},
};
</script>
在下面的代码中,子组件获取到了父组件的 parentVal
值,父组件扭转了子组件中 message
的值。须要留神:
- 通过
$parent
拜访到的是上一级父组件的实例,能够应用$root
来拜访根组件的实例 - 在组件中应用
$children
拿到的是所有的子组件的实例,它是一个数组,并且是无序的 - 在根组件
#app
上拿$parent
失去的是new Vue()
的实例,在这实例上再拿$parent
失去的是undefined
,而在最底层的子组件拿$children
是个空数组 $children
的值是 数组 ,而$parent
是个 对象
(5)$attrs / $listeners
思考一种场景,如果 A 是 B 组件的父组件,B 是 C 组件的父组件。如果想要组件 A 给组件 C 传递数据,这种隔代的数据,该应用哪种形式呢?
如果是用 props/$emit
来一级一级的传递,的确能够实现,然而比较复杂;如果应用事件总线,在多人开发或者我的项目较大的时候,保护起来很麻烦;如果应用 Vuex,确实也能够,然而如果仅仅是传递数据,那可能就有点节约了。
针对上述情况,Vue 引入了$attrs / $listeners
,实现组件之间的跨代通信。
先来看一下 inheritAttrs
,它的默认值 true,继承所有的父组件属性除props
之外的所有属性;inheritAttrs:false
只继承 class 属性。
$attrs
:继承所有的父组件属性(除了 prop 传递的属性、class 和 style),个别用在子组件的子元素上$listeners
:该属性是一个对象,外面蕴含了作用在这个组件上的所有监听器,能够配合v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)
A 组件(APP.vue
):
<template>
<div id="app">
// 此处监听了两个事件,能够在 B 组件或者 C 组件中间接触发
<child1
:p-child1="child1"
:p-child2="child2"
@test1="onTest1"
@test2="onTest2"
></child1>
</div>
</template>
<script>
import Child1 from "./Child1.vue";
export default {components: { Child1},
methods: {onTest1() {console.log("test1 running");
},
onTest2() {console.log("test2 running");
},
},
};
</script>
B 组件(Child1.vue
):
<template>
<div class="child-1">
<p>props: {{pChild1}}</p>
<p>$attrs: {{$attrs}}</p>
<child2 v-bind="$attrs" v-on="$listeners"></child2>
</div>
</template>
<script>
import Child2 from "./Child2.vue";
export default {props: ["pChild1"],
components: {Child2},
inheritAttrs: false,
mounted() {this.$emit("test1"); // 触发 APP.vue 中的 test1 办法
},
};
</script>
C 组件 (Child2.vue
):
<template>
<div class="child-2">
<p>props: {{pChild2}}</p>
<p>$attrs: {{$attrs}}</p>
</div>
</template>
<script>
export default {props: ["pChild2"],
inheritAttrs: false,
mounted() {this.$emit("test2"); // 触发 APP.vue 中的 test2 办法
},
};
</script>
在上述代码中:
- C 组件中能间接触发 test 的起因在于 B 组件调用 C 组件时 应用 v-on 绑定了
$listeners
属性 - 在 B 组件中通过 v -bind 绑定
$attrs
属性,C 组件能够间接获取到 A 组件中传递下来的 props(除了 B 组件中 props 申明的)
(6)总结
(1)父子组件间通信
- 子组件通过 props 属性来承受父组件的数据,而后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
- 通过 ref 属性给子组件设置一个名字。父组件通过
$refs
组件名来取得子组件,子组件通过$parent
取得父组件,这样也能够实现通信。 - 应用 provide/inject,在父组件中通过 provide 提供变量,在子组件中通过 inject 来将变量注入到组件中。不管子组件有多深,只有调用了 inject 那么就能够注入 provide 中的数据。
(2)兄弟组件间通信
- 应用 eventBus 的办法,它的实质是通过创立一个空的 Vue 实例来作为消息传递的对象,通信的组件引入这个实例,通信的组件通过在这个实例上监听和触发事件,来实现音讯的传递。
- 通过
$parent/$refs
来获取到兄弟组件,也能够进行通信。
(3)任意组件之间
- 应用 eventBus,其实就是创立一个事件核心,相当于中转站,能够用它来传递事件和接管事件。
如果业务逻辑简单,很多组件之间须要同时解决一些公共的数据,这个时候采纳下面这一些办法可能不利于我的项目的保护。这个时候能够应用 vuex,vuex 的思维就是将这一些公共的数据抽离进去,将它作为一个全局的变量来治理,而后其余组件就能够对这个公共数据进行读写操作,这样达到理解耦的目标。
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 中应用插件的步骤
- 采纳
ES6
的import ... from ...
语法或CommonJS
的require()
办法引入插件 - 应用全局办法
Vue.use(plugin)
应用插件, 能够传入一个选项对象Vue.use(MyPlugin, { someOption: true})
Vue 中 diff 算法原理
DOM
操作是十分低廉的,因而咱们须要尽量地缩小 DOM
操作。这就须要找出本次 DOM
必须更新的节点来更新,其余的不更新,这个找出的过程,就须要利用 diff 算法
vue
的diff
算法是平级比拟,不思考跨级比拟的状况。外部采纳深度递归的形式 + 双指针 (头尾都加指针)
的形式进行比拟。
简略来说,Diff 算法有以下过程
- 同级比拟,再比拟子节点(依据
key
和tag
标签名判断) - 先判断一方有子节点和一方没有子节点的状况 (如果新的
children
没有子节点,将旧的子节点移除) - 比拟都有子节点的状况(外围
diff
) - 递归比拟子节点
- 失常
Diff
两个树的工夫复杂度是O(n^3)
,但理论状况下咱们很少会进行跨层级的挪动DOM
,所以Vue
将Diff
进行了优化,从O(n^3) -> O(n)
,只有当新旧children
都为多个子节点时才须要用外围的Diff
算法进行同层级比拟。 Vue2
的外围Diff
算法采纳了双端比拟
的算法,同时从新旧children
的两端开始进行比拟,借助key
值找到可复用的节点,再进行相干操作。相比React
的Diff
算法,同样状况下能够缩小挪动节点次数,缩小不必要的性能损耗,更加的优雅- 在创立
VNode
时就确定其类型,以及在mount/patch
的过程中采纳位运算来判断一个VNode
的类型,在这个根底之上再配合外围的Diff
算法,使得性能上较Vue2.x
有了晋升
vue3 中采纳最长递增子序列来实现
diff
优化
答复范例
思路
diff
算法是干什么的- 它的必要性
- 它何时执行
- 具体执行形式
- 拔高:说一下
vue3
中的优化
答复范例
Vue
中的diff
算法称为patching
算法,它由Snabbdo
m 批改而来,虚构DOM
要想转化为实在DOM
就须要通过patch
办法转换- 最后
Vue1.x
视图中每个依赖均有更新函数对应,能够做到精准更新,因而并不需要虚构DOM
和patching
算法反对,然而这样粒度过细导致Vue1.x
无奈承载较大利用;Vue 2.x
中为了升高Watcher
粒度,每个组件只有一个Watcher
与之对应,此时就须要引入patching
算法能力准确找到发生变化的中央并高效更新 vue
中diff
执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render
函数取得最新的虚构DOM
,而后执行patc
h 函数,并传入新旧两次虚构 DOM,通过比对两者找到变动的中央,最初将其转化为对应的DOM
操作patch
过程是一个递归过程,遵循深度优先、同层比拟的策略;以vue3
的patch
为例- 首先判断两个节点是否为雷同同类节点,不同则删除从新创立
- 如果单方都是文本则更新文本内容
- 如果单方都是元素节点则递归更新子元素,同时更新元素属性
-
更新子节点时又分了几种状况
- 新的子节点是文本,老的子节点是数组则清空,并设置文本;
- 新的子节点是文本,老的子节点是文本则间接更新文本;
- 新的子节点是数组,老的子节点是文本则清空文本,并创立新子节点数组中的子元素;
- 新的子节点是数组,老的子节点也是数组,那么比拟两组子节点,更新细节 blabla
vue3
中引入的更新策略:动态节点标记等
vdom 中 diff 算法的繁难实现
以下代码只是帮忙大家了解 diff
算法的原理和流程
- 将
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;
}
- 用繁难
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
指令在表单input
、textarea
、select
等元素上创立双向数据绑定,咱们晓得v-model
实质上不过是语法糖(能够看成是value + input
办法的语法糖),v-model
在外部为不同的输出元素应用不同的属性并抛出不同的事件:
text
和textarea
元素应用value
属性和input
事件checkbox
和radio
应用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
}
答复范例
剖析
这是一道特地常见的问题,次要考查大家对虚构 DOM
和patch
细节的把握水平,可能反映面试者了解档次
思路剖析:
- 给出论断,
key
的作用是用于优化patch
性能 key
的必要性- 理论应用形式
- 总结:可从源码层面形容一下
vue
如何判断两个节点是否雷同
答复范例:
key
的作用次要是为了更高效的更新虚构DOM
vue
在patch
过程中 判断两个节点是否是雷同节点是key
是一个必要条件 ,渲染一组列表时,key
往往是惟一标识,所以如果不定义key
的话,vue
只能认为比拟的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch
过程比拟低效,影响性能- 理论应用中在渲染一组列表时
key
必须设置,而且必须是惟一标识,应该防止应用数组索引作为key
,这可能导致一些荫蔽的bug
;vue
中在应用雷同标签元素过渡切换时,也会应用key
属性,其目标也是为了让vue
能够辨别它们,否则vue
只会替换其外部属性而不会触发过渡成果 - 从源码中能够晓得,
vue
判断两个节点是否雷同时次要判断两者的key
和标签类型 (如 div)
等,因而如果不设置key
,它的值就是undefined
,则可能永远认为这是两个雷同节点,只能去做更新操作,这造成了大量的dom
更新操作,显著是不可取的
如果不应用
key
,Vue
会应用一种最大限度缩小动静元素并且尽可能的尝试就地批改 / 复用雷同类型元素的算法。key
是为Vue
中vnode
的惟一标记,通过这个key
,咱们的diff
操作能够更精确、更疾速
diff 程能够概括为:
oldCh
和newCh
各有两个头尾的变量 StartIdx
和EndIdx
,它们的2
个变量互相比拟,一共有4
种比拟形式。如果4
种比拟都没匹配,如果设置了key
,就会用key
进行比拟,在比拟的过程中,变量会往两头靠,一旦StartIdx>EndIdx
表明oldCh
和newCh
至多有一个曾经遍历完了,就会完结比拟, 这四种比拟形式就是首
、尾
、旧尾新头
、旧头新尾
相干代码如下
// 判断两个 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 有什么区别
题目剖析
mutations
和actions
是vuex
带来的两个独特的概念。老手程序员容易混同,所以面试官喜爱问。- 咱们只需记住批改状态只能是
mutations
,actions
只能通过提交mutation
批改状态即可
答复范例
- 更改
Vuex
的store
中的状态的惟一办法是提交mutation
,mutation
十分相似于事件:每个mutation
都有一个字符串的类型 (type
)和一个 回调函数 (handler
)。Action
相似于mutation
,不同在于:Action
能够蕴含任意异步操作,但它不能批改状态,须要提交mutation
能力变更状态 - 开发时,蕴含异步操作或者简单业务组合时应用
action
;须要间接批改状态则提交mutation
。但因为dispatch
和commit
是两个API
,容易引起混同,实际中也会采纳对立应用dispatch action
的形式。调用dispatch
和commit
两个API
时简直齐全一样,然而定义两者时却不甚雷同,mutation
的回调函数接管参数是state
对象。action
则是与Store
实例具备雷同办法和属性的上下文context
对象,因而个别会解构它为{commit, dispatch, state}
,从而不便编码。另外dispatch
会返回Promise
实例便于解决外部异步后果 - 实现上
commit(type)
办法相当于调用options.mutations[type](state)
;dispatch(type)
办法相当于调用options.actions[type](store)
,这样就很容易了解两者应用上的不同了
实现
咱们能够像上面这样简略实现 commit
和dispatch
,从而分别两者不同
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 有更快的内容达到工夫
- 因为
-
毛病:开发条件会受到限制,服务器端渲染只反对
beforeCreate
和created
两个钩子,当咱们须要一些内部扩大库时须要非凡解决,服务端渲染应用程序也须要处于Node.js
的运行环境。服务器会有更大的负载需要- 在 Node.js 中渲染残缺的应用程序,显然会比仅仅提供动态文件的
server
更加大量占用CPU
资源 (CPU-intensive – CPU 密集),因而如果你意料在高流量环境 (high traffic) 下应用,请筹备相应的服务器负载,并明智地采纳缓存策略
- 在 Node.js 中渲染残缺的应用程序,显然会比仅仅提供动态文件的
其根本实现原理
app.js
作为客户端与服务端的专用入口,导出Vue
根实例,供客户端entry
与服务端entry
应用。客户端entry
次要作用挂载到DOM
上,服务端entry
除了创立和返回实例,还进行路由匹配与数据预获取。webpack
为客服端打包一个Client Bundle
,为服务端打包一个Server Bundle
。- 服务器接管申请时,会依据
url
,加载相应组件,获取和解析异步数据,创立一个读取Server Bundle
的BundleRenderer
,而后生成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}],
})
答复范例
- 当打包构建利用时,JavaScript 包会变得十分大,影响页面加载。利用路由懒加载咱们能把不同路由对应的组件宰割成不同的代码块,而后当路由被拜访的时候才加载对应组件,这样会更加高效,是一种优化伎俩
- 一般来说,对所有的 路由都应用动静导入 是个好主见
- 给
component
选项配置一个返回Promise
组件的函数就能够定义懒加载路由。例如:{path: '/users/:id', component: () => import('./views/UserDetails') }
- 联合正文
() => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
能够做webpack
代码分块
你感觉 vuex 有什么毛病
剖析
相较于 redux
,vuex
曾经相当简便好用了。但模块的应用比拟繁琐,对 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 应用感觉也没简化
答复范例
vuex
利用响应式,应用起来曾经相当方便快捷了。然而在应用过程中感觉模块化这一块做的过于简单,用的时候容易出错,还要常常查看文档- 比方:拜访
state
时要带上模块key
,内嵌模块的话会很长,不得不配合mapState
应用,加不加namespaced
区别也很大,getters
,mutations
,actions
这些默认是全局,加上之后必须用字符串类型的 path 来匹配,应用模式不对立,容易出错;对 ts 的反对也不敌对,在应用模块时没有代码提醒。 - 之前
Vue2
我的项目中用过vuex-module-decorators
的解决方案,尽管类型反对上有所改善,但又要学一套新货色,减少了学习老本。pinia
呈现之后应用体验好了很多,Vue3 + pinia
会是更好的组合
原理
上面咱们来看看 vuex
中store.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:none
,dom
元素仍旧还在。v-if
显示暗藏是将dom
元素整个增加或删除- 编译过程:
v-if
切换有一个部分编译 / 卸载的过程,切换过程中适合地销毁和重建外部的事件监听和子组件;v-show
只是简略的基于css
切换 - 编译条件:
v-if
是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染 v-show
由false
变为true
的时候不会触发组件的生命周期v-if
由false
变为true
的时候,触发组件的beforeCreate
、create
、beforeMount
、mounted
钩子,由true
变为false
的时候触发组件的beforeDestory
、destoryed
办法- 性能耗费:
v-if
有更高的切换耗费;v-show
有更高的初始渲染耗费
v-show 与 v -if 的应用场景
v-if
与v-show
都能管制dom
元素在页面的显示v-if
相比v-show
开销更大的(间接操作dom 节
点减少与删除)- 如果须要十分频繁地切换,则应用 v-show 较好
- 如果在运行时条件很少扭转,则应用
v-if
较好
v-show 与 v -if 原理剖析
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)
}
}
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 路由的钩子函数
首页能够管制导航跳转,
beforeEach
,afterEach
等,个别用于页面title
的批改。一些须要登录能力调整页面的重定向性能。
beforeEach
次要有 3 个参数to
,from
,next
。to
:route
行将进入的指标路由对象。from
:route
以后导航正要来到的路由。next
:function
肯定要调用该办法resolve
这个钩子。执行成果依赖 next
办法的调用参数。能够管制网页的跳转
vue-router 路由钩子函数是什么 执行程序是什么
路由钩子的执行流程, 钩子函数品种有:
全局守卫
、路由守卫
、组件守卫
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫 (2.2+
)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫 (2.5+
)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发
DOM
更新。 - 调用
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
答复范例
Composition API
是一组API
,包含:Reactivity API
、生命周期钩子
、依赖注入
,使用户能够通过导入函数形式编写vue
组件。而Options API
则通过申明组件选项的对象模式编写组件Composition API
最次要作用是可能简洁、高效复用逻辑。解决了过来Options API
中mixins
的各种毛病;另外Composition API
具备更加麻利的代码组织能力,很多用户喜爱Options API
,认为所有货色都有固定地位的选项搁置代码,然而单个组件增长过大之后这反而成为限度,一个逻辑关注点扩散在组件各处,造成代码碎片,保护时须要重复横跳,Composition API
则能够将它们无效组织在一起。最初Composition API
领有更好的类型推断,对 ts 反对更敌对,Options API
在设计之初并未思考类型推断因素,尽管官网为此做了很多简单的类型体操,确保用户能够在应用Options API
时取得类型推断,然而还是没方法用在mixins
和provide/inject
上Vue3
首推Composition API
,然而这会让咱们在代码组织上多花点心理,因而在抉择上,如果咱们我的项目属于中低复杂度的场景,Options API
仍是一个好抉择。对于那些大型,高扩大,强保护的我的项目上,Composition API
会取得更大收益
可能的诘问
Composition API
是否和Options API
一起应用?
能够在同一个组件中应用两个 script
标签,一个应用 vue3,一个应用 vue2 写法,一起应用没有问题
<!-- vue3 -->
<script setup>
// vue3 写法
</script>
<!-- 降级 vue2 -->
<script>
export default {data() {},
methods: {}}
</script>
子组件能够间接扭转父组件的数据么,阐明起因
这是一个实际知识点,组件化开发过程中有个 单项数据流准则
,不在子组件中批改父组件是个常识问题
思路
- 讲讲单项数据流准则,表明为何不能这么做
- 举几个常见场景的例子说说解决方案
- 联合实际讲讲如果须要批改父组件状态应该如何做
答复范例
- 所有的
prop
都使得其父子之间造成了一个单向上行绑定:父级prop
的更新会向下流动到子组件中,然而反过来则不行。这样会避免从子组件意外变更父级组件的状态,从而导致你的利用的数据流向难以了解。另外,每次父级组件产生变更时,子组件中所有的prop
都将会刷新为最新的值。这意味着你不应该在一个子组件外部扭转prop
。如果你这样做了,Vue
会在浏览器控制台中收回正告
const props = defineProps(['foo'])
// ❌ 上面行为会被正告, props 是只读的!
props.foo = 'bar'
- 理论开发过程中有两个场景会想要批改一个属性:
这个 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())
- 实际中如果的确想要扭转父组件属性应该
emit
一个事件让父组件去做这个变更。留神尽管咱们不能间接批改一个传入的对象或者数组类型的prop
,然而咱们还是可能间接改内嵌的对象或属性
既然 Vue 通过数据劫持能够精准探测数据变动,为什么还须要虚构 DOM 进行 diff 检测差别
- 响应式数据变动,
Vue
的确能够在数据变动时,响应式零碎能够立即得悉。然而如果给每个属性都增加watcher
用于更新的话,会产生大量的watcher
从而升高性能 - 而且粒度过细也得导致更新不精确的问题,所以
vue
采纳了组件级的watcher
配合diff
来检测差别
Vue 为什么须要虚构 DOM?优缺点有哪些
因为在浏览器中操作
DOM
是很低廉的。频繁的操作DOM
,会产生肯定的性能问题。这就是虚构Dom
的产生起因。Vue2
的Virtual 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
中的作用
答复范例
- 虚构
dom
顾名思义就是虚构的dom
对象,它自身就是一个JavaScript
对象,只不过它是通过不同的属性去形容一个视图构造 - 通过引入
vdom
咱们能够取得如下益处: -
将实在元素节点形象成
VNode
,无效缩小间接操作dom
次数,从而进步程序性能- 间接操作
dom
是有限度的,比方:diff
、clone
等操作,一个实在元素上有许多的内容,如果间接对其进行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
。
- 挂载过程完结后,
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
还存在js
中diff
算法的比对过程。所以,上述性能劣势仅仅实用于大量数据的渲染并且扭转的数据只是一小部分的状况。
如下 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
性能的库,相比vue
、react
,对于vdom
这块更加繁难,适宜咱们学习vdom
。vdom
外面有两个外围的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)