vue原理

MVVM

组件化基础

  1. 很久以前就有组件化 asp,jsp,php,nodejs中也有类似的组件化,传统组件,只是静态渲染,更新还要依赖于操作DOM
  2. 数据驱动视图(vue MVVM,react setState)
    • v -> view 页面视图
    • m -> model 数据,data
    • vm -> 链接 v 和 m 的事件操作,方法函数等,业务处理和数据转化

vue 响应式

  • 组件data的数据一旦变化,立刻触发视图的更新
  • 实现数据驱动的第一步
  • 考察vue原理的第一题

核心API - Object.defineProperty

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const data = {}
const name = '张三'
Object.defineProperty(data, 'name', {
get: function(){
console.log('get')
return name
},
set: function(newVal){
console.log('set')
name = newVal
}
})

// 测试
console.log(data.name) // get
data.name = 'list'

Object.defineProperty 实现响应式

  1. 监听对象,监听数组
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
// 触发更新视图
function updateView() {
console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)

// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)

// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue

// 触发更新视图
updateView()
}
}
})
}

// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}

// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }

if (Array.isArray(target)) {
target.__proto__ = arrProto
}

// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}

// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}

// 监听数据
observer(data)

// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
  • 需要深度监听,递归到底,一次性计算量大
  • 无法监听新增,删除属性(vue.set vue.delete)
  • 无法原生监听数组,需要特殊处理

Object.defineProperty的缺点(vue 3.0 启用 Proxy)

  • proxy 兼容性不好,且无法 ployfill
  • vue2.x 还会存在一段时间,所以都得学

虚拟 DOM 和 diff

  • vdom 是实现 vue 和 react 的重要基石

  • diff 算法是 vdom 中最核心,最关键部分

  • vdom 是热门话题

  • dom操作非常好耗费性能,以前用jq,可以自行控制dom操作的时机,手动调整

  • vue和react是数据驱动视图,如何有效控制操作dom

  • 有了一定复杂度,想减少计算次数比较难

  • 能不能把计算,更多的转移js计算?因为js执行速度很快

  • vdom-用js模拟dom结构,计算出最小的变更,操作dom

用js模拟dom结构

1
2
3
4
5
6
<div id="div1" class="container">
<p>vdom</p>
<ul style="font-size: 20px">
<li>a</li>
</ul>
</div>

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
{
tag: 'div',
props: {
className: 'container',
id: 'div1'
},
children: [
{
tag: 'p',
children: 'vdom'
},
{
tag: 'ul',
props: {
style: 'font-size: 20px'
},
children: [
{
tag: 'li',
children: 'a'
}
// ...
]
}
]
}

通过snabbdom学习vdom

  • 简洁强大的vdom库,易学易用
  • vue参考它实现的vdom和diff
  • 地址
  • vue3.0重写了vdom,优化了性能,但基本理念不变
  • react vdom具体实现和vue不同

diff算法

  • diff算法是vdom种最核心,关键的部分
  • diff算法能在日常使用 vue react中体现出来(如key)
  • diff算法是前端热门话题

diff概述

  • diff即对比,是一个广泛的概念,如linux diff、git diff
  • 两个js对象也可以做diff,如https://github.com/cujojs/jiff
  • 两棵树做diff,如这里的vdom diff

树diff

树diff的时间复杂度是O(n^3)

  • 步骤
    • 遍历tree1
    • 遍历tree2
    • 排序
  • 1000个节点,计算1亿次,算法不可用

优化时间复杂度到O(n)

  • 只比较同一级,不跨级比较
  • tag不相同,则直接删掉重建,不再深度比较
  • tag 和 key ,两者都相同,则认为是相同节点,不再深度比较

snabbdom源码解读

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
// h函数 产出 vnode 结构
var vnode = h('div#container.two.classes', { on: { click: someFn } }, [
h('span', { style: { fontWeight: 'bold' } }, 'This is bold'),
' and this is just normal text',
h('a', { props: { href: '/foo' } }, 'I\'ll take you places!')
])
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode)

var newVnode = h('div#container.two.classes', { on: { click: anotherEventHandler } }, [
h('span', { style: { fontWeight: 'normal', fontStyle: 'italic' } }, 'This is now italic type'),
' and this is still just normal text',
h('a', { props: { href: '/bar' } }, 'I\'ll take you places!')
])
// Second `patch` invocation
patch(vnode, newVnode) // Snabbdom efficiently updates the old view to the new state


