vue常见原理总结
# vue常见原理
- 编译原理
- 响应式原理,依赖收集
- 组件化开发(贯穿vue的流程)
- diff算法
# 模板编译过程概述
- 将模板编译成抽象语法树 抽象语法树是什么样的可以在这里查看 (opens new window)选择vue选项即可 解析模板通过正则表达式获取标签、属性、表达式等,当遇到开始标签放入栈中,遇到结束标签则出栈,拼接成AST抽象语法树,然后通过优化器遍历AST进行标记静态节点,主要用来做虚拟DOM的渲染优化。
- 通过generate根据抽象语法树生成代码生成render函数vue template compiler将模板编译为render函数,执行render 函数生成vnode。基于vnode进行patch和diff。
使用vue-loader在开发环境下编译模板。
width函数
const obj = { a: 100, b: 200}
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) // undefined
with(obj){
console.log(a);
console.log(b);
console.log(c); // 会报错
}
// with语法改变自由变量的查找规则,当做obj属性来查找,如果调用了查找不到的属性会报错
// with打破了作用域的规则,易读性变差
2
3
4
5
6
7
8
9
10
11
12
13
14
引入vue-template-compiler
编译字符串模板 在index.js中写入模板和输出,在terminal中执行node index.js即可查看输出
// 插值
const template = `<p>{{message}}</p>`
// 编译后的结果是
// with(this){return _c('p',[_v(_s(message))])}
// 替换成函数
// with(this){return createElement('p',[createTextVNode(toString(message))])}
// // 表达式
// 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" :key="item.id">{{item.title}}</li>
// </ul>
// `
// with(this){return _c('ul',_l((list),function(item){return _c('li',{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}}})
// }
// 在模板渲染阶段v-model已经被挂载上了onInput的事件来更新数据,每次更新触发渲染
// 编译
const res = compiler.compile(template)
console.log(res.render)
// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
// target._o = markOnce;
// target._n = toNumber;
// target._s = toString;
// target._l = renderList;
// target._t = renderSlot;
// target._q = looseEqual;
// target._i = looseIndexOf;
// target._m = renderStatic;
// target._f = resolveFilter;
// target._k = checkKeyCodes;
// target._b = bindObjectProps;
// target._v = createTextVNode;
// target._e = createEmptyVNode;
// target._u = resolveScopedSlots;
// target._g = bindObjectListeners;
// target._d = bindDynamicKeys;
// target._p = prependModifier;
// }
// this._c = (a, b, c, d) => createElement(contextVm, a, b, c, d, needNormalization)
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
# 响应式
实现原理:
通过Object.defineProperty
来监听数据的改变,当数据变更后触发set更新视图。当遇到object对象时继续深度遍历实现对象的深度监听,当遇到数组时重新定义数组对象,扩展新的方法,实现数据的监听
Object.defineProperty
的缺点:
- 深度监听需要一次性递归到底,计算量大。如果数据太大会卡。
- 无法监听新增属性/删除属性(可通过vue.set, vue.delete解决)
- 无法原生监听数组,需要特殊处理
// 触发更新视图
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) // 监听数组
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
vue2.x
- 核心api - object.defineProperty
- 在监听对象需要深度遍历和监听数组时需要对数组属性做特殊处理,处理数组数据重新定义数组原型触发视图更新
- 复杂对象的深度监听会有问题,因为处理监听时需要递归到底,一次性计算量大
- 无法监听新增、删除属性(vue.set、vue.delete)
vue3
- 核心api proxy 、reflect
- 深度监听性能更好
- 可监听 新增、删除属性
- 可监听数组变化
- proxy可规避object.defineProperty的问题,但proxy无法兼容所有浏览器
// proxy创建响应式
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)
// 深度监听
// 性能如何提升的?
return reactive(result)
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
const ownKeys = Reflect.ownKeys(target)
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)
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
# vdom
vdom是用js模拟dom结构,计算出最小的变更,操作dom
为什么会有vdom?
- 正常的dom操作非常耗费性能,以前用jQuery操作dom全靠手动调整很费力。容易出错,不易于维护。
- 当程序有了一定的复杂度,想减少计算次数比较难,用js描述dom节点,计算最小的变更再操作dom性能更高。
- vdom可以实现数据驱动视图,控制dom操作
<!-- html -->
<div id="div1" class="container">
<p>vdom</p>
<ul style="font-size:20px">
<li>a</li>
</ul>
</div>
2
3
4
5
6
7
// 翻译成vdom
{
tag: 'div',
props: {
className: 'container',
id: 'div1'
},
children: [
{
tag: 'p',
children: 'vdom'
},
{
tag: 'ul',
props: {
style: 'font-size:20px'
},
children: [{
tag: 'li',
children: 'a',
}]
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# diff算法概述
下面是snabbdom的实现原理
- diff算法是vdom的核心部分。
- diff算法是一个比较广泛的概念,如:linux diff、git diff
- 两个js对象也可以做diff,两个树也能做diff
- vue dom diff主要是同一层级做比较,tag不同则直接删除掉重建,不再深入比较,tag与key相同则认为是相同节点(sameVnode的条件)。
- 不使用key的dom更新时直接删除重新建,如果使用key可以通过判断key与tag的匹配度来确认保留还是新建。当在patch中确定为相同的vnode后会执行patchVnode
- patchVnode。新旧都有children进行updateChildren。新旧children是否有值,旧的有,新的无则删除节点(revmoeVnodes)。如果新有旧无,则新建(addVnodes)
- updateChildren 规则是 开始和开始对比,结束和结束对比,开始和结束对比,结束和开始对比。如果都未命中,用新节点的key能否对应旧节点的某个key,没对就上建新节点,对就上判断tag是否相同,不相同时创建新节点,相同时patchVnode。 这里面体现了key的重要性。
# 组件的渲染更新过程
- 初次渲染
- 解析模板为render函数
- 绑定响应式,通过getter setter 监听data属性
- 执行render函数,生成vnode, 然后patch(elem,vnode)
注
执行render函数会触发getter
- 更新过程
- 修改data触发setter(此前已被收集监听的getter)
- 重新执行render函数,成生newVnode
- patch(vnode,newVnode)
流程图
在render函数执行时会touch触发data中的getter,触发后会收集依赖(触发了哪个变量的getter,就会观察哪个变量),一旦修改data的时候触发re-render,然后重新渲染,重新生成virtual dom tree
- 异步渲染
- $nextTick待dom渲染完再回调。
- 页面渲染时会将data的修改做整合,多次data修改只会渲染一次。
- 减少dom操作次数,提高性能。
注
注: ajax请求要放到mounted中。vue本身不支持ajax请求,需要使用vue-resource、axios等插件实现。一个组件的created比mounted早调用不了几微秒。放到created性能提高不了多少,而且等异步渲染的时候,create可能被中途打断,中断之后渲染又要重做一遍,在created中做ajax调用,代码里只有一次调用,但实际上可能是n次调用。如果将ajax调用放到mounted阶段,不会有重复的调用,更合适。
# 将props传递子组件
<User v-bind="$Props"/>
因为在React的的高阶组件用的时候,最好将props全部传递给调用的父组件。所以vue这里也提一下。
# hash模式
- hash变化会触发浏览器的跳转,但不会刷新页面
- hash变化永远不会提交到服务端
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
console.log('old url', event.oldURL)
console.log('new url', event.newURL)
console.log('hash:', location.hash)
}
// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
console.log('hash:', location.hash)
})
// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
location.href = '#/user'
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# hisotry模式
history跳转时不刷新页面,主要通过window.pushState和window.onpopState来实现
因为对于所有路径都会返回 index.html 文件。为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面。
// 页面初次加载获取hash
document.addEventListener('DOMContentLoaded', () =>{
console.log('loaded',location.pathname)
})
// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
const state = { name: 'page1' }
console.log('切换路由到', 'page1')
history.pushState(state, '', 'page1') // 重要!!
})
// 监听浏览器前进、后退
window.onpopstate = (event) => { // 重要!!
console.log('onpopstate', event.state, location.pathname)
}
// 每次请求服务器都要保证返回index.html页面,然后端端通过onpopstate来控制和监听页面跳到
// 哪里才不会出现页面丢失的现象
// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在vue中实现vue-router需要通过组件的install方法,在方法里面可能通过调用object.defineProperty注册全局$route。通过Vue.component注册routerLink和routerView组件。
import View from './components/view'
import Link from './components/link'
export let _Vue
export function install (Vue) {
if (install.installed && _Vue === Vue) return
install.installed = true
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
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
# hash与hisotry
- hash模式url会出现
- history模式需要服务端的配合
- toB的系统推荐用hash,简单易用不需要server配合,对url不敏感
- toC的系统考虑h5 history,但需要服务端支持,history有利于seo,因为搜索引擎对于#后面的内容(锚)点一般是不收录的。