Tiny'Wo | 小窝

网络中的一小块自留地

面试官:Vue项目中你是如何解决跨域的呢?

一、跨域是什�?

跨域本质是浏览器基于同源策略的一种安全手�?
同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功�?
所谓同源(即指在同一个域)具有以下三个相同点

  • 协议相同(protocol�?- 主机相同(host�?- 端口相同(port�?
    反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨�?

    一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用postman请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制�?

二、如何解�?

解决跨域的方法有很多,下面列举了三种�?

  • JSONP
  • CORS
  • Proxy

而在vue项目中,我们主要针对CORSProxy这两种方案进行展开

CORS

CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响�?
CORS 实现起来非常方便,只需要增加一�?HTTP 头,让服务器能声明允许的访问来源

只要后端实现�?CORS,就实现了跨�?

koa框架举例

添加中间件,直接设置Access-Control-Allow-Origin响应�?

1
2
3
4
5
6
7
8
9
10
app.use(async (ctx, next)=> {
ctx.set('Access-Control-Allow-Origin', '*');
ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
if (ctx.method == 'OPTIONS') {
ctx.body = 200;
} else {
await next();
}
})

ps: Access-Control-Allow-Origin 设置�?其实意义不大,可以说是形同虚设,实际应用中,上线前我们会将Access-Control-Allow-Origin 值设为我们目标host

Proxy

代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻�?

方案一

如果是通过vue-cli脚手架工具搭建项目,我们可以通过webpack为我们起一个本地服务器作为请求的代理对�?
通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨�?
vue.config.js文件,新增以下代�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
amodule.exports = {
devServer: {
host: '127.0.0.1',
port: 8084,
open: true,// vue项目启动时自动打开浏览�? proxy: {
'/api': { // '/api'是代理标识,用于告诉node,url前面�?api的就是使用代理的
target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址
changeOrigin: true, //是否跨域
pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'�?"代替
'^/api': ""
}
}
}
}
}

通过axios发送请求中,配置请求的根路�?

1
axios.defaults.baseURL = '/api'

*方案�?

此外,还可通过服务端实现代理请求转�?
express框架为例

1
2
3
4
5
6
7
var express = require('express');
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false
}));
module.exports = app

*方案�?

通过配置nginx实现代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server {
listen 80;
# server_name www.josephxia.com;
location / {
root /var/www/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://127.0.0.1:3000;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

面试官:你了解vue的diff算法吗?说说�?

一、是什�?

diff 算法是一种通过同层的树节点进行比较的高效算�?
其有两个特点�?- 比较只会在同层级进行, 不会跨层级比�?- 在diff比较的过程中,循环从两边向中间比�?
diff 算法在很多场景下都有应用,在 vue 中,作用于虚�?dom 渲染成真�?dom 的新�?VNode 节点比较

二、比较方�?

diff整体策略为:深度优先,同层比�?

  1. 比较只会在同层级进行, 不会跨层级比�?

    img
  2. 比较的过程中,循环从两边向中间收�?

    img

下面举个vue通过diff算法更新的例子:

新旧VNode节点如下图所示:

第一次循环后,发现旧节点D与新节点D相同,直接复用旧节点D作为diff后的第一个真实节点,同时旧节点endIndex移动到C,新节点�?startIndex 移动到了 C

第二次循环后,同样是旧节点的末尾和新节点的开�?都是 C)相同,同理,diff 后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点�?endIndex 移动到了 B,新节点�?startIndex 移动到了 E

第三次循环中,发现E没有找到,这时候只能直接创建新的真实节�?E,插入到第二次创建的 C 节点之后。同时新节点�?startIndex 移动到了 A。旧节点�?startIndex �?endIndex 都保持不�?

第四次循环中,发现了新旧节点的开�?都是 A)相同,于�?diff 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点�?startIndex 移动到了 B,新节点的 startIndex 移动到了 B

第五次循环中,情形同第四次循环一样,因此 diff 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点�?startIndex 移动到了 C,新节点�?startIndex 移动到了 F

新节点的 startIndex 已经大于 endIndex 了,需要创�?newStartIdx �?newEndIdx 之间的所有节点,也就是节点F,直接创�?F 节点对应的真实节点放�?B 节点后面

三、原理分�?

当数据发生改变时,set方法会调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视�?
源码位置:src/core/vdom/patch.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) { // 没有新节点,直接执行destory钩子函数
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}

let isInitialPatch = false
const insertedVnodeQueue = []

if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 判断旧节点和新节点自身一样,一致执行patchVnode
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 否则直接销毁及旧节点,根据新节点生成dom元素
if (isRealElement) {

if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
}
oldVnode = emptyNodeAt(oldVnode)
}
return vnode.elm
}
}
}

patch函数前两个参数位为oldVnode �?Vnode ,分别代表新的节点和之前的旧节点,主要做了四个判断:

  • 没有新节点,直接触发旧节点的destory钩子
  • 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用 createElm
  • 旧节点和新节点自身一样,通过 sameVnode 判断节点是否一样,一样时,直接调�?patchVnode 去处理这两个节点
  • 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节�?
    下面主要讲的是patchVnode部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果新旧节点一致,什么都不做
if (oldVnode === vnode) {
return
}

// 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变�? const elm = vnode.elm = oldVnode.elm

// 异步占位�? if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 如果新旧都是静态节点,并且具有相同的key
// 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode�? // 也不用再有其他操�? if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}

let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}

const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 如果vnode不是文本节点或者注释节�? if (isUndef(vnode.text)) {
// 并且都有子节�? if (isDef(oldCh) && isDef(ch)) {
// 并且子节点不完全一致,则调用updateChildren
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)

// 如果只有新的vnode有子节点
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// elm已经引用了老的dom节点,在老的dom节点上添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)

// 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)

// 如果老节点是文本节点
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}

// 如果新vnode和老vnode是文本节点或注释节点
// 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}

patchVnode主要做了几个判断�?

  • 新节点是否是文本节点,如果是,则直接更新dom的文本内容为新节点的文本内容
  • 新节点和旧节点如果都有子节点,则处理比较更新子节�?- 只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新DOM,并且添加进父节�?- 只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把DOM 删除

