TikTok前端面经整理

1,626 阅读12分钟

Vue相关

1. Vue相应式原理[9]

vue渲染过程: new Vue -> init -> mount -> compile -> render -> vdom -> DOM

    1. init: 对所有属性做reactive化,为一个属性绑定getter函数,setter函数和Dep对象(防止无限递归)
const dep = new Dep();
Object.defineProperty(obj, key) {
	get() {
        ...
    	dep.depend();
        return value;
    }
    set(newVal) {
    	...
        val = newVal;
        dep.notify;
    }
}
    1. Mount: 对每一个component新建一个watcher对象
    1. 依赖收集: watcher的constructor会在创建的同时调用updateComponent -> updateComponent里面会调用render(渲染虚拟DOM)和update(更新真实DOM)函数 -> 触发组件内所有属性的getter —> dep.deppend/watcher.addDep中会把彼此加入自己的list里面
    1. 派发更新: reactive化属性变更后,dep.notify会循环通知list里所有绑定的watcher,由watcher去重新渲染更新页面
  • 特殊情况: 由于绑定发生在页面渲染的最开始,后续加入的元素无法监听。数组的索引不是属性,难以绑定。可以用Vue.set来解决。

2. 手写数据劫持/Proxy[9]

  • Vue 2.0 Object.property:
<span id = "spanName"></span>
let obj = {
	name: '';
}
let newObj = JSON.parse(JSON.stringify(obj));//模拟新建一个Dep()
Object.defineProperty(obj, 'name', {
	getter() {
    	//dep.depend();
     	return newObj.name;
    },
    setter(val) {
    	if (val == newObj.name) return;
        newObj.val = val;
        notify();
    }
})
function notify() {//模拟Dep.notify()
	spanName.innerHTML = obj.name;
}
  • Vue 3.0 proxy: 优势:
  1. 直接监听对象而非属性
  2. 可以监听数组变化
  3. 有13种拦截方法,适用范围更广 劣势: 兼容性差,IE9之前的浏览器无法支持

3. 对比MVVM/MVC/MVP[9]

  • MVC:Model/View/Controller 数据更新->视图更新->业务逻辑更新,单向数据绑定,需要对视图层变化进行额外操作
  • MVP:Model/View/Presenter 增加Presenter当做视图层和数据层的中介,但是p层比较臃肿
  • MVVM: 通过数据劫持和观察者模式监听视图层变化并用setter数据,并且在数据更改的时候用getter来获取数据从而实现双向绑定,在MVC的基础上增加了v-model/v-bind来监听数据更新

4. Vuex[4]

5. Diff算法,虚拟dom[4]

页面由DOM树构成,当某个部分被更改时对应的DOM节点也会发生更改,我们需要找到两个树之间最小修改值来其高效率

  • 传统:通过循环递归对每个节点一次对比,N个树节点的复杂度O(N^3)
  • diff算法:Facebook在React中提出,复杂度O(N),大致有三个策略
  1. tree diff:因为DOM节点跨层移动特别少,只对前后树进行分层对比,只需要O(N)的时间,如果真的跨层就删除旧的,生成新的
  2. Component diff:由于不同类的组件生成的树形状不同,当发现类型不同的时候直接替换整个组件节省效率
  3. element diff:同一层级下用唯一id来区分,相当于hashmap,查找效率非常高 总的来说是用低复杂度来处理高概率发生的事件,对于低概率事件直接add/remove

6. vue-router原理/分类/刷新[2]

  • 1.hash: SPA单页面应用为了提高用户体验,在页面交互的时候不会刷新页面,而是直接在页面内进行更新。14 年以前url路由是添加在#后面,hash值变化的时候不会向浏览器发送请求,但是会触发hashchange事件,从而触发页面更新组件。

  • 2.history模式: 2014年,HTML5/history API标准发布,新增了pushState/replaceState/popState,和hash原理相同,没有#更加美观,但是会发送请求给服务器,需要额外的处理。

7. Vue通信方式:父子/非父子[3]

