字节跳动北京国际化

1、雨水算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function rain(arr){
let l_m = [arr[0]];
let r_m = [arr[arr.length-1]];
let res = 0;
for(let i = 1;i < arr.length-1;i++){
if(l_m[l_m.length-1] < arr[i]){
l_m.push(arr[i])
}else{
l_m.push(l_m[l_m.length-1])
}
if(r_m[0] < arr[arr.length-i-1]){
r_m.unshift(arr[arr.length-i-1])
}else{
r_m.unshift(r_m[0]);
}
}
for(let i = 1;i < arr.length-1;i++){
let now = Math.min(l_m[i],r_m[i])-arr[i]
res += now>0?now:0;
}
return res;
}
console.log(rain([0,1,0,2,1,0,1,3,2,1,2,1]));
2、Vue.nextTick()的原理和用途

Vue实现响应式并不是数据发生变化之后DOM立刻变化,而是按一定的策略进行DOM的更新。

在Vue的文档中,说明Vue是异步执行的DOM更新的。

具体来说,异步执行的运行机制如下。

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
  2. 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在“任务队列”之中放置一个事件。
  3. 一旦“执行栈”中所有同步任务执行完毕,系统就会读取“任务队列”,看看里面有哪些事件。哪些对应的异步任务于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

简单来说,Vue在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。

1
2
3
4
5
6
7
8
9
//改变数据
vm.message = 'changed'

//想要立刻使用更新后的DOM。这样不行,因为设置message后DON还没更新
console.log(vm.$el.textContent) //并不会得到'changed'

Vue.nextTick(function(){
console.log(vm.$el.textContent) //可以得到'changed'
})

事件循环:

第一个tick,就是本次循环:首先会修改数据,这是同步任务。同一事件循环的所有的同步任务都在主线程上执行,形成一个执行栈,此时未涉及DOM。Vue开启一个异步队列,并缓冲在此事件循环中发生的所有数据改变。如果同一个watcher被多次触发,只会被推入到队列中一次。

第二个tick,就是下次更新循环:同步任务执行完毕,开始执行异步watcher队列的任务,更新DOM。Vue在内部尝试对异步队列使用原生的Promise.then和MessageChannel方法,如果执行环境不支持,会采用setTimeout(fn,0)代替。

第三个tick:也就是下次DOM更新循环结束之后,此时通过Vue.nextTick获取到改变后的DOM。通过setTImeout(fn,0)也可以同样获取到。

注意:在created和mounted阶段,如果需要操作渲染后的视图,也要使用nextTick方法。

mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted

3、会引起回流和重绘的情况,并且如何优化

大多数浏览器都会通过队列化修改并批量执行来优化重排过程。

浏览器会把所有会引起回流、重绘的操作都放入一个队列,等过一定的时间或者操作达到了一定的阈值,浏览器就会flush队列,进行一个批处理。这样让多次的回流和重绘变成一次回流重绘。
但是,当获取布局信息操作的时候,会强制浏览器提前flush队列,触发回流重绘返回信息,这种情况下,浏览器的优化就不起作用了。比如以下属性或方法:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight
  • scrollTop、scrollLeft、scrollWidth、scrollHeight
  • clientTop、clientLeft、clientWidth、clientHeight
  • getComputedStyle()
  • getBoundingClientRect

那么,除了浏览器的优化,我们还有什么方法可以来减少回流和重绘达到性能优化?

通过合并多次DOM样式的修改,来减少回流和重绘的发生次数。

利用className修改元素的class

const el = document.getElementById(‘test’);
el.className += ’ active’;

利用cssText合并样式修改操作

const el = document.getElementById(‘test’);
el.style.cssText += ‘border-left: 1px; border-right: 2px; padding: 5px;’;

4、如何实现一秒输出一个数字里的元素
1
2
3
4
5
6
7
8
9
10
11
12
function print(arr) {
for(let i = 0;i < arr.length;i++){
(function (i) {
setTimeout(()=>console.log(arr[i]),2000*i)
})(i)}
// for (let i=0; i<arr.length; i++) {
// setTimeout(() => console.log(arr[i]), 1000*i)
// }
}

let arr = [1,2,3,4]
print(arr);
5、手写一个双向绑定
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text" id ='inpName'>
姓名: <span id = 'spanName'><span>
<script>
let obj = {
name: ''
}
// let newObj = JSON.parse(JSON.stringify(obj));
// Object.defineProperty(obj,'name',{
// get(){
// return newObj.name;
// },
// set(val){
// if(val === newObj.name)return;
// newObj.name = val;
// observer();
// }
// })
//vue3.0的简易实现
obj = new Proxy(obj,{
get(target,prop){
return target[prop];
},
set(target,prop,value){
target[prop] = value;
observer();
}
})

function observer(){
spanName.innerHTML = obj.name;
inpName.value = obj.name;
}

inpName.oninput = function (){
obj.name = this.value;
}