子节点不完全一致,则调用updateChildren

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 旧头索引
let newStartIdx = 0 // 新头索引
let oldEndIdx = oldCh.length - 1 // 旧尾索引
let newEndIdx = newCh.length - 1 // 新尾索引
let oldStartVnode = oldCh[0] // oldVnode的第一个child
let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
let newStartVnode = newCh[0] // newVnode的第一个child
let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
let oldKeyToIdx, idxInOld, vnodeToMove, refElm

// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly

// 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结�? while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果oldVnode的第一个child不存�? if (isUndef(oldStartVnode)) {
// oldStart索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left

// 如果oldVnode的最后一个child不存�? } else if (isUndef(oldEndVnode)) {
// oldEnd索引左移
oldEndVnode = oldCh[--oldEndIdx]

// oldStartVnode和newStartVnode是同一个节�? } else if (sameVnode(oldStartVnode, newStartVnode)) {
// patch oldStartVnode和newStartVnode�?索引左移,继续循�? patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]

// oldEndVnode和newEndVnode是同一个节�? } else if (sameVnode(oldEndVnode, newEndVnode)) {
// patch oldEndVnode和newEndVnode,索引右移,继续循环
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]

// oldStartVnode和newEndVnode是同一个节�? } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// patch oldStartVnode和newEndVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// oldStart索引右移,newEnd索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]

// 如果oldEndVnode和newStartVnode是同一个节�? } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// patch oldEndVnode和newStartVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// oldEnd索引左移,newStart索引右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]

// 如果都不匹配
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)

// 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)

// 如果未找到,说明newStartVnode是一个新的节�? if (isUndef(idxInOld)) { // New element
// 创建一个新Vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)

// 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}

// 比较两个具有相同的key的新节点是否是同一个节�? //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom�? if (sameVnode(vnodeToMove, newStartVnode)) {
// patch vnodeToMove和newStartVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 清除
oldCh[idxInOld] = undefined
// 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
// 移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)

// 如果key相同,但是节点不相同,则创建一个新的节�? } else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}

// 右移
newStartVnode = newCh[++newStartIdx]
}
}

while循环主要处理了以下五种情景:

  • 当新�?VNode 节点�?start 相同时,直接 patchVnode ,同时新�?VNode 节点的开始索引都�?1
  • 当新�?VNode 节点�?end相同时,同样直接 patchVnode ,同时新�?VNode 节点的结束索引都�?1
  • 当�?VNode 节点�?start 和新 VNode 节点�?end 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动�?oldEndVnode 的后面,同时�?VNode 节点开始索引加 1,新 VNode 节点的结束索引减 1
  • 当�?VNode 节点�?end 和新 VNode 节点�?start 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动�?oldStartVnode 的前面,同时�?VNode 节点结束索引�?1,新 VNode 节点的开始索引加 1
  • 如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况�? - 从旧�?VNode �?key 值,对应 index 序列�?value 值的哈希表中找到�?newStartVnode 一�?key 的旧�?VNode 节点,再进行patchVnode ,同时将这个真实 dom 移动�?oldStartVnode 对应的真�?dom 的前�? - 调用 createElm 创建一个新�?dom 节点放到当前 newStartIdx 的位�?

小结

  • 当数据发生改变时,订阅者watcher就会调用patch给真实的DOM打补�?- 通过isSameVnode进行判断,相同则调用patchVnode方法
  • patchVnode做了以下操作�? - 找到对应的真实dom,称为el
    • 如果都有都有文本节点且不相等,将el文本节点设置为Vnode的文本节�? - 如果oldVnode有子节点而VNode没有,则删除el子节�? - 如果oldVnode没有子节点而VNode有,则将VNode的子节点真实化后添加到el
    • 如果两者都有子节点,则执行updateChildren函数比较子节�?- updateChildren主要做了以下操作�? - 设置新旧VNode的头尾指�? - 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用patchVnode进行patch重复流程、调用createElem创建一个新节点,从哈希表寻�?key一致的VNode 节点再分情况操作

参考文�?

面试官:动态给vue的data添加一个新的属性时会发生什么?怎样解决�?

image.png

一、直接添加属性的问题

我们从一个例子开�?
定义一个p标签,通过v-for指令进行遍历

然后给botton标签绑定点击事件,我们预期点击按钮时,数据新增一个属性,界面�?新增一�?

1
2
3
4
<p v-for="(value,key) in item" :key="key">
{{ value }}
</p>
<button @click="addProperty">动态添加新属�?/button>

实例化一个vue实例,定义data属性和methods方法

1
2
3
4
5
6
7
8
9
10
11
12
13
const app = new Vue({
el:"#app",
data:()=>{
item:{
oldProperty:"旧属�?
}
},
methods:{
addProperty(){
this.items.newProperty = "新属�? // 为items添加新属�? console.log(this.items) // 输出带有newProperty的items
}
}
})

点击按钮,发现结果不及预期,数据虽然更新了(console打印出了新属性),但页面并没有更�?

二、原理分�?

为什么产生上面的情况呢?