props/vue自定义事件/消息订阅与发布/vuex/slot/eventBus/ref

  • props:一般属性子向父,函数属性是父向子
  • 自定义事件:事件绑定+$emit回调,只能子向父
  • 订阅发布: 比如PubSub可以适用于任何场景
  • vuex: 适用于所有场景,管理更方便
  • slot: 实现父向子传递带数据的标签

CSS相关

1.移动端适配方案 [2]

  • media媒体查询针对不同媒体定义不同样式
  • rem/em根据字体大小改变
  • vw/vh根据视窗大小自适应
  • flex盒模型也可以实现一些具体操作

2.自适应布局/左边固定右边自适应 [2]

  • flex布局:左边设为 0 0 20, (order: -1) 右边 flex:1自适应
  • 左:float,width:100%,padding-left:200px;右:float,margin:-100%
  • calc自动计算宽度
  • table
  • grid

3.选择器种类及优先级/解析方式[3]

  1. !important;
  2. 行内样式 1000
  3. ID选择器 100
  4. 类/伪类 10
  5. 元素/伪元素 1
  6. 通配符选择器/父子/后代 0

4.盒模型/基于哪个点[2]

主要分为标准盒模型,怪异盒模型,弹性盒子,标准盒模型以content宽高为基准,在真实开发中容易造成bug,可以用boxing-size:border-box改为怪异盒模型。

5.水平居中/水平垂直居中[2]

水平:

  • text-alian: center
  • margin: 0 auto
  • left: 50%, absolute, translateX(-%50)
  • left: 50%, absolute, margin-left:-(宽度/2)
  • flex,justify-content:center 水平垂直居中:
  • flex:justify-content, align-item
  • top:50%, left:50%, transform
  • top:0;left:0;right:0;bottom:0;margin:auto
  • display:table-cell;vertival-align:middle; text-align:center

网络相关

1. http缓存/离线缓存/协商缓存/强缓存/304[9]

缓存: 浏览器对之前请求过的文件进行缓存,以便下一次访问可以重复使用,节省带宽,提升访问速度,减缓服务器压力。

  • 强缓存:浏览器根据请求头里expires/cache-control判断缓存是否过期,如果没有过期直接使用强缓存不发送请求。
  • 协商缓存:如果强缓存失效会向服务器发送请求,并携带ETag(分布式系统ETag不同且last-modified需要保持一致)和Last-Modified/If-Modified-Since询问是否更改,如果服务器返回304(Not Modified),则直接读取缓存
  • 如果缓存全部失效则向服务器发起请求

2. HTTP状态码[3]

  • 200 请求成功
  • 301 永久重定向
  • 302 临时重定向
  • 304 未修改,可以从浏览器缓存读取
  • 400 Request语法错误
  • 401 未授权
  • 403 拒绝提供服务
  • 404 无法找到请求资源
  • 500 服务器内部错误
  • 503 服务器宕机

3. 输入域名到页面展现过程[3]

-> 解析URL,生成HTTP报文 -> 递归逐层读取DNS缓存,解析成IP地址,并动态寻找最快路由路径 -> 进行ARP解析,通过IP地址和子网掩码解析出ARP -> 进行三次握手 -> Nginx反向代理接受到请求 -> 向CDN请求静态资源 -> 将动态资源请求分发到Tomcat/Apache -> 去redis/Mysql中请求资源并逐层返回 -> 浏览器接收到静态文件以后进行渲染

4. TCP如何保证链接安全[3]

  1. 效验和,通过Hash值来确定字段正确
  2. 序列号,保证传输顺序和去重
  3. 应答机制,三次握手确认连接成功
  4. 超时重传
  5. 滑动窗口/慢启动,防止拥堵丢包
  6. 四次挥手,确定传输完成

5. DNS查询过程[2]

查看浏览器缓存->查看本机缓存->查看本地DNS服务器->查看根域名服务器... 依次向上逐层寻找

6. 浏览器加载页面过程[2]

