前端面试题整理
- 闭包
- 为啥vue要用set增加对象属性
- react 中setState原理
- react Hook写法useState原理
- js多线程 (主线程和子线程关系)
- es6 Set去重实现原理
- js 异步原理
- 箭头函数和普通函数区别
- vue 虚拟DOM理解和diff算法
- Promise理解及简单实现
- get和post区别
- 节流和防抖
- js事件循环机制
- 伪类和伪元素
- Tcp协议几次握手几次挥手
- 理解http浏览器的协商缓存和强制缓存
- 什么重排重绘
- class和普通构造函数
- vue 数据双向绑定原理
- 原型和原型链
- js继承的几种方式
- 盒模型
- position
- v-bind 可传对象
- es6 模块化
- vue 懒加载组件和路由懒加载
- Vue 中hash路由和history路由原理及优缺点
- call,apply,bind 三者的用法区别
暂时就更新这些啦...(搬运工不容易)
1.闭包
闭包:就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁>:就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁
使用注意点
2.为啥vue要用set增加对象属性
data
对象上存在才能让 Vue 将它转换为响应式的。3.react 中setState原理
4.react Hook写法useState原理
useState的功能是设置一个状态的初始值,并返回当前状态和设置状态的函数。
[状态,设置状态函数] = useState(初始状态)
输入 | hook函数 | 输出 |
---|---|---|
初始状态 | useState | 状态,设置状态函数 |
useState直接被调用的过程也是组件初始化和组件更新,其还有一个调用设置状态函数的过程。
组件初始化:
- 若初始状态为函数,则将函数执行结果设为当前状态。否则将初始状态设为当前状态。
- 生成设置状态函数
- 缓存当前状态和设置状态函数
- 返回当前状态
组件更新:
- 读取缓存状态和设置状态函数
- 返回缓存状态
执行设置状态函数:
- 更新缓存状态
- 触发React组件树更新
- 在下一次组件更新时,将返回已被更新的缓存状态
let hookState; function useState(intlValue){ hooState = hooState || intlValue function setState(newState){ hookState = newState } return [hookState,setState] }
现在我们来使用它
const [count,setCount] = useState(1)
setCount(5)
但是我们多次useState,会产生多个useState解构出来的值都是同一个值,改变的也都是同一个值,为了解决这种情况,我们来升级一下
let hookState = []; let index = 0 function useState(intlValue) { hookState[index] = hookState[index] || intlValue let currentIndex = index //利用闭包保存函数 function setState(newState) { hookState[currentIndex] = newState } //下次使用useState index为index+1 index++ return [hookState[index], setState] } const [count, setCount] = useState(1) const [number, setNumber] = useState(1) setCount(5) setNumber(8) console.log(setCount === setNumber) //false console.log(useState === useState)//true
现在我们可以多次useState了,知道原理我们就能更好的使用hook
5.js多线程 (主线程和子线程关系)
一、多线程理解
首先,我们要理解什么是多线程,百度百科上说:多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。[1] 在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程(台湾译作“执行绪”),进而提升整体处理性能。
按照我的理解就是不阻塞的前提下,时间最优的方法,不局限于流水线(单线)的方法。理解多线程的原理后,结合javascript,本身javascript是不支持多线程的。把异步处理的东西放到一个池中,当同步的解决完即图中的t1到t7,然后唤醒异步队列。
二、Concurrent.Thread.js
Concurrent.Thread.js库是利用setTimeout和setInterval方法来模拟线程的概念。并行执行任务。下载地址:Concurrent.Thread.js
主要是为了解决浏览器死卡的现象,当一个函数执行非常浪费时间和内存的时候,给另外开辟一个线程。因为javascript是单线程,会阻塞。这时候我们引入这个库文件,可以使代码不阻塞哦,应用方法主要是create方法创建一个单线程。
Concurrent.Thread.create(function(){ $('#test').click(function () { alert(1); }); for (var i = 0;i<1000000;i++) { console.log(i); }; });
这么就可以在浏览器上边点击div有效果,同时console也在一直不停的打印数据。各忙各的。
这是理解Concurrent.Thread.js库应用的最简单方法。Concurrent.Thread提供了一个应用JavaScript 的异步通信方式实现的定制通信库。类似于AJAX的原理,用get或者post方法发送和响应数据。具体参考可以穿越链接http://www.cnblogs.com/0banana0/archive/2011/06/01/2067402.html,这里可以看到更详细的解释。
三、WebWork
js是单线程的去跑代码,比如如果做一个循环从0到一个很大的数字相加然后输出,浏览器可能会假死(无响应状态)。但是用webwork以后,就可以非常方便的进行渲染网页的同时,计算这个数据。在 HTML5 中提出了工作线程(Web Worker)的概念,并且规范出 Web Worker 的三大主要特征:能够长时间运行(响应),理想的启动性能以及理想的内存消耗。Web Worker 允许开发人员编写能够长时间运行而不被用户所中断的后台程序,去执行事务或者逻辑,并同时保证页面对用户的及时响应。
WebWork能做什么?
1.可以加载一个JS进行大量的复杂计算而不挂起主进程,并通过postMessage,onmessage进行通信, 在主线程与子线程间进行通信,使用的是线程对象的postMessage和onmessage方法。不管是谁向谁发数据,发送发使用的都是postMessage方法,接收方都是使用onmessage方法接收数据。postMessage只有一个参数,那就是传递的数据,onmessage也只有一个参数,假设为event,则通过event.data获取收到的数据。
2.可以在worker中通过importScripts(url)加载另外的脚本文件,即多个js文件
3.可以使用 setTimeout(), clearTimeout(), setInterval(), and clearInterval():定时器可以使用线程
4.可以使用XMLHttpRequest来发送请求,使用AJAX
5.可以访问navigator的部分属性:可以在localStorage和sessionStorage
下面来具体说明一下webwork的专用线程使用步骤。
主线程代码
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title></title> <script type="text/javascript"> //创建线程 work对象 var work = new Worker("work.js"); //work文件中不要存在跟ui代码 //发送消息 work.postMessage("100"); // 监听消息 work.onmessage = function(event) { alert(event.data); }; </script> </head> <body> </body> </html>
work.js子线程代码
onmessage = function (event) { //从1加到num var num = event.data; var result = 0; for (var i = 1; i <= num; i++) { result += i; } postMessage(result); }
总结:个人认为多线程开启就是为了避免页面阻塞的,所以子线程可以进行一些数据处理,然后回馈给主线程。
6.es6 Set去重实现原理
es6官网指出了,Set内部判断两个值是否不同,使用“Same-value-zero equality”算法,类似于精确运算符“===”
7.js 异步原理
调用栈
-
JS执行时会形成调用栈,调用一个函数时,返回地址、参数、本地变量都会被推入栈中,如果当前正在运行的函数中调用另外一个函数,则该函数相关内容也会被推入栈顶.该函数执行完毕,则会被弹出调用栈.变量也随之弹出,由于复杂类型值存放于堆中,因此弹出的只是指针,他们的值依然在堆中,由GC决定回收.
-
尾调用:指某个函数的最后一步是调用另一个函数。由调用栈可知,调用栈中有a函数,如果a函数调用b函数,则b函数也随之入栈,此时栈中就会有两个函数.但是如果b函数是a函数最后一步,并且不需保留外层函数调用记录,即a函数调用位置变量等都不需要用到,则该调用栈中会只保留b函数,这就叫做"尾调用优化"(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。
1 function a() { 2 let m = 1; 3 let n = 2; 4 return b(m + n); 5 } 6 a(); 7 8 // 等同于 9 function a() { 10 return b(3); 11 } 12 a(); 13 14 // 等同于 15 b(3);
事件循环(event loop)和任务队列(task queue)
-
JS的异步机制由事件循环和任务队列构成.JS本身是单线程语言,所谓异步依赖于浏览器或者操作系统等完成. JavaScript 主线程拥有一个执行栈以及一个任务队列,主线程会依次执行代码,当遇到函数时,会先将函数入栈,函数运行完毕后再将该函数出栈,直到所有代码执行完毕。
-
遇到异步操作(例如:setTimeout, AJAX)时,异步操作会由浏览器(OS)执行,浏览器会在这些任务完成后,将事先定义的回调函数推入主线程的任务队列(task queue)中,当主线程的执行栈清空之后会读取task queue中的回调函数,当task queue被读取完毕之后,主线程接着执行,从而进入一个无限的循环,这就是事件循环.
However, we only have one main thread and one call-stack, so in case there is another request being served when the said file is read, its callback will need to wait for the stack to become empty. The limbo where callbacks are waiting for their turn to be executed is called the task queue (or event queue, or message queue). Callbacks are being called in an infinite loop whenever the main thread has finished its previous task, hence the name 'event loop'.
Microtask 与 Macrotask
-
一个浏览器环境(unit of related similar-origin browsing contexts.)只能有一个事件循环(Event loop),而一个事件循环可以多个任务队列(Task queue),每个任务都有一个任务源(Task source)。例如,客户端可能实现了一个包含鼠标键盘事件的任务队列,还有其他的任务队列,而给鼠标键盘事件的任务队列更高优先级,例如75%的可能性执行它。这样就能保证流畅的交互性,而且别的任务也能执行到了。但是,同一个任务队列中的任务必须按先进先出的顺序执行。多个任务队列,是为了方便控制优先级。任务队列是一个先进先出的队列.
-
macrotask 和 microtask 是异步任务的两种分类。在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。
-
全部代码(script)是一个macrotask,js先执行一个macrotask,执行过程中遇到(setTimeout, setInterval, setImmediate等)异步操作则创建一个macrotask,遇到(process.nextTick, Promises等)创建一个microtask,这两个queue分别被挂起.执行栈为空时开始处理macrotask,完成后处理microtask,直到该microtask全部执行完,然后继续主线程调用栈.
注:每一次事件循环(one cycle of the event loop),只处理一个 (macro)task。待该 macrotask 完成后,所有的 microtask 会在同一次循环中处理。处理这些 microtask 时,还可以将更多的 microtask 入队,它们会一一执行,直到整个 microtask 队列处理完。
两个类别的具体分类如下:
macro-task: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
micro-task: process.nextTick, Promises(这里指浏览器实现的原生 Promise), Object.observe, MutationObserver
8.箭头函数和普通函数区别
- 相比普通函数更简洁的语法
- 没有this
- 不能使用new
- 不绑定arguments,用rest参数...解决
- 使用call()和apply()调用
- 捕获其所在上下文的 this 值,作为自己的 this 值
- 箭头函数没有原型属性
- 不能简单返回对象字面量
- 箭头函数不能当做Generator函数,不能使用yield关键字
- 箭头函数不能换行
正文
ES6标准新增了一种新的函数:Arrow Function(箭头函数)。
为什么叫Arrow Function?因为它的定义用的就是一个箭头:
语法:
1 //1、没有形参的时候 2 let fun = () => console.log('我是箭头函数'); 3 fun(); 4 //2、只有一个形参的时候()可以省略 5 let fun2 = a => console.log(a); 6 fun2('aaa'); 7 8 //3、俩个及俩个以上的形参的时候 9 let fun3 = (x,y) =>console.log(x,y); //函数体只包含一个表达式则省略return 默认返回 10 fun3(24,44); 11 12 //4、俩个形参以及函数体多条语句表达式 13 let fun4 = (x,y) => { 14 console.log(x,y); 15 return x+y; //必须加return才有返回值 16 }//5、如果要返回对象时需要用小括号包起来,因为大括号被占用解释为代码块了,正确写法let fun5 = ()=>({ foo: x }) //如果x => { foo: x } //则语法出错
那么箭头函数有哪些特点?
- 更简洁的语法
- 没有this
- 不能使用new 构造函数
- 不绑定arguments,用rest参数...解决
- 使用call()和apply()调用
- 捕获其所在上下文的 this 值,作为自己的 this 值
- 箭头函数没有原型属性
- 不能简单返回对象字面量
- 箭头函数不能当做Generator函数,不能使用yield关键字
- 箭头函数不能换行
相比普通函数更简洁的语法
1 //箭头函数 2 var a = ()=>{ 3 return 1; 4 } 5 6 //相当于普通函数 7 function a(){ 8 return 1; 9 }
没有this
在箭头函数出现之前,每个新定义的函数都有其自己的 this 值
1 var myObject = { 2 value:1, 3 getValue:function(){ 4 console.log(this.value) 5 }, 6 double:function(){ 7 return function(){ //this指向double函数内不存在的value 8 console.log(this.value = this.value * 2); 9 } 10 } 11 } 12 /*希望value乘以2*/ 13 myObject.double()(); //NaN 14 myObject.getValue(); //1
使用箭头函数
1 var myObject = { 2 value:1, 3 getValue:function(){ 4 console.log(this.value) 5 }, 6 double:function(){ 7 return ()=>{ 8 console.log(this.value = this.value * 2); 9 } 10 } 11 } 12 /*希望value乘以2*/ 13 myObject.double()(); //2 14 myObject.getValue(); //2
不能使用new
箭头函数作为匿名函数,是不能作为构造函数的,不能使用new
var B = ()=>{ value:1; } var b = new B(); //TypeError: B is not a constructor
不绑定arguments,用rest参数...解决
1 /*常规函数使用arguments*/ 2 function test1(a){ 3 console.log(arguments); //1 4 } 5 /*箭头函数不能使用arguments*/ 6 var test2 = (a)=>{console.log(arguments)} //ReferenceError: arguments is not defined 7 /*箭头函数使用reset参数...解决*/ 8 let test3=(...a)=>{console.log(a[1])} //22 9 10 test1(1); 11 test2(2); 12 test3(33,22,44);
使用call()和apply()调用
由于 this 已经在词法层面完成了绑定,通过 call() 或 apply() 方法调用一个函数时,只是传入了参数而已,对 this 并没有什么影响:
1 var obj = { 2 value:1, 3 add:function(a){ 4 var f = (v) => v + this.value; //a==v,3+1 5 return f(a); 6 }, 7 addThruCall:function(a){ 8 var f = (v) => v + this.value; //此this指向obj.value 9 var b = {value:2}; 10 return f.call(b,a); //f函数并非指向b,只是传入了a参数而已 11 12 } 13 } 14 15 console.log(obj.add(3)); //4 16 console.log(obj.addThruCall(4)); //5
捕获其所在上下文的 this 值,作为自己的 this 值
1 var obj = { 2 a: 10, 3 b: function(){ 4 console.log(this.a); //10 5 }, 6 c: function() { 7 return ()=>{ 8 console.log(this.a); //10 9 } 10 } 11 } 12 obj.b(); 13 obj.c()();
箭头函数没有原型属性
1 var a = ()=>{ 2 return 1; 3 } 4 5 function b(){ 6 return 2; 7 } 8 9 console.log(a.prototype);//undefined 10 console.log(b.prototype);//object{...}
不能简单返回对象字面量
如果要返回对象时需要用小括号包起来,因为大括号被占用解释为代码块了,正确写法
let fun5 = ()=>({ foo: x }) //如果x => { foo: x } //则语法出错
箭头函数不能当做Generator函数,不能使用yield关键字
箭头函数不能换行
let a = () =>1; //SyntaxError: Unexpected token =>
9.vue 虚拟DOM理解和diff算法
10.Promise理解及简单实现
Promise 的含义
Promise 是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和更强大。简单说Promise就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。
解决问题:
- 解决了回调地狱问题,不会导致难以维护;
- 合并多个异步请求,节约时间。
两个特点:
①对象的状态不受外界影响
Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
②一旦状态改变,就不会再变
Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。
Promise
也有一些缺点。首先,无法取消Promise
,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise
内部抛出的错误,不会反应到外部。第三,当处于pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
Promise简易实现
Promise 有三种状态:Pending 初始态; Fulfilled 成功态; Rejected 失败态。
1 function Promise(executor) { 2 let self = this; 3 self.status = 'pending'; //等待态 4 self.value = undefined; //成功的返回值 5 self.reason = undefined; //失败的原因 6 7 function resolve(value){ 8 if(self.status === 'pending'){ 9 self.status = 'resolved'; 10 self.value = value; 11 } 12 } 13 function reject(reason) { 14 if(self.status === 'pending') { 15 self.status = 'rejected'; 16 self.reason = reason; 17 } 18 } 19 try{ 20 executor(resolve, reject); 21 }catch(e){ 22 reject(e);// 捕获时发生异常,就直接失败 23 } 24 } 25 //onFufiled 成功的回调 26 //onRejected 失败的回调 27 Promise.prototype.then = function (onFufiled, onRejected) { 28 let self = this; 29 if(self.status === 'resolved'){ 30 onFufiled(self.value); 31 } 32 if(self.status === 'rejected'){ 33 onRejected(self.reason); 34 } 35 } 36 module.exports = Promise;
参考地址:https://segmentfault.com/a/1190000023180502
1 class WPromise { 2 static pending = 'pending'; 3 static fulfilled = 'fulfilled'; 4 static rejected = 'rejected'; 5 6 constructor(executor) { 7 this.status = WPromise.pending; // 初始化状态为pending 8 this.value = undefined; // 存储 this._resolve 即操作成功 返回的值 9 this.reason = undefined; // 存储 this._reject 即操作失败 返回的值 10 // 存储then中传入的参数 11 // 至于为什么是数组呢?因为同一个Promise的then方法可以调用多次 12 this.callbacks = []; 13 executor(this._resolve.bind(this), this._reject.bind(this)); 14 } 15 16 // onFulfilled 是成功时执行的函数 17 // onRejected 是失败时执行的函数 18 then(onFulfilled, onRejected) { 19 // 返回一个新的Promise 20 return new WPromise((nextResolve, nextReject) => { 21 // 这里之所以把下一个Promsie的resolve函数和reject函数也存在callback中 22 // 是为了将onFulfilled的执行结果通过nextResolve传入到下一个Promise作为它的value值 23 this._handler({ 24 nextResolve, 25 nextReject, 26 onFulfilled, 27 onRejected 28 }); 29 }); 30 } 31 32 _resolve(value) { 33 // 处理onFulfilled执行结果是一个Promise时的情况 34 // 这里可能理解起来有点困难 35 // 当value instanof WPromise时,说明当前Promise肯定不会是第一个Promise 36 // 而是后续then方法返回的Promise(第二个Promise) 37 // 我们要获取的是value中的value值(有点绕,value是个promise时,那么内部存有个value的变量) 38 // 怎样将value的value值获取到呢,可以将传递一个函数作为value.then的onFulfilled参数 39 // 那么在value的内部则会执行这个函数,我们只需要将当前Promise的value值赋值为value的value即可 40 if (value instanceof WPromise) { 41 value.then( 42 this._resolve.bind(this), 43 this._reject.bind(this) 44 ); 45 return; 46 } 47 48 this.value = value; 49 this.status = WPromise.fulfilled; // 将状态设置为成功 50 51 // 通知事件执行 52 this.callbacks.forEach(cb => this._handler(cb)); 53 } 54 55 _reject(reason) { 56 if (reason instanceof WPromise) { 57 reason.then( 58 this._resolve.bind(this), 59 this._reject.bind(this) 60 ); 61 return; 62 } 63 64 this.reason = reason; 65 this.status = WPromise.rejected; // 将状态设置为失败 66 67 this.callbacks.forEach(cb => this._handler(cb)); 68 } 69 70 _handler(callback) { 71 const { 72 onFulfilled, 73 onRejected, 74 nextResolve, 75 nextReject 76 } = callback; 77 78 if (this.status === WPromise.pending) { 79 this.callbacks.push(callback); 80 return; 81 } 82 83 if (this.status === WPromise.fulfilled) { 84 // 传入存储的值 85 // 未传入onFulfilled时,value传入 86 const nextValue = onFulfilled 87 ? onFulfilled(this.value) 88 : this.value; 89 nextResolve(nextValue); 90 return; 91 } 92 93 if (this.status === WPromise.rejected) { 94 // 传入存储的错误信息 95 // 同样的处理 96 const nextReason = onRejected 97 ? onRejected(this.reason) 98 : this.reason; 99 nextReject(nextReason); 100 } 101 } 102 }
11.get和post区别
在以下情况中,请使用 POST 请求:
1. 无法使用缓存文件(更新服务器上的文件或数据库)
2. 向服务器发送大量数据(POST 没有数据量限制)
3. 发送包含未知字符的用户输入时,POST 比 GET 更稳定也更可靠
12.节流和防抖
- 实现方式:每次触发事件时设置一个延迟调用方法,并且取消之前的延时调用方法
- 缺点:如果事件在规定的时间间隔内被不断的触发,则调用方法会被不断的延迟
1 //防抖debounce代码: 2 function debounce(fn,timeOut=500) { 3 let timer = null; // 创建一个标记用来存放定时器的返回值 4 return function () { 5 // 每当用户输入的时候把前一个 setTimeout clear 掉 6 clearTimeout(timer); 7 // 然后又创建一个新的 setTimeout, 这样就能保证interval 间隔内如果时间持续触发,就不会执行 fn 函数 8 timer = setTimeout(() => { 9 fn.apply(this, arguments); 10 }, timeOut); 11 }; 12 } 13 // 处理函数 14 function handle() { 15 console.log(Math.random()); 16 } 17 // 滚动事件 18 window.addEventListener('scroll', debounce(handle));
- 实现方式:每次触发事件时,如果当前有等待执行的延时函数,则直接return、
1 //节流throttle代码: 2 function throttle(fn,timeOut=500) { 3 let canRun = true; // 通过闭包保存一个标记 4 return function () { 5 // 在函数开头判断标记是否为true,不为true则return 6 if (!canRun) return; 7 // 立即设置为false 8 canRun = false; 9 // 将外部传入的函数的执行放在setTimeout中 10 setTimeout(() => { 11 // 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。 12 // 当定时器没有执行的时候标记永远是false,在开头被return掉 13 fn.apply(this, arguments); 14 canRun = true; 15 },timeOut); 16 }; 17 } 18 19 function sayHi(e) { 20 console.log(e.target.innerWidth, e.target.innerHeight); 21 } 22 window.addEventListener('resize', throttle(sayHi));
总结:
函数防抖:将多次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
函数节流:使得一定时间内只触发一次函数。原理是通过判断是否有延迟调用函数未执行。
区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
13.js事件循环机制
前言
众所周知,JavaScript 是一门单线程语言,虽然在 html5 中提出了 Web-Worker ,但这并未改变 JavaScript 是单线程这一核心。可看HTML规范中的这段话:
To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.
为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,用户引擎必须使用 event loops。Event Loop 包含两类:一类是基于 Browsing Context ,一种是基于 Worker ,二者是独立运行的。 下面本文用一个例子,着重讲解下基于 Browsing Context 的事件循环机制。
来看下面这段 JavaScript 代码:
1 console.log('script start'); 2 3 setTimeout(function() { 4 console.log('setTimeout'); 5 }, 0); 6 7 Promise.resolve().then(function() { 8 console.log('promise1'); 9 }).then(function() { 10 console.log('promise2'); 11 }); 12 13 console.log('script end');
先猜测一下这段代码的输出顺序是什么,再去浏览器控制台输入一下,看看实际输出的顺序和你猜测出的顺序是否一致,如果一致,那就说明,你对 JavaScript 的事件循环机制还是有一定了解的,继续往下看可以巩固下你的知识;而如果实际输出的顺序和你的猜测不一致,那么本文下面的部分会为你答疑解惑。
同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入 Event Queue 。主线程内的任务执行完毕为空,会去 Event Queue 读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。
在事件循环中,每进行一次循环操作称为tick,通过阅读规范可知,每一次 tick 的任务处理模型是比较复杂的,其关键的步骤可以总结如下:
- 在此次 tick 中选择最先进入队列的任务( oldest task ),如果有则执行(一次)
- 检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtask Queue
- 更新 render
- 主线程重复执行上述步骤
可以用一张图来说明下流程:
这里相信有人会想问,什么是 microtasks ?规范中规定,task分为两大类, 分别是 Macro Task (宏任务)和 Micro Task(微任务), 并且每个宏任务结束后, 都要清空所有的微任务,这里的 Macro Task也是我们常说的 task ,有些文章并没有对其做区分,后面文章中所提及的task皆看做宏任务( macro task)。
(macro)task 主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)
microtask主要包含:Promise、MutaionObserver、process.nextTick(Node.js 环境)
setTimeout/Promise 等API便是任务源,而进入任务队列的是由他们指定的具体执行任务。来自不同任务源的任务会进入到不同的任务队列。其中 setTimeout 与 setInterval 是同源的。
分析示例代码
千言万语,不如就着例子讲来的清楚。下面我们可以按照规范,一步步执行解析下上面的例子,先贴一下例子代码(免得你往上翻)。
任务队列
所有的任务可以分为同步任务和异步任务,同步任务,顾名思义,就是立即执行的任务,同步任务一般会直接进入到主线程中执行;而异步任务,就是异步执行的任务,比如ajax网络请求,setTimeout 定时函数等都属于异步任务,异步任务会通过任务队列( Event Queue )的机制来进行协调。具体的可以用下面的图来大致说明一下:
1 console.log('script start'); 2 3 setTimeout(function() { 4 console.log('setTimeout'); 5 }, 0); 6 7 Promise.resolve().then(function() { 8 console.log('promise1'); 9 }).then(function() { 10 console.log('promise2'); 11 }); 12 13 console.log('script end');
- 整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 script start
- 遇到 setTimeout,其回调函数被分发到宏任务 Event Queue 中
- 遇到 Promise,其 then函数被分到到微任务 Event Queue 中,记为 then1,之后又遇到了 then 函数,将其分到微任务 Event Queue 中,记为 then2
- 遇到 console.log,输出 script end
至此,Event Queue 中存在三个任务,如下表:
宏任务 | 微任务 |
---|---|
setTimeout | then1 |
- | then2 |
- 执行微任务,首先执行then1,输出 promise1, 然后执行 then2,输出 promise2,这样就清空了所有微任务
- 执行 setTimeout 任务,输出 setTimeout 至此,输出的顺序是:script start, script end, promise1, promise2, setTimeout
so,你猜对了吗?
看看你掌握了没
1 console.log('script start'); 2 3 setTimeout(function() { 4 console.log('timeout1'); 5 }, 10); 6 7 new Promise(resolve => { 8 console.log('promise1'); 9 resolve(); 10 setTimeout(() => console.log('timeout2'), 10); 11 }).then(function() { 12 console.log('then1') 13 }) 14 15 console.log('script end');
这个题目就稍微有点复杂了,我们再分析下:
首先,事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 scrip t(整体代码)任务;当遇到任务源 (task source) 时,则会先分发任务到对应的任务队列中去。所以,就和上面例子类似,首先遇到了console.log,输出 script start; 接着往下走,遇到 setTimeout 任务源,将其分发到任务队列中去,记为 timeout1; 接着遇到 promise,new promise 中的代码立即执行,输出 promise1, 然后执行 resolve ,遇到 setTimeout ,将其分发到任务队列中去,记为 timemout2, 将其 then 分发到微任务队列中去,记为 then1; 接着遇到 console.log 代码,直接输出 script end 接着检查微任务队列,发现有个 then1 微任务,执行,输出then1 再检查微任务队列,发现已经清空,则开始检查宏任务队列,执行 timeout1,输出 timeout1; 接着执行 timeout2,输出 timeout2 至此,所有的都队列都已清空,执行完毕。其输出的顺序依次是:script start, promise1, script end, then1, timeout1, timeout2
用流程图看更清晰:
总结
有个小 tip:从规范来看,microtask 优先于 task 执行,所以如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。
最后的最后,记住,JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。
这个知识点全部照搬啦https://www.cnblogs.com/yugege/p/9598265.html
参考文献
这一次,彻底弄懂 JavaScript 执行机制 Tasks, microtasks, queues and schedules 从一道题浅说 JavaScript 的事件循环
14.伪类和伪元素
1.伪元素和伪类的区别
- 伪元素和伪类都是为了给一些特殊需求加样式,定义上基本一致。
- 伪类像类选择器一样给已存在某个元素添加额外的样式;伪元素则是给自己虚拟的元素添加样式。
- 已存在元素是指DOM中存在的,伪元素则是虚拟的一种,样式也是给这个虚拟的元素使用的。比如虚拟一个div
- 声明不同,伪类和选择器之间用一个冒号隔开,伪元素则是两个冒号隔.
2.常用伪类种类
3.常用伪元素种类
15.Tcp协议几次握手几次挥手
有几个字段需要重点介绍下:
(1)序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
(2)确认序号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。
(3)标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:
(A)URG:紧急指针(urgent pointer)有效。
(B)ACK:确认序号有效。
(C)PSH:接收方应该尽快将这个报文交给应用层。
(D)RST:重置连接。
(E)SYN:发起一个新连接。
(F)FIN:释放一个连接。
需要注意的是:
(A)不要将确认序号Ack与标志位中的ACK搞混了。
(B)确认方Ack=发起方Req+1,两端配对。
三次握手:
TCP(Transmission Control Protocol) 传输控制协议
TCP是主机对主机层的传输控制协议,提供可靠的连接服务,采用三次握手确认建立一个连接
位码即tcp标志位,有6种标示:
SYN(synchronous建立联机)
ACK(acknowledgement 确认)
PSH(push传送)
FIN(finish结束)
RST(reset重置)
URG(urgent紧急)
Sequence number(顺序号码)
Acknowledge number(确认号码)
establish 建立,创建
所谓三次握手(Three-Way Handshake)即建立TCP连接,是指建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发,整个流程如下图所示:
(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack (number )=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
四次挥手:
三次握手耳熟能详,四次挥手估计就..所谓四次挥手(Four-Way Wavehand)即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发,整个流程如下图所示:
由于TCP连接时全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。
(1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
(2)第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
(3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
(4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
16.理解http浏览器的协商缓存和强制缓存
一:浏览器缓存的作用是什么?
1. 缓存可以减少冗余的数据传输。节省了网络带宽,从而更快的加载页面。
2. 缓存降低了服务器的要求,从而服务器更快的响应。
那么我们使用缓存,缓存的资源文件到什么地方去了呢?
那么首先来看下 memory cache 和 disk cache 缓存
memory cache: 它是将资源文件缓存到内存中。等下次请求访问的时候不需要重新下载资源,而是直接从内存中读取数据。
disk cache: 它是将资源文件缓存到硬盘中。等下次请求的时候它是直接从硬盘中读取。
那么他们两则的区别是?
memory cache(内存缓存)退出进程时数据会被清除,而disk cache(硬盘缓存)退出进程时数据不会被清除。内存读取比硬盘中读取的速度更快。但是我们也不能把所有数据放在内存中缓存的,因为内存也是有限的。
memory cache(内存缓存)一般会将脚本、字体、图片会存储到内存缓存中。
disk cache(硬盘缓存) 一般非脚本会存放在硬盘中,比如css这些。
缓存读取的原理:先从内存中查找对应的缓存,如果内存中能找到就读取对应的缓存,否则的话就从硬盘中查找对应的缓存,如果有就读取,否则的话,就重新网络请求。
那么浏览器缓存它又分为2种:强制缓存和协商缓存。
协商缓存原理:客户端向服务器端发出请求,服务端会检测是否有对应的标识,如果没有对应的标识,服务器端会返回一个对应的标识给客户端,客户端下次再次请求的时候,把该标识带过去,然后服务器端会验证该标识,如果验证通过了,则会响应304,告诉浏览器读取缓存。如果标识没有通过,则返回请求的资源。
那么协商缓存的标识又有2种:ETag/if-None-Match 和 Last-Modified/if-Modify-Since
二:协商缓存Last-Modified/if-Modify-Since
浏览器第一次发出请求一个资源的时候,服务器会返回一个last-Modify到hearer中. Last-Modify 含义是最后的修改时间。
当浏览器再次请求的时候,request的请求头会加上 if-Modify-Since,该值为缓存之前返回的 Last-Modify. 服务器收到if-Modify-Since后,根据资源的最后修改时间(last-Modify)和该值(if-Modify-Since)进行比较,如果相等的话,则命中缓存,返回304,否则, 如果 Last-Modify > if-Modify-Since, 则会给出200响应,并且更新Last-Modify为新的值。
下面我们使用node来模拟下该场景。基本的代码如下:
1 import Koa from 'koa'; 2 import path from 'path'; 3 4 //静态资源中间件 5 import resource from 'koa-static'; 6 const app = new Koa(); 7 const host = 'localhost'; 8 const port = 7788; 9 10 const url = require('url'); 11 const fs = require('fs'); 12 const mime = require('mime'); 13 14 app.use(async(ctx, next) => { 15 // 获取文件名 16 const { pathname } = url.parse(ctx.url, true); 17 // 获取文件路径 18 const filepath = path.join(__dirname, pathname); 19 const req = ctx.req; 20 const res = ctx.res; 21 // 判断文件是否存在 22 fs.stat(filepath, (err, stat) => { 23 if (err) { 24 res.end('not found'); 25 } else { 26 // 获取 if-modified-since 这个请求头 27 const ifModifiedSince = req.headers['if-modified-since']; 28 // 获取最后修改的时间 29 const lastModified = stat.ctime.toGMTString(); 30 // 判断两者是否相等,如果相等返回304读取浏览器缓存。否则的话,重新发请求 31 if (ifModifiedSince === lastModified) { 32 res.writeHead(304); 33 res.end(); 34 } else { 35 res.setHeader('Content-Type', mime.getType(filepath)); 36 res.setHeader('Last-Modified', stat.ctime.toGMTString()); 37 // fs.createReadStream(filepath).pipe(res); 38 } 39 } 40 }); 41 await next(); 42 }); 43 44 app.use(resource(path.join(__dirname, './static'))); 45 46 app.listen(port, () => { 47 console.log(`server is listen in ${host}:${port}`); 48 });
当我们第一次访问的时候(清除浏览器的缓存),如下图所示:
当我们继续刷新浏览器的时候,我们再看下如下数据:
如上可以看到,当我们第二次请求的时候,请求头部加上了 If-Modified-Since 该参数,并且该参数的值该响应头中的时间相同。因此返回304状态。
三:协商缓存ETag/if-None-Match
ETag的原理和上面的last-modified是类似的。ETag则是对当前请求的资源做一个唯一的标识。该标识可以是一个字符串,文件的size,hash等。只要能够合理标识资源的唯一性并能验证是否修改过就可以了。ETag在服务器响应请求的时候,返回当前资源的唯一标识(它是由服务器生成的)。但是只要资源有变化,ETag会重新生成的。浏览器再下一次加载的时候会向服务器发送请求,会将上一次返回的ETag值放到request header 里的 if-None-Match里面去,服务器端只要比较客户端传来的if-None-Match值是否和自己服务器上的ETag是否一致,如果一致说明资源未修改过,因此返回304,如果不一致,说明修改过,因此返回200。并且把新的Etag赋值给if-None-Match来更新该值。
last-modified 和 ETag之间对比
1. 在精度上,ETag要优先于 last-modified。
2. 在性能上,Etag要逊于Last-Modified,Last-Modified需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
3. 在优先级上,服务器校验优先考虑Etag。
下面我们继续使用node来演示下:基本代码如下:
1 import path from 'path'; 2 import Koa from 'koa'; 3 4 //静态资源中间件 5 import resource from 'koa-static'; 6 const app = new Koa(); 7 const host = 'localhost'; 8 const port = 7878; 9 10 const url = require('url'); 11 const fs = require('fs'); 12 const mime = require('mime'); 13 /* 14 const crypto = require('crypto'); 15 app.use(async(ctx, next) => { 16 // 获取文件名 17 const { pathname } = url.parse(ctx.url, true); 18 // 获取文件路径 19 const filepath = path.join(__dirname, pathname); 20 const req = ctx.req; 21 const res = ctx.res; 22 // 判断文件是否存在 23 fs.stat(filepath, (err, stat) => { 24 if (err) { 25 res.end('not found'); 26 } else { 27 console.log(111); 28 // 获取 if-none-match 这个请求头 29 const ifNoneMatch = req.headers['if-none-match']; 30 const readStream = fs.createReadStream(filepath); 31 const md5 = crypto.createHash('md5'); 32 // 通过流的方式读取文件并且通过md5进行加密 33 readStream.on('data', (d) => { 34 console.log(333); 35 console.log(d); 36 md5.update(d); 37 }); 38 readStream.on('end', () => { 39 const eTag = md5.digest('hex'); 40 // 验证Etag 是否相同 41 if (ifNoneMatch === eTag) { 42 res.writeHead(304); 43 res.end(); 44 } else { 45 res.setHeader('Content-Type', mime.getType(filepath)); 46 // 第一次服务器返回的时候,会把文件的内容算出来一个标识,发给客户端 47 fs.readFile(filepath, (err, content) => { 48 // 客户端看到etag之后,也会把此标识保存在客户端,下次再访问服务器的时候,发给服务器 49 res.setHeader('Etag', etag); 50 // fs.createReadStream(filepath).pipe(res); 51 }); 52 } 53 }); 54 } 55 }); 56 await next(); 57 }); 58 */ 59 // 我们这边直接使用 现成的插件来简单的演示下。如果要比较的话,可以看上面的代码原理即可 60 import conditional from 'koa-conditional-get'; 61 import etag from 'koa-etag'; 62 app.use(conditional()); 63 app.use(etag()); 64 65 app.use(resource(path.join(__dirname, './static'))); 66 67 app.listen(port, () => { 68 console.log(`server is listen in ${host}:${port}`); 69 });
如上基本代码,当我们第一次请求的时候(先清除浏览器缓存),可以看到如下图所示:
如上我们可以看到返回值里面有Etag的值。
然后当我们再次刷新浏览器代码的时候,浏览器将会带上 if-None-Match请求头,并赋值为上一次返回头的Etag的值。
然后和服务器端的Etag的值进行对比,如果相等的话,就会返回304 Not Modified。如下图所示:
我们再来改下html的内容,我们再来刷新下看看,可以看到页面内容发生改变了,因此Etag值是不一样的。如下图所示
然后我们继续刷新,就会返回304了,因为它会把最新的Etag的值赋值给 if-None-Match请求头,然后请求的时候,会把该最新值带过去,因此如下图所示可以看到。
如上就是协商缓存的基本原理了。下面我们来看下强制缓存。
四:理解强制缓存
基本原理:浏览器在加载资源的时候,会先根据本地缓存资源的header中的信息(Expires 和 Cache-Control)来判断是否需要强制缓存。如果命中的话,则会直接使用缓存中的资源。否则的话,会继续向服务器发送请求。
Expires
Expires 是http1.0的规范,它的值是一个绝对时间的GMT格式的时间字符串。这个时间代表的该资源的失效时间,如果在该时间之前请求的话,则都是从缓存里面读取的。但是使用该规范时,可能会有一个缺点就是当服务器的时间和客户端的时间不一样的情况下,会导致缓存失效。
Cache-Control
Cache-Control 是http1.1的规范,它是利用该字段max-age值进行判断的。该值是一个相对时间,比如 Cache-Control: max-age=3600, 代表该资源的有效期是3600秒。除了该字段外,我们还有如下字段可以设置:
no-cache: 需要进行协商缓存,发送请求到服务器确认是否使用缓存。
no-store:禁止使用缓存,每一次都要重新请求数据。
public:可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器。
private:只能被终端用户的浏览器缓存,不允许 CDN 等中继缓存服务器对其缓存。
Cache-Control 与 Expires 可以在服务端配置同时启用,同时启用的时候 Cache-Control 优先级高。
下面我们来看下使用 max-age 设置多少秒后过期来验证下。最基本的代码如下:
1 import path from 'path'; 2 import Koa from 'koa'; 3 4 //静态资源中间件 5 import resource from 'koa-static'; 6 const app = new Koa(); 7 const host = 'localhost'; 8 const port = 7878; 9 10 app.use(async (ctx, next) => { 11 // 设置响应头Cache-Control 设置资源有效期为300秒 12 ctx.set({ 13 'Cache-Control': 'max-age=300' 14 }); 15 await next(); 16 }); 17 18 app.use(resource(path.join(__dirname, './static'))); 19 20 app.listen(port, () => { 21 console.log(`server is listen in ${host}:${port}`); 22 });
如上我们设置了300秒后过期,也就是有效期为5分钟,当我们第一次请求页面的时候,我们可以查看下如下所示:
我们可以看到响应头中有Cache-Control字段 max-age=300 这样的,并且状态码是200的状态。
下面我们继续来刷新下页面,可以看到请求如下所示:
请求是200,但是数据是从内存里面读取,如上截图可以看到。
我们现在再把该页面关掉,重新打开新的页面,打开控制台网络,再查看下可以看到如下所示:
因为内存是存在进程中的,当我们关闭页面的时候,内存中的资源就被释放掉了,但是磁盘中的数据是永久的,如上我们可以看到数据从硬盘中读取的。
如上设置的有效期为5分钟,5分钟过后我们再来刷新下页面。如下所示:
如上可以看到 5分钟过期后,就不会从内存或磁盘中读取了,而是重新请求下服务器的资源。如上就是使用 max-age 来演示强制缓存的了。
16.什么是重排重绘
Dom树
Dom树,主要是用来表示页面的Dom结构。
渲染树
渲染树主要是用来表示页面是如何进行渲染的。
Dom树中,除了隐藏节点,其余的节点需要与渲染树中的至少存在一个对应的节点。渲染树中的每一个节点,被称为帧或者是盒子。盒子具有内边距,外边距,边框,位置等属性。一旦渲染树构建完成之后,浏览器就开始进行绘制页面。
当Dom的变化影响到了元素的几何属性(宽和高等)——比如说修改了边框的宽度,或者是修改了高度,又或者给文章增加了内容导致元素的高度增加等,会引起浏览器进行重新计算元素的几何属性,同样,其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效。并重新构建渲染树,这个过程称为重排。完成重排之后,浏览器会重新绘制受影响的元素,这个过程被称为重绘。
并不是所有的Dom变化会影响元素的几何属性,例如,改变背景色,不会影响元素的几何属性,因此,这个时候是不会发生重排,仅仅会发生重绘,因为,元素的不布局没有发生变化。重排和重绘的代价都是昂贵的操作,他们会导致浏览器的UI线程卡顿,因此尽可能避免这类操作。
下面就是整个的基本流程图:
什么时候会发生重排
正如前面所说的,当页面的布局和几何属性发生改变的时候,就需要进行重排
。以下的情况也同样会发生重排:
- 添加和或者删除可见的DOM元素
- 元素的位置发生变化
- 元素的尺寸发生变化(包括:外边距,内边距,边框厚度,宽度,高度等属性发生改变)
- 内容发生变化(例如:内容增加引起高度变化或者是图片被另外一个不同尺寸的图片所替换)
- 页面渲染器进行初始化的
- 浏览器窗口尺寸发生改变
根据改变的内容,渲染树中相对应的部分也需要进行计算。有些改变会触发整个页面的重排:例如,当滚动条出现的时候。
由于每次的重排都会产生计算消耗,大多数浏览器通过队列化修改并批量来优化 重排过程。但是,有些时候我们会强制进行刷新队列,并要求计划任务立刻执行。这些方法包括以下 方法:
- offsetTop,offsetLeft,offsetWidth,offsetHeight
- scrollTop,scrollLeft,scrollWidth,scrollHeight
- clientTop,clientLeft,clientWidth,clientHeight
- getComputedStyle() (currentStyle in IE)
以上的属性和方法需要返回最新的布局信息,因此浏览器不得不执行渲染队列中的“待处理变化”并触发重排,以返回正确的值。因此,修改样式的过程中,最好避免使用以上的属性或者是方法。
如何优化重排效率
前面说到,重排和重绘的代价其实是非常昂贵的,因此,为了提高程序的响应速度,我们在平时的开发过程中应该尽量减少该操作的发生。为了减少重排或者是重绘的发生次数,我们可以有以下几点的操作。
合并对Dom的多次修改
var el = document.getElementById('mydiv'); el.style.width = '300px'; el.style.height = '400px'; el.style.margin = '15px';
在以上打代码中,我们能够看到对元素的几何属性发生了三次的修改,因此,上面的代码中会触发三次的重排和重绘。因此,我们可以将对元素的三次修改合并成一次修改,这样,就智慧触发一次重排重绘。修改后的代码如下:
var el = document.getElementById('mydiv'); el.style.cssText = 'width:30px;height:400px;margin:15px';
批量修改dom
当我们需要对Dom进行一系列操作时,可以通过以下步骤来减少重绘和重排:
-
是元素脱离文档流
-
对其应用多重改变
-
把元素带回文档中
该过程会触发两次重排,第一步和第三步,如果忽略这两个步骤,那么第二步的修改就会触发多次的重排。这里要说的是,怎么才能使元素脱离文档流?主要的又以下几个方法:
-
隐藏元素,应用修改,重新显示
-
使用文档片段,在当前Dom之外构建一个子树,再把拷贝会文档
-
将原始元素拷贝到一个脱离文档的节点中,修改副本,完成后再替换原始元素
-
使用虚拟Dom
接下来,就演示一下有关如何脱离文档流,来进行批量修改Dom的。我们先来构建一个场景:
<ul id="myList"> <li><a href="https://www.baidu.com">baidu</a></li> <li><a href="https://www.qq.com">qq</a></li> </ul>
上面是一个列表,假设我们要向上述列表中添加以下的数据:
var data = [ { url: 'https://www.cnblogs.com/', name: '博客园' }, { url: 'https://weibo.com/', name: '新浪微博' } ]
如果按照我们习惯性的思维,我们会这么去写:
function appendDataToElement (appendToElement, data) { var a; var li; for (var i = 0; i <data.length; i++) { a = document.createElement('a'); li = document.createElement('li'); a.href = data[i].url; a.appendChild(document.createTextNode(data[i].name)); li.appendChild(a); appendToElement.appendChild(li); } } var appendToElement = document.getElementById('myList'); appendDataToElement(appendToElement, data);
这样写虽说是能够实现我们所想要的功能,但这样做的话,会在每次appendChild之后,引起浏览器的重排和重绘,如果数据量特别大的时候,就会发生很多次的重排和重绘,因此我们需要对上面的方法进行修改:
var appendToElement = document.getElementById('myList'); appendToElement.style.display = 'none'; appendDataToElement(appendToElement, data); appendToElement.style.display = 'block';
这样话就只用渲染两次,这就是上面所说的隐藏元素,应用修改,重新显示;
接下来实现一下后面的两种(除虚拟dom之外的方法)。
// 创建子树的方法 var appendToElement = document.createDocumentFragment(); appendDataToElement(appendToElement, data); document.getElementById('myList').appendChild(appendToElement); // 将原始元素拷贝到一个脱离文档的节点中 var old = document.getElementById('myList'); var clone = old.cloneNode(); appendDataToElement(appendToElement, data); old.parentNode.replaceChild(clone, old);
针对于以上的方法,这边推荐使用构建子树的方法是创建子树的方法,因为他们所产生的的Dom遍历和重排次数最少,唯一潜在的问题就是文档片段未被充分利用。
还有一种就是虚拟Dom,有关虚拟Dom这个,其实可以参考Vue和React等比较现代的前端开发的内容。
18.class和普通构造函数
普通构造函数
1 //JS构造函数 2 function MathHandle(x,y){ 3 this.x = x; 4 this.y = y; 5 } 6 MathHandle.prototype.add = function(){ 7 return this.x + this.y; 8 } 9 let test = new MathHandle(1,2); 10 console.log(test.add()); 11 console.log(typeof MathHandle); 12 console.log(MathHandle.prototype.constructor === MathHandle); 13 console.log(test.__proto__ === MathHandle.prototype); 14 // 3 15 // function 16 // true 17 // true
Class基本语法
1 //ES6的语法糖,本质还是JavaScript面向对象那套东西。最终还是会转换成上面的代码 2 //形式上强行模仿了java,c#,有点失去了JavaScript的个性 3 class MathHandle { 4 constructor(x,y) { 5 this.x = x; 6 this.y = y; 7 } 8 9 add() { 10 return this.x + this.y; 11 } 12 } 13 const m = new MathHandle(1,2); 14 console.log(m.add()); 15 16 console.log(typeof MathHandle) // 'function' 17 console.log(MathHandle.prototype.constructor === MathHandle) //true 18 //构造函数的显式原型对象等于实例的隐式原型对象 19 console.log(m.__proto__ === MathHandle.prototype) // true
原生继承
1 function Animal() { 2 this.eat = function() { 3 console.log('animal eat') 4 } 5 } 6 function Dog() { 7 this.break = function() { 8 console.log('dog bark'); 9 } 10 } 11 Dog.prototype = new Animal(); 12 //哈士奇 13 var hashiqi = new Dog(); 14 hashiqi.bark(); 15 hashiqi.eat();
Class继承
1 class Animal { 2 constructor(name) { 3 this.name = name; 4 } 5 eat() { 6 console.log(this.name + 'eat'); 7 } 8 } 9 class Dog extends Animal { 10 constructor(name) { 11 super(name); 12 } 13 say() { 14 console.log(this.name + 'say'); 15 } 16 } 17 const dog = new Dog('哈士奇'); 18 dog.say(); 19 dog.eat();
综上所述
Class 在语法上更加贴合面向对象的写法
Class 实现继承更加易读、易理解
更易于写 java 等后端语言的使用
本质还是语法糖,使用 prototype
19.vue 数据双向绑定原理
实现原理:
首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。因此接下去我们执行以下3个步骤,实现数据的双向绑定:
1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器
总结:
首先我们为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;然后在编译的时候在该属性的数组dep中添加订阅者,v-model会添加一个订阅者,{{}}也会,v-bind也会,只要用到该属性的指令理论上都会,接着为input会添加监听事件,修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。
20.原型和原型链
一、原型
①所有
引用类型
都有一个__proto__(隐式原型)
属性,属性值是一个普通的对象
②所有函数
都有一个prototype(原型)
属性,属性值是一个普通的对象
③所有引用类型的__proto__
属性指向
它构造函数的prototype
var a = [1,2,3]; a.__proto__ === Array.prototype; // true
二、原型链
当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。
举例,有以下代码
function Parent(month){ this.month = month; } var child = new Parent('Ann'); console.log(child.month); // Ann console.log(child.father); // undefined
在child中查找某个属性时,会执行下面步骤
:
访问链路
为:
参考地址:https://www.cnblogs.com/loveyaxin/p/11151586.html①一直往上层查找,直到到null还没有找到,则返回
undefined
②Object.prototype.__proto__ === null
③所有从原型或更高级原型中的得到、执行的方法,其中的this在执行时,指向当前这个触发事件执行的对象
21.js继承的几种方式
// 父类 function Person(name) { // 给构造函数添加了参数 this.name = name; this.sum = function () { console.log(this.name) } } Person.prototype.age = 10; // 给构造函数t添加原型属性
一、原型链继承
1 // 1.原型链继承 2 function Per() { 3 this.name = 'per'; 4 } 5 Per.prototype = new Person(); // 主要 6 let per1 = new Per(); 7 per1.age = 12 8 console.log(per1.age) //12 9 // instanceof 判断元素是否在另一个元素的原型链上 10 // per1 继承了Person的属性,返回true 11 console.log(per1 instanceof Person) // true 12 per1.sum() // per
重点:让新实例的原型等于父类的实例。
特点:1、实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。(新实例不会继承父类实例的属性!)
缺点:1、新实例无法向父类构造函数传参。
2、继承单一。
3、所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)
二、借用构造函数继承
// 2.借用构造函数继承 function Con(){ Person.call(this,'Con') this.age = 12 } var con1 = new Con(); console.log(con1.name) // Con console.log(con1.age) // 12 console.log(con1 instanceof Person) // false
重点:用.call()和.apply()将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))
特点:1、只继承了父类构造函数的属性,没有继承父类原型的属性。
2、解决了原型链继承缺点1、2、3。
3、可以继承多个构造函数属性(call多个)。
4、在子实例中可向父实例传参。
缺点:1、只能继承父类构造函数的属性。
2、无法实现构造函数的复用。(每次用每次都要重新调用)
3、每个新实例都有父类构造函数的副本,臃肿。
三、组合继承(组合原型链继承和借用构造函数继承)(常用)
// 3.组合原型链和构造函数继承 function SubType(name){ Person.call(this,name) // 借用构造函数模式 } SubType.prototype = new Person(); // 原型链继承 var sub = new SubType('SubType'); console.log(sub.name); //"SubType"继承了构造函数属性 console.log(sub.age); //10继承了父类原型的属性
重点:结合了两种模式的优点,传参和复用
特点:1、可以继承父类原型上的属性,可以传参,可复用。
2、每个新实例引入的构造函数属性是私有的。
缺点:调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
四、原型式继承
/ 4.原型式继承 // 先封装一个函数容器,用来输出对象和承载的原型 function content(obj){ function F(){} F.prototype = obj; // 继承了传入的参数 return new F(); // 返回函数对象 } var sup = new Person(); // 拿到父类的实例 var sup1 = content(sup); console.log(sup1.age) // 10 继承了父类函数的属性
重点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。
特点:类似于复制一个对象,用函数来包装。
缺点:1、所有实例都会继承原型上的属性。
2、无法实现复用。(新实例属性都是后面添加的)
五、寄生式继承
// 5. 寄生式继承 function content(obj){ function F(){} F.prototype = obj; // 继承了传入的参数 return new F(); // 返回函数对象 } var sup = new Person(); // 以上是原型式继承,给原型式继承再套个壳子传参数 function subObject(obj){ var sub = content(obj); sub.name = 'subObject'; return sub; } var sup2 = subObject(sup); //这个函数经过声明之后就成了可增添属性的对象 console.log(typeof subObject) // function console.log(typeof sup2) // Object console.log(sup2.name) // subObject,返回了sub对象,继承了sub的属性
重点:就是给原型式继承外面套了个壳子。
优点:没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。
缺点:没用到原型,无法复用。
六、寄生组合式继承(常用)
寄生:在函数内返回对象然后调用
组合:1、函数的原型等于另一个实例。2、在函数中用apply或者call引入另一个构造函数,可传参
1 // 6.寄生组合式继承(常用) 2 function content(obj){ 3 function F(){} 4 F.prototype = obj; // 继承了传入的参数 5 return new F(); // 返回函数对象 6 } 7 // content 就是F实例的另一种表示法 8 var con = content(Person.prototype) 9 // con实例(F实例)的原型继承了父类函数的原型 10 // 上述更像是原型链继承,只不过只继承了原型属性 11 12 // 组合 13 function Sub(){ 14 Person.call(this); // 这个继承了父类构造函数的属性 15 } // 解决了组合式两次调用构造函数的属性的缺点 16 // 重点 17 Sub.prototype = con; // 继承了con实例 18 con.constructor = Sub; //一定要修复实例 19 var sub1 = new Sub(); 20 // Sub的实例就继承了构造函数的属性,父类实例,con的函数属性 21 console.log(sub1.age) // 10
重点:修复了组合继承的问题
最后还有 ES6 class继承
1 / es6的继承 2 class Parent { 3 constructor(name) { 4 this.name = name; 5 } 6 say() { 7 console.log('parent has say method. ' + this.name); 8 } 9 } 10 11 class Child extends Parent { 12 constructor(name) { 13 super(name); 14 } 15 16 fly() { 17 console.log('chid can fly...'); 18 } 19 } 20 var p = new Child('tom'); 21 p.say(); 22 p.fly();
继承这些知识点与其说是对象的继承,更像是函数的功能用法,如何用函数做到复用,组合,这些和使用继承的思考是一样的。上述几个继承的方法都可以手动修复他们的缺点,但就是多了这个手动修复就变成了另一种继承模式。
这些继承模式的学习重点是学它们的思想,不然你会在coding书本上的例子的时候,会觉得明明可以直接继承为什么还要搞这么麻烦。就像原型式继承它用函数复制了内部对象的一个副本,这样不仅可以继承内部对象的属性,还能把函数(对象,来源内部对象的返回)随意调用,给它们添加属性,改个参数就可以改变原型对象,而这些新增的属性也不会相互影响。
22.盒模型
当对一个文档进行布局(lay out)的时候,浏览器的渲染引擎会根据标准之一的 CSS 基础框盒模型(CSS basic box model),将所有元素表示为一个个矩形的盒子(box)。CSS 决定这些盒子的大小、位置以及属性(例如颜色、背景、边框尺寸…)。
每个盒子由四个部分(或称区域)组成,其效用由它们各自的边界(Edge)所定义(原文:defined by their respective edges,可能意指容纳、包含、限制等)。如图,与盒子的四个组成区域相对应,每个盒子有四个边界:内容边界 Content edge、内边距边界 Padding Edge、边框边界 Border Edge、外边框边界 Margin Edge。
从上图可以看出,w3c盒子模型的范围包括margin、border、padding、content,并且content部分不包含其他部分。
2.IE盒模型:box-sizing:border-box;
从上图可以看出,IE盒子模型的范围包括margin、border、padding、content,和w3c盒子模型不同的是,IE盒子模型的content部分包含了padding和border.
注意:
IE5.5及更早的版本使用的是IE盒模型。IE6及其以上的版本在标准兼容模式下使用的是W3C的盒模型标准。我们说这是一个好消息因为这意味着此盒模型问题,只会出现在IE5.5及其更早的版本中。只要为文档设置一个DOCTYPE,就会使得IE遵循标准兼容模式的方式工作。另外,我们现在应该能理解了,css3的box-sizing属性给了开发者选择盒模型解析方式的权利。W3C的盒模型方式被称为“content-box”,IE的被称为“border-box”,使用box-sizing: border-box;就是为了在设置有padding值和border值的时候不把宽度撑开。
23.position
值 | 描述 |
---|---|
absolute |
生成绝对定位的元素,相对于 static 定位以外的第一个父元素进行定位。 元素的位置通过 "left", "top", "right" 以及 "bottom" 属性进行规定。 |
fixed |
生成绝对定位的元素,相对于浏览器窗口进行定位。 元素的位置通过 "left", "top", "right" 以及 "bottom" 属性进行规定。 |
relative |
生成相对定位的元素,相对于其正常位置进行定位。 因此,"left:20" 会向元素的 LEFT 位置添加 20 像素。 |
static | 默认值。没有定位,元素出现在正常的流中(忽略 top, bottom, left, right 或者 z-index 声明)。 |
inherit | 规定应该从父元素继承 position 属性的值。 |
24.v-bind 可传对象
post: { id: 1, title: 'My Journey with Vue' }
下面的模板:
<blog-post v-bind="post"></blog-post>
等价于:
<blog-post
v-bind:id="post.id"
v-bind:title="post.title"
></blog-post>
25.es6 模块化
Es6模块化的优点
- 预声明的方式导入(在一开始确定依赖关系)
- 多种导入导出方式
Es6缺点:某些情况下效率低.相比CommonJs依赖延迟申明两者优缺点正好相反。
Es6引入入口文件:使用type属性来告知我这个是一个模块
<script src="./index.js" type="module"></script>
Es6 分为基本导出和默认导出 两种方式可以同时存在
//a.js文件export var a = 1; export function print() { console.log(a); } export class Message { } let name = 'zwq'; let age = 18; // 注意这里的{}不是一个对象,只是一种特殊的语法,表示导出括号里面的内容 export { name, age } /** * 上面的导出的内容有 * 变量a * 函数print * 类Message * 变量name * 变量age */
- 基本导出
基本导出类似于CommonJs的exports。
语法: export 声明表达式 或 export {具名符号}
- 基本导入
由于使用的是依赖预加载,因此导入任何其他模块,导入代码必须当时到所有代码之前
语法:import {具名符号列表} from "模块路径"
导入时可以使用as关键字进行重命名
导入时的符号是常量不可修改,不能重新赋值
导入时使用 * 导入导出的全部内容保存在一个对象中,这里要使用as关键字对*进行重命名
//index.js入口文件 import{name,age,a as moduleA} from "./a.js"; import {b} from "./b.js"; import * as Amoudle from "./a.js" var a = 5; console.log(a); console.log(name,age); console.log(b); console.log(moduleA);
console.log(Amoudle);
- 默认导出
每个模块除了允许有多个基本导出之外,还允许有一个默认导出
默认导出类似于CommonJs的module.exports,由于只有一个,因此无需具名
语法:export default 默认导出的数据 或 export {默认导出数据 as default}
//a.js文件
// 基本导出 a export var a = 1; export var b = 2; export var c = 3 // 默认导出 a变量的值 export default a;
//b.js文件 var num = 1; export default { print(){ console.log(num); }, name:'zwq', age:18, sayName(){ return this.name } }
- 默认导入
语法:import 随便起一个变量名 from "文件路径"
- 默认导入和基本导入一起使用
语法:import 默认导入,{基本导入} from "文件路径"
- 基本导出直接把默认的也一起导出
语法 export {基本导出,默认导出 as default}
import m from "./a.js" import bMoudle from "./b.js" // 导入所有的导出(基本导出和默认导出-->default对象) import * as all from "./a.js" // 将默认导出放在def里,将基本到处放在a,b变量里 import def,{a,b} from "./a.js" console.log(a); console.log(m); console.log(bMoudle.sayName()); console.log(all);
如果只是运行一下模块内容,没有导出的内容,直接使用 import "模块路径"
拓展:当需要对对个模块进行整合到一个模块时(将多个模块导入到一个模块,在这个模块进行导出)export {导出具名符号} from “文件路径”
export {a,b} from "./m1.js" 导出m1文件的a和b
export {k,default,a as m2A} form "./m2.js" 导出m2文件的 k,m2的默认导出,m2的a并改名为m2A
export const r = "自己本模块的内容"
参考地址:https://es6.ruanyifeng.com/#docs/module
26.vue 懒加载组件和路由懒加载
1 import Vue from 'vue' 2 import Router from 'vue-router' 3 import HelloWorld from '@/components/HelloWorld' 4 5 Vue.use(Router) 6 7 export default new Router({ 8 routes: [ 9 { 10 path: '/', 11 name: 'HelloWorld', 12 component: HelloWorld 13 } 14 ] 15 })
2、vue异步组件实现懒加载
方法如下:component:resolve=>(require(['需要加载的路由的地址']),resolve)
1 import Vue from 'vue' 2 import Router from 'vue-router' 3 /* 此处省去之前导入的HelloWorld模块 */ 4 Vue.use(Router) 5 6 export default new Router({ 7 routes: [ 8 { 9 path: '/', 10 name: 'HelloWorld', 11 component: resolve=>(require(["@/components/HelloWorld"],resolve)) 12 } 13 ] 14 })
3、ES 提出的import方法,(------最常用------)
方法如下:const HelloWorld = ()=>import('需要加载的模块地址')
import Vue from 'vue' import Router from 'vue-router' Vue.use(Router) // 没有指定webpackChunkName,每个组件打包成一个js文件。 const HelloWorld = ()=>import("@/components/HelloWorld") // 指定了相同的webpackChunkName,会合并打包成一个js文件。 Webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。 // const Home = () => import(/* webpackChunkName: 'home' */ '@/components/home') export default new Router({ routes: [ { path: '/', name: 'HelloWorld', component:HelloWorld } ] })
https://es6.ruanyifeng.com/#docs/module#import
4.webpack提供的require.ensure()
第一个参数的依赖关系是一个数组,代表了当前需要进来的模块的一些依赖;
第二个参数回调就是一个回调函数其中需要注意的是,这个回调函数有一个参数要求,通过这个要求就可以在回调函数内动态引入其他模块值得注意的是,虽然这个要求是回调函数的参数,理论上可以换其他名称,但是实际上是不能换的,否则的的的的WebPack就无法静态分析的时候处理它;
第三个参数errorCallback比较好理解,就是处理错误的回调;
第四个参数chunkName则是指定打包的组块名称。
// r就是resolve const list = r => require.ensure([], () => r(require('../components/list/list')), 'list'); // 路由也是正常的写法 这种是官方推荐的写的 按模块划分懒加载 const router = new Router({ routes: [ { path: '/list/blog', component: list, name: 'blog' } ] })
https://zhuanlan.zhihu.com/p/55990173
四、组件懒加载
相同与路由懒加载,
1、原来组件中写法
1 <template> 2 <div class="hello"> 3 <One-com></One-com> 4 1111 5 </div> 6 </template> 7 8 <script> 9 import One from './one' 10 export default { 11 components:{ 12 "One-com":One 13 }, 14 data () { 15 return { 16 msg: 'Welcome to Your Vue.js App' 17 } 18 } 19 } 20 </script>
2、const方法
1 <template> 2 <div class="hello"> 3 <One-com></One-com> 4 1111 5 </div> 6 </template> 7 8 <script> 9 const One = ()=>import("./one"); 10 export default { 11 components:{ 12 "One-com":One 13 }, 14 data () { 15 return { 16 msg: 'Welcome to Your Vue.js App' 17 } 18 } 19 } 20 </script>
3、异步方法
<template> <div class="hello"> <One-com></One-com> 1111 </div> </template> <script> export default { components:{ "One-com":resolve=>(['./one'],resolve) }, data () { return { msg: 'Welcome to Your Vue.js App' } } } </script>
五、总结:
路由和组件的常用两种懒加载方式:
1、vue异步组件实现路由懒加载
component:resolve=>(['需要加载的路由的地址',resolve])
2、es提出的import(推荐使用这种方式)
const HelloWorld = ()=>import('需要加载的模块地址')
27.Vue 中hash路由和history路由原理及优缺点
目前单页应用(SPA)越来越成为前端主流,单页应用一大特点就是使用前端路由,由前端来直接控制路由跳转逻辑,而不再由后端人员控制,这给了前端更多的自由。
目前前端路由主要有两种实现方式:hash
模式和 history
模式,下面分别详细说明。
1. hash模式
比如在用超链接制作锚点跳转的时候,就会发现,url后面跟了"#id",hash值就是url中从"#"号开始到结束的部分。
hash值变化浏览器不会重新发起请求,但是会触发window.hashChange
事件,假如我们在hashChange事件中获取当前的hash值,并根据hash值来修改页面内容,则达到了前端路由的目的。
1 <!-- html:菜单中href设置为hash形式,id为app中放置页面内容 --> 2 <ul id="menu"> 3 <li> 4 <a href="#index">首页</a> 5 </li> 6 <li> 7 <a href="#news">资讯</a> 8 </li> 9 <li> 10 <a href="#user">个人中心</a> 11 </li> 12 13 </ul> 14 15 <div id="app"></div>
1 //在window.onhashchange中获取hash值,根据不同的值,修改app中不同的内容,起到了路由的效果 2 function hashChange(e){ 3 // console.log(location.hash) 4 // console.log(location.href) 5 // console.log(e.newURL) 6 // console.log(e.oldURL) 7 8 let app = document.getElementById('app') 9 switch (location.hash) { 10 case '#index': 11 app.innerHTML = '<h1>这是首页内容</h1>' 12 break 13 case '#news': 14 app.innerHTML = '<h1>这是新闻内容</h1>' 15 break 16 case '#user': 17 app.innerHTML = '<h1>这是个人中心内容</h1>' 18 break 19 default: 20 app.innerHTML = '<h1>404</h1>' 21 } 22 } 23 window.onhashchange = hashChange 24 hashChange()
上面这个实现方式比较简陋,我们可以再封装一下:
1 class Router { 2 constructor(){ 3 this.routers = [] //存放我们的路由配置 4 } 5 add(route,callback){ 6 this.routers.push({ 7 path:route, 8 render:callback 9 }) 10 } 11 listen(callback){ 12 window.onhashchange = this.hashChange(callback) 13 this.hashChange(callback)() //首次进入页面的时候没有触发hashchange,必须要就单独调用一下 14 } 15 hashChange(callback){ 16 let self = this 17 return function () { 18 let hash = location.hash 19 console.log(hash) 20 for(let i=0;i<self.routers.length;i++){ 21 let route = self.routers[i] 22 if(hash===route.path){ 23 callback(route.render()) 24 return 25 } 26 } 27 } 28 } 29 } 30 31 let router = new Router() 32 router.add('#index',()=>{ 33 return '<h1>这是首页内容</h1>' 34 }) 35 router.add('#news',()=>{ 36 return '<h1>这是新闻内容</h1>' 37 }) 38 router.add('#user',()=>{ 39 return '<h1>这是个人中心内容</h1>' 40 }) 41 router.listen((renderHtml)=>{ 42 let app = document.getElementById('app') 43 app.innerHTML = renderHtml 44 })
实现一个Router类,通过add方法添加路由配置,第一个参数为路由路径,第二个参数为render函数,返回要插入页面的html;通过listen方法,监听hash变化,并将每个路由返回的html,插入到app中。这样我们就实现了一个简单的hash路由。
2. history模式
hash模式看起来是比较丑的,都带个"#"号,我们也可以采取history模式,history就是我们平时看到的正常的连接形式。history模式基于window.history
对象的方法。
在HTML4中,已经支持window.history
对象来控制页面历史记录跳转,常用的方法包括:
history.forward()
:在历史记录中前进一步history.back()
:在历史记录中后退一步history.go(n)
:在历史记录中跳转n步骤,n=0为刷新本页,n=-1为后退一页。
在HTML5中,window.history
对象得到了扩展,新增的API包括:
history.pushState(data[,title][,url])
:向历史记录中追加一条记录history.replaceState(data[,title][,url])
:替换当前页在历史记录中的信息。history.state
:是一个属性,可以得到当前页的state信息。window.onpopstate
:是一个事件,在点击浏览器后退按钮或js调用forward()
、back()
、go()
时触发。监听函数中可传入一个event对象,event.state
即为通过pushState()
或replaceState()
方法传入的data参数
history模式原理可以这样理解,首先我们要改造我们的超链接,给每个超链接增加onclick方法,阻止默认的超链接跳转,改用history.pushState
或history.replaceState
来更改浏览器中的url,并修改页面内容。由于通过history的api调整,并不会向后端发起请求,所以也就达到了前端路由的目的。
如果用户使用浏览器的前进后退按钮,则会触发window.onpopstate
事件,监听页面根据路由地址修改页面内容。
也不一定非要用超链接,任意元素作为菜单都行,只要在点击事件中通过 history 进行调整即可。
1 <!--html:--> 2 <ul id="menu"> 3 <li> 4 <a href="/index">首页</a> 5 </li> 6 <li> 7 <a href="/news">资讯</a> 8 </li> 9 <li> 10 <a href="/user">个人中心</a> 11 </li> 12 13 </ul> 14 <div id="app"></div>
1 //js: 2 //改造超链接,阻止默认跳转,默认的跳转是会刷新页面的 3 document.querySelector('#menu').addEventListener('click',function (e) { 4 if(e.target.nodeName ==='A'){ 5 e.preventDefault() 6 let path = e.target.getAttribute('href') //获取超链接的href,改为pushState跳转,不刷新页面 7 window.history.pushState({},'',path) //修改浏览器中显示的url地址 8 render(path) //根据path,更改页面内容 9 } 10 }) 11 12 function render(path) { 13 let app = document.getElementById('app') 14 switch (path) { 15 case '/index': 16 app.innerHTML = '<h1>这是首页内容</h1>' 17 break 18 case '/news': 19 app.innerHTML = '<h1>这是新闻内容</h1>' 20 break 21 case '/user': 22 app.innerHTML = '<h1>这是个人中心内容</h1>' 23 break 24 default: 25 app.innerHTML = '<h1>404</h1>' 26 } 27 } 28 //监听浏览器前进后退事件,并根据当前路径渲染页面 29 window.onpopstate = function (e) { 30 render(location.pathname) 31 } 32 //第一次进入页面显示首页 33 render('/index')
上面这个写法太low,我们可以用类封装一下,通过 add 方法添加路由,通过 pushState 进行跳转,初始化时更改所以超链接的跳转方式:
1 class Router { 2 constructor(){ 3 this.routers = [] 4 this.renderCallback = null 5 } 6 add(route,callback){ 7 this.routers.push({ 8 path:route, 9 render:callback 10 }) 11 } 12 pushState(path,data={}){ 13 window.history.pushState(data,'',path) 14 this.renderHtml(path) 15 } 16 listen(callback){ 17 this.renderCallback = callback 18 this.changeA() 19 window.onpopstate = ()=>this.renderHtml(this.getCurrentPath()) 20 this.renderHtml(this.getCurrentPath()) 21 } 22 changeA(){ 23 document.addEventListener('click', (e)=> { 24 if(e.target.nodeName==='A'){ 25 e.preventDefault() 26 let path = e.target.getAttribute('href') 27 this.pushState(path) 28 } 29 }) 30 } 31 getCurrentPath(){ 32 return location.pathname 33 } 34 renderHtml(path){ 35 for(let i=0;i<this.routers.length;i++){ 36 let route = this.routers[i] 37 if(path===route.path){ 38 this.renderCallback(route.render()) 39 return 40 } 41 } 42 } 43 } 44 45 let router = new Router() 46 router.add('/index',()=>{ 47 return '<h1>这是首页内容</h1>' 48 }) 49 router.add('/news',()=>{ 50 return '<h1>这是新闻内容</h1>' 51 }) 52 router.add('/user',()=>{ 53 return '<h1>这是个人中心内容</h1>' 54 }) 55 router.listen((renderHtml)=>{ 56 let app = document.getElementById('app') 57 app.innerHTML = renderHtml 58 })
当然,上面这个实现只是一个非常初级的 demo,并不能用于真正的开发场景,只是加深对前端路由的理解。
3. hash模式和history模式的区别
- hash 模式较丑,history 模式较优雅
- pushState 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,故只可设置与当前同文档的 URL
- pushState 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发记录添加到栈中
- pushState 通过 stateObject 可以添加任意类型的数据到记录中;而 hash 只可添加短字符串
- pushState 可额外设置 title 属性供后续使用
- hash 兼容IE8以上,history 兼容 IE10 以上
- history 模式需要后端配合将所有访问都指向 index.html,否则用户刷新页面,会导致 404 错误
参考地址:https://router.vuejs.org/zh/guide/essentials/history-mode.html
28.call,apply,bind 三者的用法区别
call、apply、bind都是改变this指向的方法
- fn.call
当前实例(函数fn)通过原型链的查找机制,找到function.prototype
上的call
方法,function call(){[native code]}
- fn.call()
把找到的call方法执行
当call方法执行的时候,内部处理了一些事情
1.首先把要操作的函数中的this关键字变为call方法第一个传递的实参
2.把call方法第二个及之后的实参获取到
3.把要操作的函数执行,并且把第二个以后传递进来的实参传递给函数
fn.call([this],[param]...)
- call中的细节
- 非严格模式
如果不传参数,或者第一个参数是null
或nudefined
,this
都指向window
1 let fn = function (a, b) { 2 console.log(this, a, b); 3 } 4 let obj = { name: "obj" }; 5 fn.call(obj, 1, 2); // this:obj a:1 b:2 6 fn.call(1, 2); // this:1 a:2 b:undefined 7 fn.call(); // this:window a:undefined b:undefined 8 fn.call(null); // this=window a=undefined b=undefined 9 fn.call(undefined); // this=window a=undefined b=undefined
- 严格模式
第一个参数是谁,this就指向谁,包括null和undefined,如果不传参数this就是undefined
1 "use strict" 2 let fn = function(a,b){ 3 console.log(this,a,b); 4 } 5 let obj = {name:"obj"}; 6 fn.call(obj,1,2); // this:obj a:1 b:2 7 fn.call(1,2); // this:1 a:2 b=undefined 8 fn.call(); // this:undefined a:undefined b:undefined 9 fn.call(null); // this:null a:undefined b:undefined 10 fn.call(undefined); // this:undefined a:undefined b:undefined
2.apply
- apply:和call基本上一致,唯一区别在于传参方式
apply把需要传递给fn的参数放到一个数组(或者类数组)中传递进去,虽然写的是一个数组,但是也相当于给fn一个个的传递
1 fn.call(obj, 1, 2); 2 fn.apply(obj, [1, 2]);
3.bind
- bind:语法和call一模一样,区别在于立即执行还是等待执行,bind不兼容IE6~8
fn.call(obj, 1, 2); // 改变fn中的this,并且把fn立即执行 fn.bind(obj, 1, 2); // 改变fn中的this,fn并不执行
this改变为obj了,但是绑定的时候立即执行,当触发点击事件的时候执行的是fn的返回值undefined
document.onclick = fn.call(obj);
bind会把fn中的this预处理为obj,此时fn没有执行,当点击的时候才会把fn执行
document.onclick = fn.bind(obj);