下面来分析一�?
vue2是用过Object.defineProperty实现数据响应�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = {}
Object.defineProperty(obj, 'foo', {
get() {
console.log(`get foo:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
console.log(`set foo:${newVal}`);
val = newVal
}
}
})
}

当我们访问foo属性或者设置foo值的时候都能够触发settergetter

1
2
obj.foo   
obj.foo = 'new'

但是我们为obj添加新属性的时候,却无法触发事件属性的拦截

1
obj.bar  = '新属�?

原因是一开始objfoo属性被设成了响应式数据,而bar是后面新增的属性,并没有通过Object.defineProperty设置成响应式数据

三、解决方�?

Vue 不允许在已经创建的实例上动态添加新的响应式属�?
若想实现数据与视图同步更新,可采取下面三种解决方案:

  • Vue.set()
  • Object.assign()
  • $forcecUpdated()

Vue.set()

Vue.set( target, propertyName/index, value )

参数

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value

返回值:设置的�?
通过Vue.set向响应式对象中添加一个property,并确保这个�?property 同样是响应式的,且触发视图更�?
关于Vue.set源码(省略了很多与本节不相关的代码)

源码位置:src\core\observer\index.js

1
2
3
4
5
6
function set (target: Array<any> | Object, key: any, val: any): any {
...
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}

这里无非再次调用defineReactive方法,实现新增属性的响应�?
关于defineReactive方法,内部还是通过Object.defineProperty实现属性拦�?
大致代码如下�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
console.log(`set ${key}:${newVal}`);
val = newVal
}
}
})
}

Object.assign()

直接使用Object.assign()添加到对象的新属性不会触发更�?
应创建一个新的对象,合并原对象和混入对象的属�?

1
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})

$forceUpdate

如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了�?
$forceUpdate迫使 Vue 实例重新渲染

PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件�?

小结

  • 如果为对象添加少量的新属性,可以直接采用Vue.set()

  • 如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对�?

  • 如果你实在不知道怎么操作时,可采取$forceUpdate()进行强制刷新 (不建�?

PS:vue3是用过proxy实现数据响应式的,直接动态添加新属性仍可以实现数据响应�?

参考文�?

面试官:你有写过自定义指令吗?自定义指令的应用场景有哪些�?

一、什么是指令

开始之前我们先学习一下指令系统这个词

指令系统是计算机硬件的语言系统,也叫机器语言,它是系统程序员看到的计算机的主要属性。因此指令系统表征了计算机的基本功能决定了机器所要求的能�?
vue中提供了一套为数据驱动视图更为方便的操作,这些操作被称为指令系�?
我们看到的v- 开头的行内属性,都是指令,不同的指令可以完成或实现不同的功能

除了核心功能默认内置的指�?(v-model �?v-show),Vue 也允许注册自定义指令

指令使用的几种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
//会实例化一个指令,但这个指令没有参�?
`v-xxx`

// -- 将值传到指令中
`v-xxx="value"`

// -- 将字符串传入到指令中,如`v-html="'<p>内容</p>'"`
`v-xxx="'string'"`

// -- 传参数(`arg`),如`v-bind:class="className"`
`v-xxx:arg="value"`

// -- 使用修饰符(`modifier`�?`v-xxx:arg.modifier="value"`

二、如何实�?

注册一个自定义指令有全局注册与局部注�?
全局注册主要是通过Vue.directive方法进行注册

Vue.directive第一个参数是指令的名字(不需要写上v-前缀),第二个参数可以是对象数据,也可以是一个指令函�?

1
2
3
4
5
6
// 注册一个全局自定义指�?`v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时…�? inserted: function (el) {
// 聚焦元素
el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功�? }
})

局部注册通过在组件options选项中设置directive属�?

1
2
3
4
5
6
directives: {
focus: {
// 指令的定�? inserted: function (el) {
el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功�? }
}
}

然后你可以在模板中任何元素上使用新的 v-focus property,如下:

1
<input v-focus />

自定义指令也像组件那样存在钩子函数:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设�?- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)

  • update:所在组件的 VNode 更新时调用,但是可能发生在其�?VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新

  • componentUpdated:指令所在组件的 VNode 及其�?VNode 全部更新后调�?- unbind:只调用一次,指令与元素解绑时调用

所有的钩子函数的参数都有以下:

  • el:指令所绑定的元素,可以用来直接操作 DOM
  • binding:一个对象,包含以下 property�? - name:指令名,不包括 v- 前缀�? - value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2�? - oldValue:指令绑定的前一个值,仅在 update �?componentUpdated 钩子中可用。无论值是否改变都可用�? - expression:字符串形式的指令表达式。例�?v-my-directive="1 + 1" 中,表达式为 "1 + 1"�? - arg:传给指令的参数,可选。例�?v-my-directive:foo 中,参数�?"foo"�? - modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
  • vnodeVue 编译生成的虚拟节�?- oldVnode:上一个虚拟节点,仅在 update �?componentUpdated 钩子中可�?

    除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素�?dataset 来进�?
    举个例子�?

    1
    2
    3
    4
    5
    6
    7
    <div v-demo="{ color: 'white', text: 'hello!' }"></div>
    <script>
    Vue.directive('demo', function (el, binding) {
    console.log(binding.value.color) // "white"
    console.log(binding.value.text) // "hello!"
    })
    </script>

三、应用场�?

使用自定义指令可以满足我们日常一些场景,这里给出几个自定义指令的案例�?

  • 表单防止重复提交
  • 图片懒加�?- 一�?Copy的功�?

表单防止重复提交

表单防止重复提交这种情况设置一个v-throttle自定义指令来实现

举个例子�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1.设置v-throttle自定义指�?Vue.directive('throttle', {
bind: (el, binding) => {
let throttleTime = binding.value; // 节流时间
if (!throttleTime) { // 用户若不设置节流时间,则默认2s
throttleTime = 2000;
}
let cbFun;
el.addEventListener('click', event => {
if (!cbFun) { // 第一次执�? cbFun = setTimeout(() => {
cbFun = null;
}, throttleTime);
} else {
event && event.stopImmediatePropagation();
}
}, true);
},
});
// 2.为button标签设置v-throttle自定义指�?<button @click="sayHello" v-throttle>提交</button>

图片懒加�?

设置一个v-lazy自定义指令完成图片懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
const LazyLoad = {
// install方法
install(Vue,options){
// 代替图片的loading�? let defaultSrc = options.default;
Vue.directive('lazy',{
bind(el,binding){
LazyLoad.init(el,binding.value,defaultSrc);
},
inserted(el){
// 兼容处理
if('IntersectionObserver' in window){
LazyLoad.observe(el);
}else{
LazyLoad.listenerScroll(el);
}

},
})
},
// 初始�? init(el,val,def){
// data-src 储存真实src
el.setAttribute('data-src',val);
// 设置src为loading�? el.setAttribute('src',def);
},
// 利用IntersectionObserver监听el
observe(el){
let io = new IntersectionObserver(entries => {
let realSrc = el.dataset.src;
if(entries[0].isIntersecting){
if(realSrc){
el.src = realSrc;
el.removeAttribute('data-src');
}
}
});
io.observe(el);
},
// 监听scroll事件
listenerScroll(el){
let handler = LazyLoad.throttle(LazyLoad.load,300);
LazyLoad.load(el);
window.addEventListener('scroll',() => {
handler(el);
});
},
// 加载真实图片
load(el){
let windowHeight = document.documentElement.clientHeight
let elTop = el.getBoundingClientRect().top;
let elBtm = el.getBoundingClientRect().bottom;
let realSrc = el.dataset.src;
if(elTop - windowHeight<0&&elBtm > 0){
if(realSrc){
el.src = realSrc;
el.removeAttribute('data-src');
}
}
},
// 节流
throttle(fn,delay){
let timer;
let prevTime;
return function(...args){
let currTime = Date.now();
let context = this;
if(!prevTime) prevTime = currTime;
clearTimeout(timer);

if(currTime - prevTime > delay){
prevTime = currTime;
fn.apply(context,args);
clearTimeout(timer);
return;
}

timer = setTimeout(function(){
prevTime = Date.now();
timer = null;
fn.apply(context,args);
},delay);
}
}

}
export default LazyLoad;

