javascript运行原理
- Parse模块会将JavaScript代码转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码;
- 如果函数没有被调用,那么是不会被转换成AST的;
- Parse的V8官方文档:v8.dev/blog/scanne…
- Ignition是一个解释器,会将AST转换成ByteCode(字节码)
- 同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);
- 如果函数只调用一次,Ignition会执行解释执行ByteCode;
- Ignition的V8官方文档:v8.dev/blog/igniti…
- TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;
- 如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;
- 但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;
- TurboFan的V8官方文档:v8.dev/blog/turbof…
上面是JavaScript代码的执行过程,事实上V8的内存回收也是其强大的另外一个原因,这里暂时先不展开讨论:
- Orinoco模块,负责垃圾回收,将程序中不需要的内存回收;
- Orinoco的V8官方文档:v8.dev/blog/trash-…
解释型语言和编译型语言
编译型语言是代码在运行前编译器将人类可以理解的语言(编程语言)转换成机器可以理解的语言。
解释型语言也是人类可以理解的语言(编程语言),也需要转换成机器可以理解的语言才能执行,但是是在运行时转换的。所以执行前需要环境中安装了解释器;但是编译型语言编写的应用在编译后能直接运行。
跨域
疑问: session和cookie的关系?并且后端通过ctx.session.名获取到的session是前端通过localStorage设置的还是后端通过this.ctx.session.名设置的?
- session是保存在服务端的。就是后端设置session,前端请求时,将
withCredentials
设置为true,就会发送cookie到服务端。
- 服务端执行session机制时候会生成session的id值,这个id值会发送给客户端,客户端每次请求都会把这个id值放到http请求的头部发送给服务端,而这个id值在客户端会保存下来,保存的容器就是cookie,因此当我们完全禁掉浏览器的cookie的时候,服务端的session也会不能正常使用。
- Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
- Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。
cors设置
在后端设置一些特定的响应头。
//设置哪个源可以访问 ctx.set("Access-Control-Allow-Origin", ctx.headers.origin); //允许携带cookie ctx.set("Access-Control-Allow-Credentials", true); //允许那些方法访问我 ctx.set("Access-Control-Request-Method", "PUT,POST,GET,DELETE,OPTIONS"); //允许携带那个头部访问我 ctx.set("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, cc");
关于 cors 的 cookie 问题
想要传递 cookie
需要满足 3 个条件
1.web 请求设置withCredentials
这里默认情况下在跨域请求,浏览器是不带 cookie 的。但是我们可以通过设置 withCredentials
来进行传递 cookie
.
// 原生 xml 的设置方式 var xhr = new XMLHttpRequest(); xhr.withCredentials = true; // axios 设置方式 axios.defaults.withCredentials = true;
2.Access-Control-Allow-Credentials
为 true
3.Access-Control-Allow-Origin
为非 *
这里请求的方式,在 chrome
中是能看到返回值的,但是只要不满足以上其一,浏览器会报错,获取不到返回值。
正向代理
代理的思路为,利用服务端请求不会跨域的特性,让接口和当前站点同域。
就是在各个框架的json文件中,设置一个proxy选项。不同框架设置不同,所以用到时,自己搜索即可。 xxx proxy
JSONP
JSONP
主要就是利用了 script
标签没有跨域限制的这个特性来完成的。只支持get方法。
原理:JSONP 的理念就是,与服务端约定好一个回调函数名,服务端接收到请求后,将返回一段 Javascript,在这段 Javascript 代码中调用了约定好的回调函数,并且将数据作为参数进行传递。当网页接收到这段 Javascript 代码后,就会执行这个回调函数,这时数据已经成功传输到客户端了。
前端代码
前端定义一个回调函数,然后通过请求路径将该回调当成参数传递。
// 1. 创建回调函数callback function Callback(res) { alert(JSON.stringify(res, null , 2)); } document.getElementById('btn-4').addEventListener('click', function() { // 2. 动态创建script标签,并设置src属性,注意参数cb=Callback var script = document.createElement('script'); script.src = 'http://127.0.0.1:3000/api/jsonp?cb=Callback'; document.getElementsByTagName('head')[0].appendChild(script); });
后端代码
通过前端传过来的回调函数,我们将返回该函数的调用。
router.get('/api/jsonp', (req, res, next) => { var str = JSON.stringify(data); // 3. 创建script脚本内容,用`callback`函数包裹住数据 // 形式:callback(data) var script = `${req.query.cb}(${str})`;//这里就是callback(data),当前端请求接口时,就会回调该函数 res.send(script); }); // 4. 前端收到响应数据会自动执行该脚本
WebSocket
这种方式本质没有使用了 HTTP 的响应头, 因此也没有跨域的限制,没有什么过多的解释直接上代码吧。
前端代码
<script> let socket = new WebSocket("ws://localhost:80"); socket.onopen = function() { socket.send("返回的内容"); }; socket.onmessage = function(e) { console.log(e.data); }; </script>
nginx反向代理
以后再学,设计nginx知识。
学习
鉴权
cookie
服务端通过set-cookie设置cookie的信息。
Session
session 是另一种记录服务器和客户端会话状态的机制。session存储在服务器端,该会话对应的key即sessionId会被存储到客户端的cookie中。
根据以上流程可知,session通过cookie来传递sessionId,达到用户鉴权的目的。除此之外,sessionId也可以不通过cookie传递,比如通过response返回客户端,再当作请求的参数传递给服务器去验证。
session-cookie
当需要登录后才可以操作其他的。需要后端设置session, 前端请求时,将服务器返回的session id保存在cookie中。
token
Token认证流程
- 客户端使用用户名跟密码请求登录
- 服务端收到请求,去验证用户名与密码
- 验证成功后,服务端会签发一个
Token
,再把这个Token
发送给客户端
- 客户端收到
Token
以后可以把它存储起来,比如放在Cookie
里或者Local Storage
里
- 客户端每次向服务端请求资源的时候需要带着服务端签发的
Token
- 服务端收到请求,然后去验证客户端请求里面带着的
Token
(request头部添加Authorization),如果验证成功,就向客户端返回请求的数据 ,如果不成功返回401错误码,鉴权失败。
//前端 axios.interceptors.request.use(config => { config.headers.Authorization = window.sessionStorage.getItem("token") return config })
jwt(基于token)
基于 token
的解决方案有许多,常用的是JWT
,JWT
的原理是,服务器认证以后,生成一个 JSON
对象,这个 JSON
对象肯定不能裸传给用户,那谁都可以篡改这个对象发送请求。因此这个 JSON
对象会被服务器端签名加密后返回给用户,返回的内容就是一张令牌,以后用户每次访问服务器端就带着这张令牌。
jwt的组成:Header(头部)、Payload(负载)、Signature(签名)。
- Header部分是一个JSON对象,描述JWT的元数据。一般描述信息为该Token的加密算法以及Token的类型。{"alg": "HS256","typ": "JWT"}的意思就是,该token使用HS256加密,token类型是JWT。这个部分基本相当于明文,它将这个JSON对象做了一个Base64转码,变成一个字符串。Base64编码解码是有算法的,解码过程是可逆的。头部信息默认携带着两个字段。
- Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。有7个官方字段,还可以在这个部分定义私有字段。一般存放用户名、用户身份以及一些JWT的描述字段。它也只是做了一个Base64编码,因此肯定不能在其中存放秘密信息,比如说登录密码之类的。
- Signature是对前面两个部分的签名,防止数据篡改,如果前面两段信息被人修改了发送给服务器端,此时服务器端是可利用签名来验证信息的正确性的。签名需要密钥,密钥是服务器端保存的,用户不知道。算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
//前端代码 //axios的请求拦截器,在每个request请求头上加JWT认证信息 axios.interceptors.request.use( config => { const token = window.localStorage.getItem("token"); if (token) { // 判断是否存在token,如果存在的话,则每个http header都加上token // Bearer是JWT的认证头部信息 config.headers.common["Authorization"] = "Bearer " + token; } return config; }, err => { return Promise.reject(err); } ); //登录方法:在将后端返回的JWT存入localStorage async login() { const res = await axios.post("/login-token", { username: this.username, password: this.password }); localStorage.setItem("token", res.data.token); }, //登出方法:删除JWT async logout() { localStorage.removeItem("token"); }, async getUser() { await axios.get("/getUser-token"); }
//后端代码 const jwt = require("jsonwebtoken"); const jwtAuth = require("koa-jwt"); //用来签名的密钥 const secret = "it's a secret"; router.post("/login-token", async ctx => { const { body } = ctx.request; //登录逻辑,略,即查找数据库,若该用户和密码合法,即将其信息生成一个JWT令牌传给用户 const userinfo = body.username; ctx.body = { message: "登录成功", user: userinfo, // 生成 token 返回给客户端 token: jwt.sign( { data: userinfo, // 设置 token 过期时间,一小时后,秒为单位 exp: Math.floor(Date.now() / 1000) + 60 * 60 }, secret ) }; }); //jwtAuth这个中间件会拿着密钥解析JWT是否合法。 //并且把JWT中的payload的信息解析后放到state中,ctx.state用于中间件的传值。 router.get( "/getUser-token", jwtAuth({ secret }), async ctx => { // 验证通过,state.user console.log(ctx.state.user); ctx.body = { message: "获取数据成功", userinfo: ctx.state.user.data }; } ) //这种密码学的方式使得token不需要存储,只要服务端能拿着密钥解析出用户信息,就说明该用户是合法的。 //若要更进一步的权限验证,需要判断解析出的用户身份是管理员还是普通用户。
疑问:token后端生成后,放在哪里,是通过数据传递给前端,还是通过本地存储技术将其存储到本地,然后前端访问时,取出然后再通过authorization请求头传递给后端?
答: 是通过数据传递给前端,前端保存在本地,在以后需要权限才可以访问的时候,携带即可。
学习自 掘金
预编译*
- 找函数里面的变量声明和形参,此时赋值为undefined
- 形参和实参相统一,就是给形参赋值
- 找函数声明,如果有与函数同名的变量和函数,函数将覆盖变量
- 然后再按照上下顺序执行代码,遇到相同的变量名和函数名,就相互覆盖
- 然后再找赋值语句对相应变量赋值
总之,变量的声明提升早与函数的声明提升
var bar = []; // 定义一个数组 for(var i = 0;i < 10;i++){ bar[i] = function(){ // 每个数组元素定义为一个函数 console.log(i) // 函数体 } } bar[1](); // 10 bar[2](); // 10,都是输出10,深入理解需要掌握“预编译”和“作用域”的知识, // 思考方向 => 函数执行前,存在函数预编译AO(Activation Object)对象
如何验证let,const
声明的变量也存在变量提升
let,const
创建的过程被提升、初始化没有提升
其实他们声明的变量也是存在声明提升的,只是存在暂时性死区,不能在声明之前访问而已。如下面的例子,如果他不存在声明提升,那么这不会报错,会输出zh
let name = 'zh' { console.log(name) // Uncaught ReferenceError: name is not defined let name = 'hy' }
负载均衡
当用户访问网站的时候,先访问一个中间服务器,再让这个中间服务器在服务器集群中选择一个压力较小的服务器,然后将该访问请求引入选择的服务器。这样保证服务器集群中的每个服务器压力趋于平衡。
执行上下文
JavaScript 中有三种执行上下文类型。
- 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置
this
的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
- 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
- Eval 函数执行上下文 — 执行在
eval
函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用eval
。
执行栈
就是当程序开始运行的时候,js引擎会创建一个栈结构,并且创建一个全局的执行上下文将其压入栈中,遇到函数调用时,会创建一个函数执行上下文,将其压入栈中,以此类推。函数执行完毕,将该函数的执行上下文从栈中弹出,将执行权交给栈顶元素。直到该程序执行完毕,将全局执行上下文从栈中弹出。
调用栈是一种数据结构。如果我们运行到一个函数,它就会将其放置到栈顶。当从这个函数返回的时候,就会将这个函数从栈顶弹出,这就是调用栈做的事情。
作用域
作用域链是依靠执行上下文连接的,环境栈中的变量对象,从上到下就组成一条作用域链。他的用途就是保证对执行环境有权访问的所有变量和函数的有序访问。
词法作用域是指内部函数在定义的时候就决定了其外部作用域。
(function autorun(){ let x = 1; //这个函数中访问变量的时候,作用域是定义的时候决定的,而不是执行的时候。 function log(){ console.log(x); }; function run(fn){ let x = 100; fn(); } run(log);//1 })();
词法环境
包含环境记录器和外部环境的引用
- 环境记录器是存储变量和函数声明的实际位置。
- 外部环境的引用意味着它可以访问其父级词法环境(作用域)。
js的事件循环
导图要表达的内容用文字来表述的话:
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
- 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
- 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
- 上述过程会不断重复,也就是常说的Event Loop(事件循环)。
MacroTask(宏任务)
script
全部代码、setTimeout
、setInterval
、setImmediate
(浏览器暂时不支持,只有IE10支持,具体可见MDN
)、I/O
、UI Rendering
。
MicroTask(微任务)
Process.nextTick(Node独有)
、Promise
的then回调、Object.observe(废弃)
、MutationObserver
(具体使用方式查看这里)
setTimeout
这个函数,是经过指定时间后,把要执行的任务加入到Event Queue中。并不是到了指定的时间就执行,只有当主线程空闲出来后,才回去执行event queue中的等待事件。
setTimeout(fn,0)
的含义是,指定某个任务在主线程最早可得的空闲时间执行,意思就是不用再等多少秒了,只要主线程执行栈内的同步任务全部执行完成,栈为空就马上执行。
宏任务和微任务的执行顺序:
执行项目逻辑 ----> 将setTimeout,setInterval等定义的回调函数加入到宏任务队列,将promiese执行的回调加入到微任务队列 -------> 主逻辑执行完毕 -------> 执行微任务队列中的回调 --------> 执行宏任务队列中的回调
整段script代码是一个宏任务,在执行宏任务的过程中会不断的有微任务加入到微任务队列中,当执行完一个宏任务后先看微任务队列里有没有微任务,如果有先把整队的微任务执行完,然后在执行下一个宏任务,如此以往形成event loop。
process.nextTick()
虽然它是异步API的一部分。process.nextTick()
从技术上讲,它不是事件循环的一部分。
process.nextTick()
方法将callback
添加到next tick
队列。 一旦当前事件轮询队列的任务全部完成,在next tick
队列中的所有callbacks
会被依次调用。
- process.nextTick应该是独立于Event Loop 之外的,它是微任务,但是它本身应该有一个自己的队列,这个队列中的回调函数会优先于微任务队列中的函数执行。比如,你把process.nextTick放在Promise.then的下方,他还是会优先执行。
//注意promise改变状态之前,他是在主线程执行的,在then,和await中是在微任务中执行的。 console.log('1'); setTimeout(function () { console.log('2'); process.nextTick(function () { console.log('3'); }) new Promise(function (resolve) { console.log('4'); resolve(); }).then(function () { console.log('5') }) // process.nextTick(function () { // console.log('3'); // }) }) process.nextTick(function () { console.log('6'); }) new Promise(function (resolve) { console.log('7'); resolve(); }).then(function () { console.log('8') }) setTimeout(async function () { console.log('9'); // process.nextTick(function () { // console.log('10'); // }) // new Promise(function (resolve) { // console.log('11'); // resolve(); // }).then(function () { // console.log('12') // }) let result = await Promise.resolve("11") console.log(result) console.log("12") }) ; (async () => { console.log('13'); let result = await Promise.resolve("14") console.log(result) console.log("15") })() // 1 7 13 6 8 14 15 2 4 3 5 9 11 12。