量变产生质变
前言
工作两年多,作为一个前端,越来越感觉到知识储备匮乏,遇到问题百度后记忆也不会深刻,遂打算写这个博客,不只是为了应对面试题,更重要的是能在自己当前能理解的深度上去分析面试题中所涉及到的知识。
1. 写 React / Vue 项目时为什么要在列表组件中写 key,其作用是什么?
vue的官方文档的描述如下:key 的特殊属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes对比时辨识VNodes。如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试修复/再利用相同类型元素的算法。使用 key,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。
React的官方文档的描述如下: key 帮助React识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。 一个元素的 key 最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用来自数据 id 来作为元素的 key vue和react都是采用diff算法来对比新旧虚拟节点,从而更新节点。在vue的diff函数中。(先了解一下diff算法吧)
key的值加与不加有什么区别呢
没有绑定key的情况下,并且在遍历模板简单的情况下,会导致虚拟新旧节点对比更快,节点也会复用。而这种复用是就地复用,一种鸭子辩型的复用。以下为简单的例子:
<div id="app">
<div v-for="i in dataList">{{ i }}</div>
</div>
复制代码
var vm = new Vue({
el: '#app',
data: {
dataList: [1, 2, 3, 4, 5]
}
})
复制代码
v-for的内容会生成以下的dom节点数组,我们给每一个节点标记一个身份id:
[
'<div>1</div>', // id: A
'<div>2</div>', // id: B
'<div>3</div>', // id: C
'<div>4</div>', // id: D
'<div>5</div>' // id: E
]
复制代码
我们对dataList做不同的操作
- 改变dataList数据,进行数据位置替换,对比改变后的数据
vm.dataList = [4, 1, 3, 5, 2] // 数据位置替换
// 没有key的情况, 节点位置不变,但是节点innerText内容更新了
[
'<div>4</div>', // id: A
'<div>1</div>', // id: B
'<div>3</div>', // id: C
'<div>5</div>', // id: D
'<div>2</div>' // id: E
]
// 有key的情况,dom节点位置进行了交换,但是内容没有更新
// <div v-for="i in dataList" :key='i'>{{ i }}</div>
[
'<div>4</div>', // id: D
'<div>1</div>', // id: A
'<div>3</div>', // id: C
'<div>5</div>', // id: E
'<div>2</div>' // id: B
]
复制代码
- 增删dataList列表项
vm.dataList = [3, 4, 5, 6, 7] // 数据进行增删
// 1. 没有key的情况, 节点位置不变,内容也更新了
[
'<div>3</div>', // id: A
'<div>4</div>', // id: B
'<div>5</div>', // id: C
'<div>6</div>', // id: D
'<div>7</div>' // id: E
]
// 2. 有key的情况, 节点删除了 A, B 节点,新增了 F, G 节点
// <div v-for="i in dataList" :key='i'>{{ i }}</div>
[
'<div>3</div>', // id: C
'<div>4</div>', // id: D
'<div>5</div>', // id: E
'<div>6</div>', // id: F
'<div>7</div>' // id: G
]
复制代码
从以上来看,不带有key,并且使用简单的模板,基于这个前提下,可以更有效的复用节点,diff速度来看也是不带key更加快速的,因为带key在增删节点上有耗时。这就是vue文档所说的默认模式。但是这个并不是key作用,而是没有key的情况下可以对节点就地复用,提高性能。
但这种模式会带来一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。VUE文档也说明了 这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出
比如,一个新闻列表,可点击列表项来将其标记为"已访问",可通过tab切换“娱乐新闻”或是“社会新闻”。不带key属性的情况下,在“娱乐新闻”下选中第二项然后切换到“社会新闻”,"社会新闻"里的第二项也会是被选中的状态,因为这里复用了组件,保留了之前的状态。要解决这个问题,可以为列表项带上新闻id作为唯一key,那么每次渲染列表时都会完全替换所有组件,使其拥有正确状态。
但是key的作用是什么?
key是给每一个vnode的唯一id,可以依靠key,更准确,更快的拿到oldVnode中对应的vnode节点。
- 更准确
因为带key就不是就地复用了,在 ameNode函数 a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。
- 更快
利用key的唯一性生成map对象来获取对应节点,比遍历方式更快。
2. js中循环遍历方法对比
查了一下迭代和遍历的区别,然后找到了下面这段解释
- 循环(loop),指的是在满足条件的情况下,重复执行同一段代码。比如,while,for,do while语句。
- 迭代(iterate),指的是按照某种顺序逐个访问列表中的每一项。比如,for语句。
- 遍历(traversal),指的是按照一定的规则访问树形结构中的每个节点,而且每个节点都只访问一次。
- 递归(recursion),指的是一个函数不断调用自身的行为。比如,以编程方式输出著名的斐波纳契数列。
好了,做正事了
- forEach 就是让数组中的每一项做一件事件,会改变原数组
var arr = [{a: 1}, {a: 2}, {a: 3}]
arr.forEach((item) => {
item.a = 12
})
console.log(arr) // [{a: 12}, {a: 12}, {a: 12}]
复制代码
- map 就是让数组通过某种计算,生成一个新数组,不改变原数组
var arr = [1, 2, 3]
var newArr = arr.map((item, index) => {
return item * 2
})
console.log(arr) // [1, 2, 3]
console.log(newArr) // [2, 4, 6]
复制代码
- filter 就是筛选出数组中符合条件的项,返回新数组,不改变原数组
var arr = [1, 2, 3]
var newArr = arr.filter((item, index) => {
return item > 1
})
console.log(arr) // [1, 2, 3]
console.log(newArr) // [2, 3]
复制代码
- reduce 就是让数组中前项和后项做某种计算,并返回累加的值
var arr = [1, 2, 3]
var res = arr.reduce((prev, next) => {
return prev*2 + next
})
console.log(arr) // [1, 2, 3]
console.log(res) // 1*2 + 2 + 2*2 + 3 = 11
复制代码
- for-in 就是遍历对象中可枚举的属性,for-in会把继承链的对象属性都会遍历一遍,所以会更花时间.
var obj = {
name: 'z',
age: 1,
do: function() {
console.log('something')
}
}
for(var key in obj) {
console.log(key) // name -> age -> do
}
复制代码
- for-of 语句只遍历可迭代对象的数据
var arr = [{
name: 'z',
do: function() {
console.log('something')
}},
'2',
3
]
for(item of arr) {
console.log(item) // {name: 'z', do: f} -> '2' -> 3
}
复制代码
这里for循环就不举例了,还有some和evey方法(这两个不完全属于数组操作方法),各种循环的速度也是不同的,下面是按速度排序的
for > for-of > forEach > filter > map > for-in
但是哪个快和应该使用哪个,并不应该划等号。
如果你需要将数组按照某种规则映射为另一个数组,就应该用 map。
如果你需要进行简单的遍历,用 forEach 或者 for of。
如果你需要对迭代器进行遍历,用 for of.
如果你需要过滤出符合条件的项,用 filter.
如果你需要先按照规则映射为新数组,再根据条件过滤,那就用一个 map 加一个 filter。不要担心这样会慢,你那点数据量浏览器根本不 care。
3. 防抖和节流
- 防抖
// 防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
function debounce (fn, delay) {
let timer
return function (...arg) {
// 如果执行期间再次触发,则清空,重新执行
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arg)
}, delay)
}
}
// 需要执行的函数
function say(context) {
console.time()
console.log('hi:'+ context + ' time: ' + new Date())
console.timeEnd()
}
var input = document.querySelector('#input')
var debounceFn = debounce(say, 1000)
input.addEventListener('keyup', (e) => {
debounceFn(e.target.value)
})
复制代码
防抖就像技能点,按一次施放后就进入冷却时间,在冷却时间内再次点按则会重置冷却时间
- 节流
// 节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效
function throttle(fn, threshhold) {
var timeout
var start = +new Date;
var threshhold = threshhold || 160 // 不给定间隔时间则取默认值
return function () {
var that = this, args = arguments, curr = +new Date()
clearTimeout(timeout)//总是干掉事件回调
// 如果当前时间 - 开始时间 >= 间隔时间
if (curr - start >= threshhold) {
console.log("now = ", curr,'time = ', curr - start)
fn.apply(that, args) //只执行一部分方法,这些方法是在某个时间段内执行一次
start = curr // 重置开始时间
} else {
//让方法在脱离事件后也能执行一次
timeout = setTimeout(function () {
console.log('else = ', curr,'time = ', curr - start)
fn.apply(that, args)
}, threshhold);
}
}
}
var mousemove = throttle(function (e) {
console.log(e.pageX, e.pageY)
}, 2000);
// 绑定监听
document.querySelector("#panel").addEventListener('mousemove', mousemove);
复制代码
运行结果
节流就像水坝,虽然不能让水不流,但是可以减缓水流的速度
总结
函数防抖和函数节流都是防止某一时间频繁触发,函数防抖是某一段时间内只执行一次,而函数节流是间隔时间执行。
应用场景
- debounce
- search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
- window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次
- throttle
- 鼠标不断点击触发,mousedown(单位时间内只触发一次)
- 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断。
复制代码