一�?Copy的功�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { Message } from 'ant-design-vue';

const vCopy = { //
/*
bind 钩子函数,第一次绑定时调用,可以在这里做初始化设置
el: 作用�?dom 对象
value: 传给指令的值,也就是我们要 copy 的�? */
bind(el, { value }) {
el.$value = value; // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用�? el.handler = () => {
if (!el.$value) {
// 值为空的时候,给出提示,我这里的提示是用的 ant-design-vue 的提示,你们随意
Message.warning('无复制内�?);
return;
}
// 动态创�?textarea 标签
const textarea = document.createElement('textarea');
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时�?textarea 移出可视区域
textarea.readOnly = 'readonly';
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
// 将要 copy 的值赋�?textarea 标签�?value 属�? textarea.value = el.$value;
// �?textarea 插入�?body �? document.body.appendChild(textarea);
// 选中值并复制
textarea.select();
// textarea.setSelectionRange(0, textarea.value.length);
const result = document.execCommand('Copy');
if (result) {
Message.success('复制成功');
}
document.body.removeChild(textarea);
};
// 绑定点击事件,就是所谓的一�?copy �? el.addEventListener('click', el.handler);
},
// 当传进来的值更新的时候触�? componentUpdated(el, { value }) {
el.$value = value;
},
// 指令与元素解绑的时候,移除事件绑定
unbind(el) {
el.removeEventListener('click', el.handler);
},
};

export default vCopy;

关于自定义指令还有很多应用场景,如:拖拽指令、页面水印、权限校验等等应用场�?

参考文�?

面试官:你是怎么处理vue项目中的错误的?

一、错误类�?

任何一个框架,对于错误的处理都是一种必备的能力

Vue 中,则是定义了一套对应的错误处理规则给到使用者,且在源代码级别,对部分必要的过程做了一定的错误处理�?
主要的错误来源包括:

  • 后端接口错误
  • 代码中本身逻辑错误

二、如何处�?

后端接口错误

通过axiosinterceptor实现网络请求的response先进行一层拦�?

1
2
3
4
5
6
7
8
9
10
11
12
13
apiClient.interceptors.response.use(
response => {
return response;
},
error => {
if (error.response.status == 401) {
router.push({ name: "Login" });
} else {
message.error("出错�?);
return Promise.reject(error);
}
}
);

代码逻辑问题

全局设置错误处理

设置全局错误处理函数

1
2
3
4
5
Vue.config.errorHandler = function (err, vm, info) {
// handle error
// `info` �?Vue 特定的错误信息,比如错误所在的生命周期钩子
// 只在 2.2.0+ 可用
}

errorHandler指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 Vue 实例

不过值得注意的是,在不同 Vue 版本中,该全局 API 作用的范围会有所不同�?

�?2.2.0 起,这个钩子也会捕获组件生命周期钩子里的错误。同样的,当这个钩子�?undefined 时,被捕获的错误会通过 console.error 输出而避免应用崩

�?2.4.0 起,这个钩子也会捕获 Vue 自定义事件处理函数内部的错误�?
�?2.6.0 起,这个钩子也会捕获 v-on DOM 监听器内部抛出的错误。另外,如果任何被覆盖的钩子或处理函数返回一�?Promise �?(例如 async 函数),则来自�?Promise 链的错误也会被处�?

生命周期钩子

errorCaptured�?2.5.0 新增的一个生命钩子函数,当捕获到一个来自子孙组件的错误时被调用

基本类型

1
(err: Error, vm: Component, info: string) => ?boolean

此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播

参考官网,错误传播规则如下�?

  • 默认情况下,如果全局�?config.errorHandler 被定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报
  • 如果一个组件的继承或父级从属链路中存在多个 errorCaptured 钩子,则它们将会被相同的错误逐个唤起�?- 如果�?errorCaptured 钩子自身抛出了一个错误,则这个新错误和原本被捕获的错误都会发送给全局�?config.errorHandler
  • 一�?errorCaptured 钩子能够返回 false 以阻止错误继续向上传播。本质上是说“这个错误已经被搞定了且应该被忽略”。它会阻止其它任何会被这个错误唤起的 errorCaptured 钩子和全局�?config.errorHandler

下面来看个例�?
定义一个父组件cat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Vue.component('cat', {
template:`
<div>
<h1>Cat: </h1>
<slot></slot>
</div>`,
props:{
name:{
required:true,
type:String
}
},
errorCaptured(err,vm,info) {
console.log(`cat EC: ${err.toString()}\ninfo: ${info}`);
return false;
}

});

定义一个子组件kitten,其中dontexist()并没有定义,存在错误

1
2
3
4
5
6
7
8
9
Vue.component('kitten', {
template:'<div><h1>Kitten: {{ dontexist() }}</h1></div>',
props:{
name:{
required:true,
type:String
}
}
});

页面中使用组�?

1
2
3
4
5
<div id="app" v-cloak>
<cat name="my cat">
<kitten></kitten>
</cat>
</div>

在父组件的errorCaptured则能够捕获到信息

1
2
cat EC: TypeError: dontexist is not a function
info: render

三、源码分�?

异常处理源码

源码位置�?src/core/util/error.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// Vue 全局配置,也就是上面的Vue.config
import config from '../config'
import { warn } from './debug'
// 判断环境
import { inBrowser, inWeex } from './env'
// 判断是否是Promise,通过val.then === 'function' && val.catch === 'function', val �?== null && val !== undefined
import { isPromise } from 'shared/util'
// 当错误函数处理错误时,停用deps跟踪以避免可能出现的infinite rendering
// 解决以下出现的问题https://github.com/vuejs/vuex/issues/1505的问�?import { pushTarget, popTarget } from '../observer/dep'

export function handleError (err: Error, vm: any, info: string) {
// Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
pushTarget()
try {
// vm指当前报错的组件实例
if (vm) {
let cur = vm
// 首先获取到报错的组件,之后递归查找当前组件的父组件,依次调用errorCaptured 方法�? // 在遍历调用完所�?errorCaptured 方法、或 errorCaptured 方法有报错时,调�?globalHandleError 方法
while ((cur = cur.$parent)) {
const hooks = cur.$options.errorCaptured
// 判断是否存在errorCaptured钩子函数
if (hooks) {
// 选项合并的策略,钩子函数会被保存在一个数组中
for (let i = 0; i < hooks.length; i++) {
// 如果errorCaptured 钩子执行自身抛出了错误,
// 则用try{}catch{}捕获错误,将这个新错误和原本被捕获的错误都会发送给全局的config.errorHandler
// 调用globalHandleError方法
try {
// 当前errorCaptured执行,根据返回是否是false�? // 是false,capture = true,阻止其它任何会被这个错误唤起的 errorCaptured 钩子和全局�?config.errorHandler
// 是true capture = fale,组件的继承或父级从属链路中存在的多�?errorCaptured 钩子,会被相同的错误逐个唤起
// 调用对应的钩子函数,处理错误
const capture = hooks[i].call(cur, err, vm, info) === false
if (capture) return
} catch (e) {
globalHandleError(e, cur, 'errorCaptured hook')
}
}
}
}
}
// 除非禁止错误向上传播,否则都会调用全局的错误处理函�? globalHandleError(err, vm, info)
} finally {
popTarget()
}
}
// 异步错误处理函数
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
// 根据参数选择不同的handle执行方式
res = args ? handler.apply(context, args) : handler.call(context)
// handle返回结果存在
// res._isVue an flag to avoid this being observed,如果传入值的_isVue为ture�?即传入的值是Vue实例本身)不会新建observer实例
// isPromise(res) 判断val.then === 'function' && val.catch === 'function', val �?== null && val !== undefined
// !res._handled _handle是Promise 实例的内部变量之一,默认是false,代表onFulfilled,onRejected是否被处�? if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// avoid catch triggering multiple times when nested calls
// 避免嵌套调用时catch多次的触�? res._handled = true
}
} catch (e) {
// 处理执行错误
handleError(e, vm, info)
}
return res
}