// h函数
import { vnode, VNode, VNodeData } from './vnode'
import * as is from './is'

export type VNodes = VNode[]
export type VNodeChildElement = VNode | string | number | undefined | null
export type ArrayOrElement<T> = T | T[]
export type VNodeChildren = ArrayOrElement<VNodeChildElement>

function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void {
data.ns = 'http://www.w3.org/2000/svg'
if (sel !== 'foreignObject' && children !== undefined) {
for (let i = 0; i < children.length; ++i) {
const childData = children[i].data
if (childData !== undefined) {
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel)
}
}
}
}

export function h (sel: string): VNode
export function h (sel: string, data: VNodeData | null): VNode
export function h (sel: string, children: VNodeChildren): VNode
export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
export function h (sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {}
var children: any
var text: any
var i: number
if (c !== undefined) {
if (b !== null) {
data = b
}
if (is.array(c)) {
children = c
} else if (is.primitive(c)) {
text = c
} else if (c && c.sel) {
children = [c]
}
} else if (b !== undefined && b !== null) {
if (is.array(b)) {
children = b
} else if (is.primitive(b)) {
text = b
} else if (b && b.sel) {
children = [b]
} else { data = b }
}
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
}
}
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
addNS(data, children, sel)
}
return vnode(sel, data, children, text, undefined)
};
// 返回的是vnode

// vnode函数如下
export function vnode (sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
const key = data === undefined ? undefined : data.key
return { sel, data, children, text, elm, key }
}

// patch函数在init函数中,对比两者是否相同
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node
const insertedVnodeQueue: VNodeQueue = []
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode)
}

if (sameVnode(oldVnode, vnode)) {
// key和sel都相等,则是相同的vnode,相同的vnode进行对比
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
elm = oldVnode.elm!
parent = api.parentNode(elm) as Node
// 不相等直接删掉重建
createElm(vnode, insertedVnodeQueue)

if (parent !== null) {
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
removeVnodes(parent, [oldVnode], 0, 0)
}
}

for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
return vnode
}

// emptyNodeAt函数
function emptyNodeAt (elm: Element) {
const id = elm.id ? '#' + elm.id : ''
const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm)
}
// sameVnode函数
function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
// key 和 sel 都相等,则是相同的 vnode,如果都不传 key,undefined === undefined // true
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}

// 两者相同,进行对比 => patchVnode函数
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
// 执行 prepatch hook
const hook = vnode.data?.hook
hook?.prepatch?.(oldVnode, vnode)

// 设置 vnode.elem
const elm = vnode.elm = oldVnode.elm!

// 旧的 children
const oldCh = oldVnode.children as VNode[]
// 新的 children
const ch = vnode.children as VNode[]
// 相等直接返回
if (oldVnode === vnode) return
if (vnode.data !== undefined) {
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
vnode.data.hook?.update?.(oldVnode, vnode)
}

// 新的 vnode.text === undefined(vnode.children一般有值), 意味着 vnode.children != undefined,text和children不共存
if (isUndef(vnode.text)) {
// 新旧都有 children
if (isDef(oldCh) && isDef(ch)) {
// updateChildren 核心函数
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
// 新 children 有,旧 children 无,有可能旧有 text
} else if (isDef(ch)) {
// 清空 text
if (isDef(oldVnode.text)) api.setTextContent(elm, '')
// 添加 children
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 旧有,新无,移除
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 旧 text 有,清空
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '')
}
// vnode.text !== undefined(vnode.children无值)
} else if (oldVnode.text !== vnode.text) {
if (isDef(oldCh)) {
// 移除旧的children
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
// 设置新的 text
api.setTextContent(elm, vnode.text!)
}
hook?.postpatch?.(oldVnode, vnode)
}

// 移除
function removeVnodes (parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number): void {
for (; startIdx <= endIdx; ++startIdx) {
let listeners: number
let rm: () => void
const ch = vnodes[startIdx]
if (ch != null) {
if (isDef(ch.sel)) {
invokeDestroyHook(ch) // hook操作
// 移除 dom 元素
listeners = cbs.remove.length + 1
rm = createRmCb(ch.elm!, listeners)
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
const removeHook = ch?.data?.hook?.remove
if (isDef(removeHook)) {
removeHook(ch, rm)
} else {
rm()
}
} else { // Text node
api.removeChild(parentElm, ch.elm!)
}
}
}
}

