组件化基础
- 传统组件,只是静态渲染,更新还要依赖于操作
DOM
Vue
数据驱动视图:Vue MVVM
React
数据驱动视图:React setState
如何理解 MVVM
数据驱动视图:不再操作dom
,需要更新视图就去修改数据,使开发者更关注数据,复杂度变低。
第一个M-Model
,第二个V-View
,第三个VM-ViewModel
。
出处:https://coding.imooc.com/lesson/419.html
Model
:vue
组件的data
。
View
和Model
通过ViewModel
来做关联,像监听事件、监听指令这一些操作。通过ViewModel
这一层,在Model
修改的时候,就可以立刻知道,然后执行View
的渲染;View
这一层如果有点击事件的操作,也可以立刻知道,然后去修改Model
的数据。
Vue 响应式
组件data
的数据一旦变化,立刻触发视图的更新。这是实现数据驱动视图的第一步。
核心 API - Object.defineProperty
Object.defineProperty
有一些缺点,Vue3.0
启用Proxy
来实现响应式。
Proxy
有兼容性问题,无法polyfill
。
// Object.defineProperty 基本用法
const data = {};
let name = 'zhangsan';
Object.defineProperty(data, 'name', {
get: function () {
console.log('get');
return name;
},
set: function (newVal) {
console.log('set');
name = newVal;
}
});
// 测试
console.log(data.name); // get zhangsan
data.name = 'lisi'; // set
如何深度监听 data 的变化
- 监听对象
- 监听数组
- 复杂对象,深度监听
// 触发更新视图
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 如何监听数组变化
- 重新定义数组原型
// 重新定义数组原型
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);
}
});
Object.defineProperty 的缺点
- 深度监听,需要递归到底,一次性计算量很大
- 无法监听新增属性、删除属性(所以需要
Vue.set
和Vue.delete
这两个API
去做这个事情) - 无法原生监听数组,需要特殊处理
虚拟 DOM 和 diff
vdom
是实现 Vue
和 React
的重要基石。
diff
算法是vdom
中最核心、最关键的部分。
背景
dom
操作非常耗费性能。
以前用jQuery
,可以自行控制DOM
操作的时机,手动调整。
vue
和react
是数据驱动视图,如何有效控制dom
操作?
有了一定复杂度,想减少计算次数比较难,就想能不能把计算更多的转移为js
计算,因为js
执行速度很快。
解决方案-vdom
。
用 JS 来模拟 DOM 结构 - vnode
<div id ="div1" class="container">
<p>vdom</p>
<ul style="font-size: 20px">
<li>a</li>
</ul>
</div>
{
tag: 'div',
props: {
className: 'container',
id: 'div1'
},
children: [
{
tag: 'p',
props: null,
children: 'vdom'
},
{
tag: 'ul',
props: {
style: 'font-size: 20px'
},
children: [
{
tag: 'li',
props: null,
children: 'a'
}
]
}
]
}
通过 snabbdom 学习 vdom
简介强大的vdom
库,易学易用。Vue
参考它实现的vdom
和diff
。
github
地址:https://github.com/snabbdom/snabbdom
h 函数
首先通过 h
函数,返回 vnode
。
出处:https://coding.imooc.com/lesson/419.html
patch 函数
然后通过 patch
函数将 vnode
挂载到真实 dom
上。(初次渲染:将虚拟 dom
真正渲染到节点上)
再通过 patch
函数将新的 vnode
给旧的 vnode
做更新。(dom
更新)
最后将新的 vnode
清空。
出处:https://coding.imooc.com/lesson/419.html
vdom 总结
- 用
js
模拟dom
结构(vnode
) - 新旧
vnode
对比,得出最小的更新范围,最后更新dom
这样在数据驱动视图的模式下,可以有效控制 dom
操作。
diff 算法
diff
算法是 vdom
中最核心、最关键的部分。
diff
算法能在日常使用 vue
、react
中体现出来,如 key
。
diff
即对比,是一个广泛的概念,如 linux diff
命令(对比两个文件的差异)、git diff
等。
两个 js
对象也可以做 diff
,如 https://github.com/cujojs/giff。
两棵树做 diff
,如这里的 vdom diff
。
出处:https://coding.imooc.com/lesson/419.html
以上 树 diff
的复杂度 O(n^3)
:
第一,遍历tree1
第二,遍历tree2
第三,排序
优化时间复杂度到O(n)
:
- 只比较同一层级,不跨级比较
tag
不相同,直接删掉重建,不再深度比较tag
和key
两者都相同,则认为是相同节点,不再深度比较。
diff 算法的对比过程(snabbdom 的对比方式)
patch
函数
patch(vnode, newVnode);
- 第一个参数不是
vnode
,就创建一个空的vnode
,关联到这个dom
元素。 - 如果是相同的
vnode
(key
和tag
都相同),就执行patchVnode
。 - 如果是不同的
vnode
,删掉重建。
patchVnode
:addVnodes
、removeVnodes
patchVnode(oldVnode, vnode, insertedVnodeQueue);
- 新的如果有
children
:- 新旧都有
children
:执行updateChildren
深度比较。 - 新的有
children
,旧的无children
:添加vnode
(addVnodes
进行添加dom
操作)。 - 旧的有
children
,新的无children
:移除vnode
(removeVnodes
进行删除dom
操作)。 - 旧的
text
有值:清空text
。
- 新旧都有
- 新的如果无
children
:- 新旧
text
不相同,移除,重新设置内容。
- 新旧
updateChildren
(key
的重要性)
出处:https://coding.imooc.com/lesson/419.html
- 开始和开始对比:执行
sameVnode
对比 - 结束和结束对比:执行
sameVnode
对比 - 开始和结束对比:执行
sameVnode
对比 - 结束和开始对比:执行
sameVnode
对比 - 其他
- 拿新节点的
key
,看能否对应上oldChildren
中的某个节点的key
key
没对应上:新增dom
key
对应上了:再判断tag
是否相等(sameVnode
的条件)tag
不相等:新增dom
tag
相等:执行patchVnode
函数
- 拿新节点的
出处:https://coding.imooc.com/lesson/419.html
模板编译
JS 的 with 语法
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(a);
console.log(b);
console.log(c); // 会报错!!!
}
- 改变
{}
内自由变量的查找规则,当作obj
属性来查找。 - 如果找不到匹配的
obj
属性,就会报错。
with
要慎用,它打破了作用域规则,易读性变差。
vue 模板
模板不是html
,有指令、插值、JS
表达式,能实现判断、循环。
html
是标签语言,只有js
才能实现判断、循环(图灵完备的语言)。
因此,模板一定是转换为某种js
代码,即编译模板。
// package.json
{
"name": "vue-template-compiler-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"vue-template-compiler": "^2.6.10"
}
}
// index.js
const compiler = require('vue-template-compiler');
// 插值
const template = `<p>{{message}}</p>`;
// with(this){return createElement('p',[createTextVNode(toString(message))])}
// h -> vnode
// createElement -> vnode
// 表达式
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}}})}
// render 函数
// 返回 vnode
// patch
// 编译
const res = compiler.compile(template)
console.log(res.render)
// ---------------分割线--------------
// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
// target._c = createElement;
// 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;
// }
- 模板编译为
render
函数,执行render
函数返回vnode
- 基于
vnode
再执行patch
函数和diff
算法 - 使用
webpack vue-loader
,会在开发环境下编译模板
// vue 组件中可以使用 render 代替 template
Vue.component('heading', {
// template: 'xxx',
render: function(createElement) {
return createElement(
'h' + this.level,
[
createElement('a', {
attrs: {
name: 'headerId',
href: '#' + 'headerId'
}
}, 'this is a tag')
]
);
}
});
// 在有些复杂情况中,不能用 template,可以考虑用 render
// react 一直都用 render (没有模板)
总结:
vue template complier
将模板编译为render
函数- 执行
render
函数生成vnode
组件渲染/更新的过程
出处:https://coding.imooc.com/lesson/419.html
初次渲染的过程:
- 解析模板为
render
函数(或者使用vue-loader
在开发过程中就解析完成) - 触发响应式,监听
data
属性getter
、setter
- 执行
render
函数,生成vnode
,然后通过patch
函数将vnode
绑定到真实dom
(patch(elem, vnode)
)
// 执行 render 函数会触发 getter
<p>{{message}}</p>
<script>
export default {
data() {
return {
message: 'hello', // 会触发 get
city: '北京' // 不会触发 get,因为模板没用到,即和视图没关系
};
}
}
</script>
更新过程:
- 修改
data
,触发setter
(此前在getter
中已被监听) - 重新执行
render
函数,生成newVnode
- 再执行
patch
函数用新的vnode
去更新旧的vnode
(patch(vnode, newVnode)
)
异步渲染:
vue
是异步渲染的,汇总 data
的修改,一次性更新视图,目的是减少 dom
操作次数,提高性能。(react
也是异步渲染)
前端路由原理
稍微复杂一点的SPA
,都需要路由。
浏览器刷新指的是重新进行网页的http请求。
vue-router 的路由模式
hash
H5 history
网页 url 组成部分
// http://127.0.0.1:8881/01-hash.html?a=100&b=20#/aaa/bbb
location.protocol // 'http:'
location.hostname // '127.0.0.1'
location.host // '127.0.0.1:8881'
location.port // '8881'
location.pathname // '/01-hash.html'
location.search // '?a=100&b=20'
location.hash // '#/aaa/bbb'
hash 的特点
hash
变化会触发网页跳转,即浏览器的前进、后退hash
变化不会刷新页面,SPA
必需的特点hash
用于不会提交到server
端(前端自生自灭)- 使用
window.onhashchange
来实现
*[浏览器刷新 ]:指的是重新进行网页的http
请求。
<!-- hash.html -->
<!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>hash test</title>
</head>
<body>
<p>hash test</p>
<button id="btn1">修改 hash</button>
<script>
// 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)
}
// 谷歌有效,火狐无效
// window.onhashchange = (event) => {
// console.log(`old url:${event.oldURL}`)
// console.log(`new url:${event.newURL}`)
// console.log(`hash:${location.hash}`)
// }
// 谷歌、火狐浏览器都有效
// window.addEventListener('hashchange', (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'
})
</script>
</body>
</html>
H5 history
用 url
规范的路由,但跳转时不刷新页面。
主要通过 history.pushState
和window.onpopstate
来实现。
需要server
端支持。
正常页面浏览:
首次访问 https://github.com/xxx
刷新页面
点击跳转 https://github.com/xxx/yyy
刷新页面
点击跳转 https://github.com/xxx/yyy/zzz
刷新页面
改造成 H5 history
模式:
首次访问 https://github.com/xxx
刷新页面
点击跳转 https://github.com/xxx/yyy
前端跳转,不刷新页面
点击跳转 https://github.com/xxx/yyy/zzz
前端跳转,不刷新页面
<!-- index.html -->
<!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>history API test</title>
</head>
<body>
<p>history API test</p>
<button id="btn1">修改 url</button>
<script>
// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
console.log('load', 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)
}
// 需要 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
</script>
</body>
</html>
两者选择
to B
的系统推荐用hash
,简单易用,对url
规范不敏感to C
的系统,可以考虑选择H5 history
,但需要服务端支持
FQA
-
关于前端路由原理。
- 前端常用路由模式分为
hash
模式和H5 history
模式。 - 其中
hash
模式,可以作为SPA
,是因为#
后面的内容变化,浏览器不会刷新页面,并且服务器也不会收到请求,可以通过window.onhashchange
事件来感知这个动作。 - 另外
H5 history
模式,剔除了#
,直接请求就会刷新页面并发送到服务器,所以我们需要在服务器端进行设置,让它只识别“协议 + 端口 + path
”,斜杠后面的都忽略,这样我们只更换斜杠后面的内容,服务器也就不会认为是浏览器重新请求了页面。H5 history
模式需要使用window.onpopstate
来进行监听,使用history.pushState
来进行斜杠后面内容的更新。
- 前端常用路由模式分为
-
什么是虚拟
dom
和diff
算法?- 由于
DOM
操作比较耗时,所以虚拟DOM
技术的出现,减少了DOM
的操作次数,提升了浏览器的性能。所谓的虚拟DOM
就是使用JS
对象来模拟DOM
结构,当js
程序中需要对DOM
进行操作时,首先会修改虚拟DOM
,然后通过diff
算法对比新旧虚拟DOM
,最终使浏览器可以通过最少的DOM
操作,完成页面的更新。 - 而
diff
算法就是虚拟DOM
的核心,对比新旧DOM
时,有三个原则:第一,只进行同一层级的虚拟DOM
对比,不会跨级对比;第二、如果新旧虚拟DOM
的标签不一样,直接按照新的虚拟DOM
进行渲染;第三、当新旧DOM
的标签相同,并且key
也相同的时候,那么则认为这两个虚拟DOM
相同,不在对这两个虚拟DOM
进行深层次的对比。也正是由于这三条原则,使得diff
算法的时间复杂度为O(n)
。
- 由于
-
为什么
v-for
中需要使用key
?- 在
diff
算法中,如果没有key
值,则会对内容进行清空,并重新赋予新值; - 如果有
key
值的存在,那么会对新旧vnode
进行对比,如果在oldvnode
找到了与newvnode
相同的key
,如果内容没变则直接使用之前的真实dom
,如果内容变了,才生成新的真实dom
进行替换。这样可以减少渲染次数,提升渲染性能。 - 每个
key
唯一才能知道一个元素前后改变的位置,如果不是唯一的话元素可能会移动到其他元素的位置而不是这个元素新变化的位置。
- 在
-
既然可以用
window
去定义全局变量,为什么vue
里还有用vuex
呢?Vuex
不仅仅是全局的,它是能结合Vue
数据响应式特性的全局。window
上可以定义全局变量,但没有响应式。
-
v-show
和v-if
的区别。v-show
通过css
的display
控制显示和隐藏v-if
控制组件真正的渲染和销毁,而不是显示和隐藏
-
watcher
和watch
的区别。watcher
是响应式内部的监听,会监听组件data
属性.watch
是data
属性改变之后的一个回调函数。
-
描述
vue
的生命周期,以及父子组件生命周期的关系。
单个组件生命周期:- 初始化阶段:
beforeCreate、created
- 挂载阶段:
beforeMount、mounted
- 更新阶段:
beforeUpdate、updated
- 销毁阶段:
beforeDestroy、destroyed
父子组件生命周期的关系:
- 父组件初始化完成、子组件才初始化完成
- 子组件挂载完成、父组件才挂载完成
- 父组件开始更新、子组件开始更新、子组件更新完成、父组件才更新完成
- 父组件开始销毁、子组件开始销毁、子组件销毁完成、父组件才销毁完成
- 初始化阶段:
-
vue
组件如何通讯?- 父子组件之间通过
props
和this.$emit
- 兄弟组件之间通过自定义事件(
event.$on
、event.$emit
、event.$off
) - 所有组件之间可通过
vuex
- 父子组件之间通过
-
描述组件渲染和更新的过程。
- 解析模板为
render
函数(或者使用vue-loader
在开发过程中就解析完成) - 触发响应式,监听
data
属性getter
、setter
- 执行
render
函数,生成vnode
,然后通过patch
函数将vnode
渲染成真实dom
(patch(elem, vnode)
) - 修改
data
,触发setter
(此前在getter
中已被监听) - 会重新执行
render
函数,生成newVnode
- 再执行
patch
函数用新的vnode
去更新旧的vnode
(patch(vnode, newVnode)
),对比后渲染出来
- 解析模板为
-
双向数据绑定
v-model
的实现原理。- 将
input
元素的value
设置为this.name
- 绑定
input
事件,将$event.target.value
赋值到this.name
data
更新,会触发re-render
,从而实现双向绑定。
- 将
-
对 MVVM 的理解。
- 第一个字母
M
指的是Model
,也就是data;第二个字母V
指的是View
;最后的VM
指的是ViewModel
。 View
和Model
通过ViewModel
来做关联,像监听事件、监听指令这一些操作,通过ViewModel
这一层,在Model
修改的时候,就可以立刻知道,然后执行View
的渲染;View
这一层如果有点击事件的操作,也可以立刻知道,然后去修改Model
的数据。
- 第一个字母
-
ajax 请求应该放在哪个生命周期?
- 一般放在moounted
中执行。
- 页面初始化时,created
到mounted
的耗时非常短,所以ajax
放在created
里所能带来的优化效果,并不明显。而且js
是单线程的,ajax
是异步获取数据,拿到数据后也是等dom
渲染完成后才能去渲染数据,放在created
里,更早地获取到数据也没什么用。 -
为什么
vue
组件的data
必须是一个函数?- 因为
export
出去的vue
组件是一个class
,每次使用这个组件的时候是对这个类的实例化,在实例化的时候去执行这个data
,如果这个data
不是函数的话,那每个组件的data
都是一样的了,就共享了,如果是函数的话,那每次执行这个data
就不会相互影响。
- 因为
-
如何将组件所有
props
传递给子组件?$props
<User v-bind="$props" />
- 多个组件有相同逻辑,如何抽离?
mixin
- 但是它有一些缺点:代码可读性变差;命名可能导致冲突;可能出现多对多的关系,复杂度变高。
- 何时使用异步组件?
- 加载大组件的时候:比较编辑器、表格
- 路由异步加载:多路由,切换的时候可以异步加载
- 何时使用
keep-alive
?- 需要缓存组件,不需要重复渲染的时候使用:比如多个静态
tab
的页切换
- 需要缓存组件,不需要重复渲染的时候使用:比如多个静态
- 何时需要使用
beforeDestory
?- 解绑自定义事件,比如:
event.$off
- 清除定时器
- 解绑自定义的
dom
事件,比如:addEventListner
- 解绑自定义事件,比如:
- 什么是作用域插槽?
- slot 里面有一个数据,想把这个数据暴露给使用它的地方。
- 子组件的
<slot>
指定:slotData
将数据暴露出去;父组件通过v-slot
定义一个名字xxx
,然后通过xxx.slotData
获取到子组件暴露出去的数据。
vuex
中action
和mutation
的区别。action
中可以处理异步,mutation
不可以mutation
只能做原子操作action
可以整合多个mutation
vue-router
常用的路由模式hash
(默认)H5 history
(需要server
端支持)
- 如何配置
vue-router
异步加载?
-components
用import()
函数这种方式。
export default new VueRouter({
routes: [
{
path: '/',
component: () => import('./../components/Navigator')
},
{
path: '/feedback',
component: () => import('./../components/FeedBack')
}
]
});
- 请用
vdom
描述一个dom
结构。
<div id ="div1" class="container">
<p>vdom</p>
<ul style="font-size: 20px">
<li>a</li>
</ul>
</div>
{
tag: 'div',
props: {
className: 'container',
id: 'div1'
},
children: [
{
tag: 'p',
props: null,
children: 'vdom'
},
{
tag: 'ul',
props: {
style: 'font-size: 20px'
},
children: [
{
tag: 'li',
props: null,
children: 'a'
}
]
}
]
}
-
监听
data
变化的核心api
是什么?Object.defineProperty
- 对象的深度监听需要递归实现
- 数组的监听需要重新定义原型,重写
pop、posh
等方法
缺点:
- 深度监听,需要递归到底,一次性计算量很大
- 无法监听新增属性、删除属性(所以需要
Vue.set
和Vue.delete
这两个API
去做这个事情) - 无法原生监听数组,需要特殊处理
-
请描述响应式原理。
- 解析模板为
render
函数 - 触发响应式,通过
Object.defineProperty
监听data
属性getter
、setter
- 执行
render
函数,生成vnode
,然后通过patch
函数将vnode
渲染成真实dom
- 修改
data
,触发setter
- 会重新执行
render
函数,生成newVnode
- 再执行
patch
函数对比新旧vnode
后重新渲染
- 解析模板为
-
diff
算法的复杂度?O(n)
- 是因为它做了一些调整,第一,只比较同一层级,不跨级比较,第二,
tag
不相同,则直接删掉重建,第三,如果tag
和key
都相同,则认为是相同节点,不再深度比较。从而将O(n^3)
变成了O(n)
。
-
简述
diff
算法的过程。- 通过
patch
函数去对比新旧vnode
,如果是相同节点,也就是tag
和key
都相同,就去执行patchVnode
对比节点内容,使用addVnodes
和removeVnodes
进行dom
的添加和删除操作。在patchVnode
对比的过程中,如果新旧vnode
都有children
,就再执行updateChildren
进行深度比较。
- 通过
-
vue
常见性能优化方式。- 合理使用
v-show
和v-if
- 合理使用
computed
v-for
时加key
,以及避免和v-if
同时使用(因为v-for
优先级更高,每次循环v-if
都会重新计算,浪费性能)- 自定义事件、
dom
事件即时销毁 - 合理使用异步组件
- 合理使用
keep-alive
data
层级不要太深(因为响应式的深度监听需要一次性递归到底,递归的层级比较多,可能导致页面卡一下)- 使用
vue-loader
在开发环境下做模版编译(预编译) webpack
层级的优化- 前端通用性能的优化:图片懒加载
- 使用
SSR
- 合理使用