//全局错误处理
function globalHandleError (err, vm, info) {
// 获取全局配置,判断是否设置处理函数,默认undefined
// 已配�? if (config.errorHandler) {
// try{}catch{} 住全局错误处理函数
try {
// 执行设置的全局错误处理函数,handle error 想干啥就干啥💗
return config.errorHandler.call(null, err, vm, info)
} catch (e) {
// 如果开发者在errorHandler函数中手动抛出同样错误信息throw err
// 判断err信息是否相等,避免log两次
// 如果抛出新的错误信息throw err Error('你好�?),将会一起log输出
if (e !== err) {
logError(e, null, 'config.errorHandler')
}
}
}
// 未配置常规log输出
logError(err, vm, info)
}

// 错误输出函数
function logError (err, vm, info) {
if (process.env.NODE_ENV !== 'production') {
warn(`Error in ${info}: "${err.toString()}"`, vm)
}
/* istanbul ignore else */
if ((inBrowser || inWeex) && typeof console !== 'undefined') {
console.error(err)
} else {
throw err
}
}

小结

  • handleError在需要捕获异常的地方调用,首先获取到报错的组件,之后递归查找当前组件的父组件,依次调用errorCaptured 方法,在遍历调用完所�?errorCaptured 方法�?errorCaptured 方法有报错时,调�?globalHandleError 方法
  • globalHandleError 调用全局�?errorHandler 方法,再通过logError判断环境输出错误信息
  • invokeWithErrorHandling更好的处理异步错误信�?- logError判断环境,选择不同的抛错方式。非生产环境下,调用warn方法处理错误

参考文�?

面试官:Vue中的过滤器了解吗?过滤器的应用场景有哪些�?

一、是什�?过滤器(filter)是输送介质管道上不可缺少的一种装�?

大白话,就是把一些不必要的东西过滤掉

过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数

Vue 允许你自定义过滤器,可被用于一些常见的文本格式�?
ps: Vue3中已废弃filter

二、如何用

vue中的过滤器可以用在两个地方:双花括号插值和 v-bind 表达式,过滤器应该被添加�?JavaScript 表达式的尾部,由“管道”符号指示:

1
2
3
4
5
<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- �?`v-bind` �?-->
<div v-bind:id="rawId | formatId"></div>

定义filter

在组件的选项中定义本地的过滤�?

1
2
3
4
5
6
7
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}

定义全局过滤器:

1
2
3
4
5
6
7
8
9
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({
// ...
})

注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器