// 新旧都有 children ,更新 children,体现了key的重要性
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx: KeyToIndexMap | undefined
let idxInOld: number
let elmToMove: VNode
let before: any

while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
// 开始和开始
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// 最后和最后
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// 开始和结束对比
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 结束和开始对比
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 以上四个都未命中
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
// 拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
idxInOld = oldKeyToIdx[newStartVnode.key as string]
// 没对应上
if (isUndef(idxInOld)) { // New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
// 对应上了
elmToMove = oldCh[idxInOld]
// 看 sel 是否相等(sameVnode 的条件)
if (elmToMove.sel !== newStartVnode.sel) {
// New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
// sel 相等,key 相等,=> patchVnode
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined as any
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
}

diff算法总结

  • patchVnode
  • addVnodes removeVnodes
  • updateChildren(key的重要性)

vdom和diff总结

  • vdom核心概念很重要:h、vnode、patch、diff过程、key等
  • vdom存在的价值更重要:数据驱动视图,控制DOM操作

模板编译

  1. 模板是vue开发中最常用的部分,即与使用相关联的原理
  2. 它不是html,有指令、插值、JS表达式,到底是什么
  3. 面试不会直接问,但会通过“组件渲染和更新过程”考察
  • 前置知识: js 的 with 语法
  • vue template complier 将模板编译为 render 函数
  • 执行 render 函数生成 vnode

with语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = {a: 100,b: 200}

console.log(obj.a)
console.log(obj.b)
console.log(obj.c) // undefined

// 使用 with,能改变 {} 内自由变量的查找方式
// 将 {} 内自由变量,当做 obj 的属性来查找

with(obj){
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) // 会报错
}
  • 改变 {} 内自由变量的查找规则,当做 obj 属性来查找
  • 如果找不到匹配的 obj 属性,就会报错
  • with 要慎用,它打破了作用域规则,易读性变差

编译模板

  • 模板不是 html ,有指令、插值、js表达式,能实现判断、循环
  • html 是标签语言,只有js才能实现判断、循环(图灵完备的)
  • 因此,模板一定是转换为某种js代码,即编译模板

代码演示

1
2
3
# 初始化一个npm环境
npm init -y
npm i vue-template-compiler --save

在项目中新建 index.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
const compiler = require('vue-template-compiler')

// 插值
const template = '<p>{{message}}</p>'
// function(){with(this){return _c('p',[_v(_s(message))])}}
// h('p', {}, [...]) 函数,赶回一个vnode,createElement返回也是一个vnode
// _c 是 createElement, _v 是 createTextVNode, _s 是 toString
// const vm = new Vue({...})

const template = `<p>{{flag ? message : 'no message found'}}</p>`
// with(this){return _c(
// 'p',
// [
// _v(
// _s(flag ? message : 'no message found')
// )
// ]
// )}


// 属性和动态属性
const template = `
<div id="div1" class="container">
<img :src="imgUrl" />
</div>
`
// with(this){
// return _c(
// 'div',
// {
// staticClass:"container",
// attrs:{
// "id":"div1"
// }
// },
// [
// _c(
// 'img',
// {
// attrs:{
// "src":imgUrl
// }
// }
// )
// ])
// }

// 条件
const template = `
<div>
<p v-if="flag === 'a'">A</p>
<p v-else>B</p>
</div>
`
// 使用了三元表达式
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}

// 循环
const template = `
<ul>
<li v-for="item in list" :ley="item.id">{{item.title}}</li>
</ul>
`
// _l = renderList
// with(this){return _c('ul',_l((list),function(item){return _c('li',{attrs:{"key":item.id}},[_v(_s(item.title))])}),0)}

// 事件
const template = `
<button @click="clickHandler">submit</button>
`
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}

// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
// with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:{"input":function($event){if($event.target.composing)return;name=$event.target.value}}})}

// 编译
const res = compiler.compile(template)
console.log(res.render)

使用命令行执行,打印

1
vuetemplate$ node index.js

小结

  • 模板编译为 render 函数,执行 render 函数返回 vnode
  • 基于 vnode 再执行 patch 和 diff
  • 使用 webpack vue-loader,会在开发环境下编译模板

vue组件中使用render代替template

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Vue.component('heading',{
render: function(createElement){
return createElement(
'h' + this.level,
[
createElement('a', {
attrs: {
name: 'headerId',
href: '#' + 'headerId'
}
}, 'this is a tag')
]
)
}
})
  • 学完模板编译,render 更好理解
  • 有些复杂情况中,不能用 template,可以考虑使用 render
  • react 一直用 render,没有模板