浏览器是多进程的:主进程/第三方插件进程/GPU进程/渲染进程... 渲染进程是多线程的:

  • GUI渲染线程:解析HTML/CSS构建DOM树/CSS树,进行计算和绘制
  • JS引擎线程:单线程,解析JS脚本,运行代码,进行回流重绘,把异步操作放入事件队列
  • 消息队列线程:根据微任务/宏任务依次解析和运行

7. HTTP2/HTTP1区别[4]

  • 1.二进制:原来是文本格式,二进制更高效,更准确
  • 2.多路传输:传输更高效
  • 3.报头压缩:由于TCP的三次握手,和数据分装报头开销很大
  • 4.服务器推送:服务器可以分析浏览器的需求直接推送资源,减少延迟

8.跨域的几种方法/jsonp的实现[5]

跨域主要是由于前后端分离和同源策略,同源策略是指必须在同IP,同协议,同端口才能够顺利获取资源

第一阶段: jsonp跨域,由于scirpt,link,img等标签请求可以忽略跨域协议,可以在发送请求的时候动态生成script标签,只能用于get请求,大小有限制。

第二阶段: iframe跨域

第三阶段: CROS, 服务器端可以通过配置Access-Control-Allow来允许跨域访问,然后在第一次访问后返回token,之后每次访问携带token来确定身份。

  • 开发:可以在package.json中,利用webapck的devServer用proxy代理转发,由于proxy和我们是同源的,所以请求可以成功

  • 上线:可以由后端进行nginx反向代理

Jsonp代码:

function jsonp (url, data={}, callback='callback') {
	data.callback = callback
    let params = []
    for (let key in data) {
    	params.push(key + '=' + data[key])
    }
    let script = document.creatElement('script')
    script.src = url + '?'+ params.join('&')
    documnet.body.appendChild(script)
    return new Promise ((resole, reject) => {
    	window[callback] = (data) => {
        	try {
            	resolve(data)
            } catch {
            	reject(data)
            } finally {
            	console.log(script)
            	script.parentNode.removeChild(script)
               
            }
        }
    })
}
jsonp('www.google.com', {
	page: 1,
    cate: 'recommand'
}, jsoncallback).then(data => {
	console.log(data)
}) 

9. HTTPS理解/为什么慢?[4]🆕

10. ISO模型[3]🆕

11. get/post请求区别[3]🆕

JS相关/编程

1. eventLoop/异步编程原理[7]

JS是单线程的,会一行一行运行js代码,当我们需要进行ajax请求或者其他耗时的请求时会阻塞后面代码执行。由于浏览器的渲染进程是多线程的,我们可以把这些请求放入任务队列里异步处理,等主线任务全都运行完毕之后再把事件队列处理好的数据进行处理。

任务队列:先微任务(promise/async)/后宏任务

2. 手写函数节流/防抖[5]

节流: 在一段时间内只运行一次函数

function throttle(fn, delay) {
	var lastTime = 0;
    return function() {
    	var nowTime = Date.time();
        if (nowTime > lastTime + delay) {
        	fn();
            lastTime = nowTime;
        }
    }
}
document.onscroll = throttle(function(){console.log(1)}, 200)
//相当于把document.onscroll的this指向改为throttle,之后每次获取之前的lastTime 

防抖: 在一段时间内频繁触发只按照最后一次执行

function debounce(fn, delay) {
	var timer = null;
    return function() {
    	clearTimeout(timer);
        timer = setTimeOut(function() {
        	fn.apply(this)
        }, delay)
    }
}

3. 实现带并发限制的scheduler[5]

class Scheduler{
	constructor() {
    	this.tasks = []
        this.running = 0
    }
	add(promiseCreator) {
    	retrun new Promise(resolve => {
        	this.tasks.push(() => promiseCreator().then(resolve));
            runTask();
        })
    }
    runTask() {
    	if (running >= 2) return
        let task = tasks.shift()
        if (task) {
        	this.running++;
            task.then(() => {
				this.concurrent -= 1
                this.runTask()
            })
        }
    }
}

4. 对闭包的理解/闭包的用处[4]