过滤器函数总接收表达式的�?(之前的操作链的结�? 作为第一个参数。在上述例子中,capitalize 过滤器函数将会收�?message 的值作为第一个参�?
过滤器可以串联:

1
{{ message | filterA | filterB }}

在这个例子中,filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函�?filterB,将 filterA 的结果传递到 filterB 中�?
过滤器是 JavaScript 函数,因此可以接收参数:

1
{{ message | filterA('arg1', arg2) }}

这里,filterA 被定义为接收三个参数的过滤器函数�?
其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达�?arg2 的值作为第三个参数

举个例子�?

1
2
3
4
5
6
7
8
9
10
<div id="app">
<p>{{ msg | msgFormat('疯狂','--')}}</p>
</div>

<script>
// 定义一�?Vue 全局的过滤器,名字叫�? msgFormat
Vue.filter('msgFormat', function(msg, arg, arg2) {
// 字符串的 replace 方法,第一个参数,除了可写一�?字符串之外,还可以定义一个正�? return msg.replace(/单纯/g, arg+arg2)
})
</script>

小结�?

  • 部过滤器优先于全局过滤器被调用
  • 一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往�?

三、应用场�?

平时开发中,需要用到过滤器的地方有很多,比如单位转换、数字打点、文本格式化、时间格式化之类的等

比如我们要实现将30000 => 30,000,这时候我们就需要使用过滤器

1
2
3
4
5
Vue.filter('toThousandFilter', function (value) {
if (!value) return ''
value = value.toString()
return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,')
})

四、原理分�?

使用过滤�?

1
{{ message | capitalize }}

在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要是用过parseFilters,我们放到最后讲

1
_s(_f('filterFormat')(message))

首先分析一下_f�?
_f 函数全名是:resolveFilter,这个函数的作用是从this.$options.filters中找出注册的过滤器并返回

1
2
3
4
5
6
7
8
9
10
11
// 变为
this.$options.filters['filterFormat'](message) // message为参�?```

关于`resolveFilter`

```js
import { indentity,resolveAsset } from 'core/util/index'

export function resolveFilter(id){
return resolveAsset(this.$options,'filters',id,true) || identity
}

内部直接调用resolveAsset,将option对象,类型,过滤器id,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器;

resolveAsset的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function resolveAsset(options,type,id,warnMissing){ // 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给�?'filters',实际这个函数还可以拿到其他很多东�?    if(typeof id !== 'string'){ // 判断传递的过滤器id 是不是字符串,不是则直接返回
return
}
const assets = options[type] // 将我们注册的所有过滤器保存在变量中
// 接下来的逻辑便是判断id是否在assets中存在,即进行匹�? if(hasOwn(assets,id)) return assets[id] // 如找到,直接返回过滤�? // 没有找到,代码继续执�? const camelizedId = camelize(id) // 万一你是驼峰的呢
if(hasOwn(assets,camelizedId)) return assets[camelizedId]
// 没找到,继续执行
const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰�? if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId]
// 如果还是没找到,则检查原型链(即访问属�?
const result = assets[id] || assets[camelizedId] || assets[PascalCaseId]
// 如果依然没找到,则在非生产环境的控制台打印警�? if(process.env.NODE_ENV !== 'production' && warnMissing && !result){
warn('Failed to resolve ' + type.slice(0,-1) + ': ' + id, options)
}
// 无论是否找到,都返回查找结果
return result
}

下面再来分析一下_s�?
_s 函数的全称是 toString,过滤器处理后的结果会当作参数传递给 toString函数,最�?toString函数执行后的结果会保存到Vnode中的text属性中,渲染到视图�?

1
2
3
4
5
6
7
function toString(value){
return value == null
? ''
: typeof value === 'object'
? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来控制字符串里面的间距
: String(value)
}

最后,在分析下parseFilters,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function parseFilters (filter) {
let filters = filter.split('|')
let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组
let i
if (filters) {
for(i = 0;i < filters.length;i++){
experssion = warpFilter(expression,filters[i].trim()) // 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参�? }
}
return expression
}
// warpFilter函数实现
function warpFilter(exp,filter){
// 首先判断过滤器是否有其他参数
const i = filter.indexof('(')
if(i<0){ // 不含其他参数,直接进行过滤器表达式字符串的拼�? return `_f("${filter}")(${exp})`
}else{
const name = filter.slice(0,i) // 过滤器名�? const args = filter.slice(i+1) // 参数,但还多�?�?�? return `_f('${name}')(${exp},${args}` // 注意这一步少给了一�?')'
}
}

小结�?

  • 在编译阶段通过parseFilters将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数)
  • 编译后通过调用resolveFilter函数找到对应过滤器并返回结果
  • 执行结果作为参数传递给toString函数,而toString执行后,其结果会保存在Vnodetext属性中,渲染到视图

参考文�?

面试官:SPA首屏加载速度慢的怎么解决�?

image.png

一、什么是首屏加载

首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容

首屏加载可以说是用户体验�?*最重要**的环�?

关于计算首屏时间

利用performance.timing提供的数据:

image.png

通过DOMContentLoad或者performance来计算出首屏时间

1
2
3
4
5
6
7
8
9
10
11
12
13
// 方案一�?document.addEventListener('DOMContentLoaded', (event) => {
console.log('first contentful painting');
});
// 方案二:
performance.getEntriesByName("first-contentful-paint")[0].startTime

// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一�?PerformancePaintTiming的实例,结构如下�?{
name: "first-contentful-paint",
entryType: "paint",
startTime: 507.80000002123415,
duration: 0,
};

二、加载慢的原�?

在页面渲染的过程,导致加载速度慢的因素可能如下�?

  • 网络延时问题
  • 资源文件体积是否过大
  • 资源是否重复发送请求去加载�?- 加载脚本的时候,渲染内容堵塞�?

三、解决方�?

常见的几种SPA首屏优化方式

  • 减小入口文件�?- 静态资源本地缓�?- UI框架按需加载
  • 图片资源的压�?- 组件重复打包
  • 开启GZip压缩
  • 使用SSR

减小入口文件体积

常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加

image.png

vue-router配置路由的时候,采用动态加载路由的形式

1
2
3
4
5
routes:[ 
path: 'Blogs',
name: 'ShowBlogs',
component: () => import('./components/ShowBlogs.vue')
]

以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组�?

静态资源本地缓�?

后端返回资源问题�?

  • 采用HTTP缓存,设置Cache-ControlLast-ModifiedEtag等响应头

  • 采用Service Worker离线缓存

前端合理利用localStorage

UI框架按需加载

在日常使用UI框架,例如element-UI、或者antd,我们经常性直接引用整个UI�?

1
2
import ElementUI from 'element-ui'
Vue.use(ElementUI)

但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用

1
2
3
4
import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui';
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)

组件重复打包

假设A.js文件是一个常用的库,现在有多个路由使用了A.js文件,这就造成了重复下�?
解决方案:在webpackconfig文件中,修改CommonsChunkPlugin的配�?

1
minChunks: 3

minChunks�?表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组�?

图片资源的压�?

图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素

对于所有的图片资源,我们可以进行适当的压�?
对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力�?

开启GZip压缩

拆完包之后,我们再用gzip做一下压�?安装compression-webpack-plugin

1
cnmp i compression-webpack-plugin -D

vue.congig.js中引入并修改webpack配置

1
2
3
4
5
6
7
8
9
10
11
const CompressionPlugin = require('compression-webpack-plugin')

configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境修改配�?..
config.mode = 'production'
return {
plugins: [new CompressionPlugin({
test: /\.js$|\.html$|\.css/, //匹配文件�? threshold: 10240, //对超�?0k的数据进行压�? deleteOriginalAssets: false //是否删除原文�? })]
}
}

在服务器我们也要做相应的配置 如果发送请求的浏览器支持gzip,就发送给它gzip格式的文�?我的服务器是用express框架搭建�?只要安装一下compression就能使用

1
2
const compression = require('compression')
app.use(compression()) // 在其他中间件使用之前调用

使用SSR

SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览�?
从头搭建一个服务端渲染是很复杂的,vue应用建议使用Nuxt.js实现服务端渲�?

小结�?

减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优�?�?页面渲染优化

下图是更为全面的首屏优化的方�?
image.png

大家可以根据自己项目的情况选择各种方式进行首屏渲染的优�?

参考文�?

面试官:v-if和v-for的优先级是什么?

一、作�?

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true值的时候被渲染

v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使�?item in items 形式的特殊语法,其中 items 是源数据数组或者对象,�?item 则是被迭代的数组元素的别�?
�?v-for 的时候,建议设置key值,并且保证每个key值是独一无二的,这便于diff算法进行优化

两者在用法�?

1
2
3
4
5
<Modal v-if="isShow" />

<li v-for="item in items" :key="item.id">
{{ item.label }}
</li>

二、优先级

v-ifv-for都是vue模板系统中的指令

vue模板编译的时候,会将指令系统转化成可执行的render函数

示例

编写一个p标签,同时使用v-if�?v-for

1
2
3
4
5
<div id="app">
<p v-if="isShow" v-for="item in items">
{{ item.title }}
</p>
</div>

创建vue实例,存放isShowitems数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const app = new Vue({
el: "#app",
data() {
return {
items: [
{ title: "foo" },
{ title: "baz" }]
}
},
computed: {
isShow() {
return this.items && this.items.length > 0
}
}
})

模板指令的代码都会生成在render函数中,通过app.$options.render就能得到渲染函数

1
2
3
4
5
6
ƒ anonymous() {
with (this) { return
_c('div', { attrs: { "id": "app" } },
_l((items), function (item)
{ return (isShow) ? _c('p', [_v("\n" + _s(item.title) + "\n")]) : _e() }), 0) }
}

_lvue的列表渲染函数,函数内部都会进行一次if判断

初步得到结论:v-for优先级是比v-if�?
再将v-forv-if置于不同标签

1
2
3
4
5
<div id="app">
<template v-if="isShow">
<p v-for="item in items">{{item.title}}</p>
</template>
</div>

再输出下render函数

1
2
3
4
5
6
ƒ anonymous() {
with(this){return
_c('div',{attrs:{"id":"app"}},
[(isShow)?[_v("\n"),
_l((items),function(item){return _c('p',[_v(_s(item.title))])})]:_e()],2)}
}

这时候我们可以看到,v-forv-if作用在不同标签时候,是先进行判断,再进行列表的渲�?
我们再在查看下vue源码

源码位置: \vue-dev\src\compiler\codegen\index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
// component or element
...
}

在进行if判断的时候,v-for是比v-if先进行判�?
最终结论:v-for优先级比v-if�?

三、注意事�?

  1. 永远不要�?v-if �?v-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断�?2. 如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环
1
2
3
<template v-if="isShow">
<p v-for="item in items">
</template>
  1. 如果条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的�?
    1
    2
    3
    4
    5
    6
    7
    computed: {
    items: function() {
    return this.list.filter(function (item) {
    return item.isShow
    })
    }
    }

面试官:说说你对keep-alive的理解是什么?

一、Keep-alive 是什�?

keep-alivevue中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染DOM

keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它�?
keep-alive可以设置以下props属性:

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存

  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存

  • max - 数字。最多可以缓存多少组件实�?
    关于keep-alive的基本用法:

1
2
3
<keep-alive>
<component :is="view"></component>
</keep-alive>

使用includesexclude�?

1
2
3
4
5
6
7
8
9
10
11
12
13
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>

<!-- 正则表达�?(使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>

<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>

匹配首先检查组件自身的 name 选项,如�?name 选项不可用,则匹配它的局部注册名�?(父组�?components 选项的键值),匿名组件不能被匹配

设置�?keep-alive 缓存的组件,会多出两个生命周期钩子(activateddeactivated):

  • 首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > … … > beforeRouteLeave > deactivated

  • 再次进入组件时:beforeRouteEnter >activated > … … > beforeRouteLeave > deactivated

二、使用场�?

使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive

举个栗子:

当我们从首页�?列表页�?商详页�?再返回,这时候列表页应该是需要keep-alive

首页�?列表页�?商详页�?返回到列表页(需要缓�?�?返回到首�?需要缓�?�?再次进入列表�?不需要缓�?,这时候可以按需来控制页面的keep-alive

在路由中设置keepAlive属性判断是否需要缓�?

1
2
3
4
5
6
7
8
9
10
{
path: 'list',
name: 'itemList', // 列表�? component (resolve) {
require(['@/pages/item/list'], resolve)
},
meta: {
keepAlive: true,
title: '列表�?
}
}

使用<keep-alive>

1
2
3
4
5
6
7
8
<div id="app" class='wrapper'>
<keep-alive>
<!-- 需要缓存的视图组件 -->
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 不需要缓存的视图组件 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>

三、原理分�?

keep-alivevue中内置的一个组�?
源码位置:src/core/components/keep-alive.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
export default {
name: 'keep-alive',
abstract: true,

props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},

created () {
this.cache = Object.create(null)
this.keys = []
},

destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},

mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},

render() {
/* 获取默认插槽中的第一个组件节�?*/
const slot = this.$slots.default
const vnode = getFirstComponentChild(slot)
/* 获取该组件节点的componentOptions */
const componentOptions = vnode && vnode.componentOptions

if (componentOptions) {
/* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
const name = getComponentName(componentOptions)

const { include, exclude } = this
/* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
if (
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}

const { cache, keys } = this
/* 获取组件的key�?*/
const key = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
/* 拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存 */
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key)
keys.push(key)
}
/* 如果没有命中缓存,则将其设置进缓�?*/
else {
cache[key] = vnode
keys.push(key)
// prune oldest entry
/* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一�?*/
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}

vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
}

可以看到该组件没有template,而是用了render,在组件渲染的时候会自动执行render函数

this.cache是一个对象,用来存储需要缓存的组件,它将以如下形式存储�?

1
2
3
4
5
this.cache = {
'key1':'组件1',
'key2':'组件2',
// ...
}

在组件销毁的时候执行pruneCacheEntry函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
/* 判断当前没有处于被渲染状态的组件,将其销�?/
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}

mounted钩子函数中观�?include �?exclude 的变化,如下�?

1
2
3
4
5
6
7
8
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}

如果includeexclude 发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache函数,函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
function pruneCache (keepAliveInstance, filter) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode = cache[key]
if (cachedNode) {
const name = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}

在该函数内对this.cache对象进行遍历,取出每一项的name值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry函数将其从this.cache对象剔除即可

关于keep-alive的最强大缓存功能是在render函数中实�?
首先获取组件的key值:

1
2
3
const key = vnode.key == null? 
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key

拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存,如下:

1
2
3
4
5
6
7
/* 如果命中缓存,则直接从缓存中�?vnode 的组件实�?*/
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
/* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一�?*/
remove(keys, key)
keys.push(key)
}

直接从缓存中�?vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一�?
this.cache对象中没有该key值的情况,如下:

1
2
3
4
5
6
7
8
9
/* 如果没有命中缓存,则将其设置进缓�?*/
else {
cache[key] = vnode
keys.push(key)
/* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一�?*/
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}

表明该组件还没有被缓存过,则以该组件的key为键,组件vnode为值,将其存入this.cache中,并且把key存入this.keys�?
此时再判断this.keys中缓存组件的数量是否超过了设置的最大缓存数量值this.max,如果超过了,则把第一个缓存组件删�?

四、思考题:缓存后如何获取数据

解决方案可以有以下两种:

  • beforeRouteEnter

  • actived

beforeRouteEnter

每次组件渲染的时候,都会执行beforeRouteEnter

1
2
3
4
5
6
7
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
},

actived

keep-alive缓存的组件被激活的时候,都会执行actived钩子

1
2
3
activated(){
this.getData() // 获取数据
},

注意:服务器端渲染期间avtived不被调用

参考文�?

面试官:你知道vue中key的原理吗?说说你对它的理�?

一、Key是什�?

开始之前,我们先还原两个实际工作场�?

  1. 当我们在使用v-for时,需要给单元加上key
1
2
3
<ul>
<li v-for="item in items" :key="item.id">...</li>
</ul>
  1. +new Date()生成的时间戳作为key,手动强制触发重新渲�?```js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43

    那么这背后的逻辑是什么,`key`的作用又是什么?

    一句话来讲

    > key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确�?更快的找到对应的vnode节点

    ### 场景背后的逻辑

    当我们在使用`v-for`时,需要给单元加上`key`

    - 如果不用key,Vue会采用就地复地原则:最小化element的移动,并且会尝试尽最大程度在同适当的地方对相同类型的element,做patch或者reuse�?
    - 如果使用了key,Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed

    用`+new Date()`生成的时间戳作为`key`,手动强制触发重新渲�?
    - 当拥有新值的rerender作为key时,拥有了新key的Comp出现了,那么旧key Comp会被移除,新key Comp触发渲染


    ## 二、设置key与不设置key区别


    举个例子�?
    创建一个实例,2秒后往`items`数组插入数据

    ```html
    <body>
    <div id="demo">
    <p v-for="item in items" :key="item">{{item}}</p>
    </div>
    <script src="../../dist/vue.js"></script>
    <script>
    // 创建实例
    const app = new Vue({
    el: '#demo',
    data: { items: ['a', 'b', 'c', 'd', 'e'] },
    mounted () {
    setTimeout(() => {
    this.items.splice(2, 0, 'f') //
    }, 2000);
    },
    });
    </script>
    </body>

在不使用key的情况,vue会进行这样的操作�?

分析下整体流程:

  • 比较A,A,相同类型的节点,进行patch,但数据相同,不发生dom操作
  • 比较B,B,相同类型的节点,进行patch,但数据相同,不发生dom操作
  • 比较C,F,相同类型的节点,进行patch,数据不同,发生dom操作
  • 比较D,C,相同类型的节点,进行patch,数据不同,发生dom操作
  • 比较E,D,相同类型的节点,进行patch,数据不同,发生dom操作
  • 循环结束,将E插入到DOM�?
    一共发生了3次更新,1次插入操�?
    在使用key的情况:vue会进行这样的操作�?
  • 比较A,A,相同类型的节点,进行patch,但数据相同,不发生dom操作
  • 比较B,B,相同类型的节点,进行patch,但数据相同,不发生dom操作
  • 比较C,F,不相同类型的节�? - 比较E、E,相同类型的节点,进行patch,但数据相同,不发生dom操作
  • 比较D、D,相同类型的节点,进行patch,但数据相同,不发生dom操作
  • 比较C、C,相同类型的节点,进行patch,但数据相同,不发生dom操作
  • 循环结束,将F插入到C之前

一共发生了0次更新,1次插入操�?
通过上面两个小例子,可见设置key能够大大减少对页面的DOM操作,提高了diff效率

设置key值一定能提高diff效率吗?

其实不然,文档中也明确表�?

�?Vue.js �?v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移�?DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元�?
这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状�?(例如:表单输入�? 的列表渲染输�?
建议尽可能在使用 v-for 时提�?key,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升

三、原理分�?

源码位置:core/vdom/patch.js

这里判断是否为同一个key,首先判断的是key值是否相等如果没有设置key,那么keyundefined,这时候undefined是恒等于undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}

updateChildren方法中会对新旧vnode进行diff,然后将比对出的结果用来更新真实的DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
...
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
...
} else if (isUndef(oldEndVnode)) {
...
} else if (sameVnode(oldStartVnode, newStartVnode)) {
...
} else if (sameVnode(oldEndVnode, newEndVnode)) {
...
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
...
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
...
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
...
}

参考文�?

0%