setTimeout(()=>{
obj.name = "数据影响视图"
},2000)

</script>
</body>
</html>
6、Scheduler
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
class Scheduler {
constructor() {
this.limitCount = 2
this.locks = []
this.values = []
}
async push(value) {
if (this.values.length >= this.limitCount) {
await new Promise(resolve => this.locks.push(resolve))
}
this.values.push(value)
}
get() {
(this.locks.shift() || (() => {}))()
return this.values.shift()
}
async add(promiseCreactor) {
await this.push(1)
await promiseCreactor()
this.get()
}
}
const timeout = (time) => new Promise(resolve => {
setTimeout(resolve, time)
})
const scheduler = new Scheduler();
const addTask = (time, order) => {
scheduler.add(() => timeout(time))
.then(() => console.log(time, 'time, order', order))
}
addTask(500, '1');
addTask(800, '2');
addTask(1000, '3');
addTask(1200, '4');
// output: 2 3 1 4
// 一开始,1、2两个任务进入队列
// 500ms时,2完成,输出2,任务3进队
// 800ms时,3完成,输出3,任务4进队
// 1000ms时,1完成,输出1
// 1200ms时,4完成,输出4
7、封装ajax请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const request = function (method, url, isajax) {
return new Promise((resolve, reject) => {
let req = new XMLHttpRequest();
req.open(method, url, isajax);
req.onload = function () {
if (req.readState === 4 && req.status === 200) {
resolve(req.response)
} else {
reject(req.statusText);
}

req.onerror = function () {
reject(Error("网络异常"))
}
}
req.send();
})
}
8、__proto__和prototype的区别

__proto__就是对象的[[prototype]]属性,ES5中是用Object.getPrototypeOf函数获得一个对象的[[prototype]]。ES6中,使用Object.setPrototypeOf可以直接修改一个对象的[[prototype]]

1
2
Array.prototype.__proto__ =  Object.__proto__
Object.protptype.__proto__ = null
9、判断对象是否存在循环引用

首先,循环引用对象本来没有什么问题,序列化的时候才会发生问题,比如调用JSON.stringify()对该类对象进行序列化,就会报错: Converting circular structure to JSON.,而序列化需求很常见,比如发起一个ajax请求提交一个对象就需要对对象进行序列化。

针对上面这样的问题,可以通过JSON扩展包的var c = JSON.decycle(a)var a = JSON.retrocycle(c)来处理。这里就不做过多的解释了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function cycle(obj, parent){
var parentArr = parent || [obj];
for(var i in obj){
if(typeof obj[i] === "object"){
parentArr.forEach(element => {
if(element === obj[i]){
obj[i] = "[cycle]";
}
});
cycle(obj[i], [...parentArr,obj[i]])
}
}
return obj;
}

var a = {
b:null,
c:null
}
a.b = a;
a.c = a.c
console.log(cycle(a))
10、实现一个任务队列
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
class Queue{
constructor(){
this.taskList = [];
}
task(time,func){
this.taskList.push(function(){
return new Promise((resolve)=>{
setTimeout(()=>{
func.call(this)
resolve();
},time)
})
})
return this;
}
async start(){
while(this.taskList.length){
await this.taskList.shift()();
}
}
}
new Queue()
.task(1000, ()=>{console.log(1)})
.task(2000, ()=>{console.log(2)})
.task(1000, ()=>{console.log(3)})
.start()
11、如果给vue对象新增一个属性,如果也实现响应式

根据官方文档定义:如果在实例创建之后添加新的属性到实例上,它不会触发视图更新

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty)把这些属性全部转为 getter/setter

1
2
3
4
5
//Vue 不允许在已经创建的实例上动态添加新的根级响应式属性 (root-level reactive property)。然而它可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上:
Vue.set(vm.obj, 'e', 0) // this.$set(this.obj,'e',0)

//但是,添加到对象上的新属性不会触发更新。在这种情况下可以创建一个新的对象,让它包含原对象的属性和新的属性:
this.obj= Object.assign({}, this.obj, { a: 1, e: 2 })
12、vue自定义指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//注册一个全局指令
Vue.directive('focus', {
// 当绑定元素插入到 DOM 中。
inserted: function (el) {
// 聚焦元素
el.focus()
}
})

//也可以在实例中使用directives选项来注册局部指令,只能在实例中使用
new Vue({
el: '#app',
directives: {
// 注册一个局部的自定义指令 v-focus
focus: {
// 指令的定义
inserted: function (el) {
// 聚焦元素
el.focus()
}
}
}
})

钩子函数

一个指令定义对象可以提供如下几个钩子函数 (均为可选):
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

unbind:只调用一次,指令与元素解绑时调用。

指令钩子函数会被传入以下参数:
el:指令所绑定的元素,可以用来直接操作 DOM 。
binding:一个对象,包含以下属性:
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 }。
vnode: Vue编译生成的虚拟节点。移步 VNode API 来了解更多详情。
oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!