Js分为全局作用域,函数作用域,eval作用域(可忽略)。我们在函数作用域可以调用全局作用域的属性,但是在全局作用域无法调用函数作用域属性。这时我们可以通过返回回调函数来保证函数在执行结束以后不被GC销毁,相当于一个接口或者说连通器。

  1. 安全访问函数内部属性
  2. 属性私有化/不污染全局变量
  3. 函数内部变量长期存在,但是需要手动释放内存

5. this指向/call/apply/bind/手写bind[3]

  • 默认绑定:全局调用/没有指定调用者则默认为window
  • 隐式绑定:自动绑定调用自己的Object
  • 硬绑定:call/apply,指定this对象
  • 构造函数绑定:创造出的实例化对象指向构造函数对象 bind/apply实现

6. 匹配URL正则/匹配后缀的正则[3]

7. 实现异步sleep函数[2]

async function test() {
  console.log('Hello')
  await sleep(1000)
  console.log('world!')
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms))
}

8. 原型链[4]

const person = new Person();

person.proto == Person.prototype Person.prototype.proto == Object.prototype 当我们访问对象实例的属性时,如果找不到就通过_proto_去原型对象里面逐层查找

9. 数据类型检测方式/隐式类型转化/手写函数[4]🆕

10. Js继承方式/应用场景[3]🆕

1.原型链继承 // Child.prototype = new Parent(); 2.call继承 // 在Child方法Parent.call(this) 3.实例继承 // var p = new Parent() 4.拷贝继承 5.组合继承 6.类继承

ES6

1. Promise理解 [5]

2. let/var/const区别[4]

  • var: 可以重新声明,重新赋值,会变量提升,作用域范围不可控
  • let: 不能重新声明,可以重新赋值,不会变量提升,会形成块级作用域
  • const: 不能重新声明,不能重新复制,不会变量提升,会形成块级作用域

3. ES6新属性/结构赋值[4]

let/const/promise/解构赋值/箭头函数

4. 实现promise.all[2]🆕

1、接收一个 Promise 实例的数组或具有 Iterator 接口的对象, 2、如果元素不是 Promise 对象,则使用 Promise.resolve 转成 Promise 对象 3、如果全部成功,状态变为 resolved,返回值将组成一个数组传给回调 4、只要有一个失败,状态就变为 rejected,返回值将直接传递给回调all() 的返回值也是新的 Promise 对象

function promiseAll(promises) {
  return new Promise(function(resolve, reject) {
    if (!isArray(promises)) {
      return reject(new TypeError('arguments must be an array'));
    }
    var resolvedCounter = 0;
    var promiseNum = promises.length;
    var resolvedValues = new Array(promiseNum);
    for (var i = 0; i < promiseNum; i++) {
      (function(i) {
        Promise.resolve(promises[i]).then(function(value) {
          resolvedCounter++
          resolvedValues[i] = value
          if (resolvedCounter == promiseNum) {
            return resolve(resolvedValues)
          }
        }, function(reason) {
          return reject(reason)
        })
      })(i)
    }
  })
}

5. 箭头函数和普通函数区别[2]🆕

箭头函数也可以看作是简洁版的匿名函数。不过箭头函数最大的优点在于,普通函数的回调函数的this默认指向window。箭头函数的this默认指向调用自己的函数的this。

杂项

1. Websocket原理[7]

2. webpack原理/loader[8]

3. 前端性能优化/渲染优化/资源优化[4]

资源优化:

  1. CDN
  2. 资源压缩,nginx会自动开启
  3. 浏览器缓存机制

渲染优化:

  1. 雪碧图(基本不用了)
  2. CSS放头,js防尾(现在自动打包基本不用了)
  3. 懒加载
  4. keep-alive在页面跳转以后不立马销毁组件,相当于缓存
  5. 懒加载/预加载HTTP2.0
  6. 首屏加载

4. hooks原理/缺点 [4]

5. 快排[3]

随机找基准点,比我大的放左边,比我小的放右边

6. mysql索引[2]

7. CDN原理/常见过程[2]🆕

8. Redis持久化/常见数据结构[2]🆕

9. 常见设计模式[2]🆕

10. Babel如何将ES6转化为ES5[2]🆕