总结

  1. witn 语法
  2. 模板到 render 函数,再到 vnode,再到渲染和更新
  3. vue 组件可以使用 render 代替 template

组件 渲染/更新 过程

  1. 一个组件渲染到页面,修改 data 触发更新(数据驱动视图)
  2. 其背后原理是什么,需要掌握哪些要点?
  3. 考察对流程了解的全面度

回顾

  1. 响应式:监听 data 属性 getter setter (包括数组)
  2. 模板编译:模板到 render 函数,再到 vnode
  3. vdom:patch(elem,vnode) 和 patch(vnode,newVnode)

过程

初次渲染过程

  1. 解析模板为 render 函数(或在开发环境已完成,vue-loader)
  2. 触发响应式,监听 data 属性 getter setter
  3. 执行 render 函数,模板用到的 data 会触发 getter,生成 vnode, patch(elem,vnode)

更新过程

  1. 修改 data,触发 setter(此前在 getter 中已被监听)
  2. 重新执行 render 函数,生成 newVnode
  3. patch(vnode, newVnode)

异步渲染

  1. 回顾 $nextTick
  2. 汇总 data 的修改,一次性更新视图
  3. 减少 DOM 操作次数,提高性能

总结

  • 渲染和响应式的关系
  • 渲染和模板编译的关系
  • 渲染和 vdom 的关系

vue常见性能优化方式

  1. 合理使用 v-if v-show
  2. 合理使用 computed
  3. v-for 时加 key,以及避免和 v-if 同时使用( v-for 优先级更高,每次 v-if 都要遍历一遍)
  4. 自定义事件、DOM事件及时销毁
  5. 合理使用异步组件
  6. 合理使用 keep-alive
  7. data 层级不要太深
  8. 使用 vue-loader 在开发环境做模板编译(预编译)
  9. webpack 层面的优化
  10. 前端通用的性能优化,如图片懒加载
  11. 使用 SSR

Proxy

Object.defineProperty 的缺点

  • 深度监听需要一次性递归
  • 无法监听新增属性/删除属性(Vue.set Vue.delete)
  • 无法原生监听数组,需要特殊处理

Proxy实现响应式

基本使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const data ={
name: 'zhangsan',
age: 18
}
const proxyData = new Proxy(data, {
get(target, key, receiver){
const result = Reflect.get(target, key, receiver)
console.log('get', key)
return result // 返回加过
},
set(target, key, val, receiver){
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
return result // 是否设置成功
},
deleteProperty(target, key){
const result = Reflect.deleteProperty(target, key)
console.log('deleteProperty', key)
return result // 是否删除成功
}
})

demo.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
// const data = {
// name: 'zhangsan',
// age: 20,
// }
const data = ['a', 'b', 'c']

const proxyData = new Proxy(data, {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
// ownKeys 是所有 key
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
return result // 返回结果
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
})

Reflect

  • 和 Proxy 一一对应
  • 规范化、标准化、函数式
1
2
3
4
5
const obj = { 'a': 10, 'b': 20 }
console.log( 'a' in obj ) // true
console.log( Reflect.has(obj, 'a') ) // true
console.log( delete obj.a ) // true
console.log( Reflect.deleteProperty(obj, 'b') ) // true
  • 替代 Object 上的工具函数
1
2
Object.getOwnPropertyNames(obj)  // ['a', 'b']
Reflect.ownKeys(obj) // ['a', 'b']

实现响应式

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
// 创建响应式
function reactive(target = {}) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则返回
return target
}
// 代理配置
const proxyConf = {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
// 深度监听
// 性能如何提升的?
// 什么时候 get 什么时候递归,获取到那层才会触发响应式,不获取就不触发那层,object是全部一次行递归
return reactive(result)
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
const ownKeys = Reflect.ownKeys(target)
// 判断是不是新增的 key
if (ownKeys.includes(key)) {
console.log('已有的 key', key)
} else {
console.log('新增的 key', key)
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}

// 测试数据
const data = {
name: 'zhangsan',
age: 20,
info: {
city: 'beijing',
a: {
b: {
c: {
d: {
e: 100
}
}
}
}
}
}

const proxyData = reactive(data)
  • 深度监听,性能更好
  • 可监听 新增/删除 属性
  • 可监听数组变化

总结

  • Proxy 能规避 Object.defineProperty 的问题
  • Proxy 无法兼容所有浏览器,无法 ployfill