2021-9-17
进程,线程
进程:进程是应用程序的执行实例,即进程是操作系统进行资源分配和独立运行的最小单元。
线程: 进程内部的一个执行单元,是被系统独立调度和分派的基本单位。
浏览器的多线程
GUI渲染线程
js引擎线程
定时器触发器线程
事件触发器线程
异步http请求线程
异步编程解决方案
回调函数
promise
async await。await后面的语句和下面的语句。
await后面的语句相当于同步函数,即调用resolve, reject。下面的语句才会进入微任务队列。
页面渲染的详细过程
查看页面的渲染性能
- 浏览器请求回来html文件。解析html元素,遇到外部资源,他会异步请求外部资源。
- 当请求完css资源,异步解析css,当html, css解析完毕后,构建成渲染树。
- 就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。 通常这一行为也被称为“自动重排”。
- 布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。
- 布局完成后,我们就发出Paint事件,将渲染树转换成屏幕上的像素。
详细说一下layer(层)就是图层
css图层
浏览器在渲染一个页面时,会将页面分为很多个图层,图层有大有小,每个图层上有一个或多个节点。 在渲染DOM的时候,浏览器所做的工作实际上是:
- 获取DOM后分割为多个图层
- 对每个图层的节点计算样式结果 (Recalculate style--样式重计算)
- 为每个节点生成图形和位置 (Layout--布局,重排,回流)
- 将每个节点绘制填充到图层位图中 (Paint--重绘)
- 图层作为纹理上传至GPU
- 组合多个图层到页面上生成最终屏幕图像 (Composite Layers--图层重组)
如何创建一个图层
- 拥有具有3D变换的CSS属性
- 使用加速视频解码的节点
- 节点
- CSS3动画的节点
- 拥有CSS加速属性的元素(will-change)
哪些API会引起回流和重绘
回流(重排)
就是改变元素的位置和大小
- 增加,删除,修改dom元素
- 获取元素的宽高
- 改变页面尺寸。
- 改变字体大小。
- dispaly: none;
重绘
改变元素外观
- 改变颜色
- visibility: hidden;
深拷贝
自己简单实现了一个
function deepClone (origin, target = {}) { for (let key in origin) { if (origin.hasOwnProperty(key)) { if (origin[key] !== null && typeof origin[key] === 'object') { if (Array.isArray(origin[key])) { // 处理数组 target[key] = [] } else { // 处理对象 target[key] = {} } deepClone(origin[key], target[key]) } else { // 处理基本数据类型包括函数 target[key] = origin[key] } } } return target }
是为了解决什么
两个变量指向同一个内存地址。改变一个变量的属性值,另一个变量也随之改变。
递归深拷贝的循环引用与爆栈怎么解决
这个不知道怎么解决,请大佬解释一下。
从你的角度看JS设计时为什么是单线程的
因为js是一个脚本语言,是为了完成用户交互的。如果不是单线程的。用户点击了一个按钮删除元素。然后点击另一个按钮需要删除的那个元素做一些事情。这样就会产生冲突。
js的单线程和它的用途有关,作为浏览器脚本语言,它主要是用来处理页面中用户的交互,以及操作DOM树、CSS样式树来给用户呈现一份动态而丰富的交互体验和服务器逻辑的交互处理。
如果JavaScript是多线程的方式来操作这些UI DOM,则可能出现UI操作的冲突;
如果Javascript是多线程的话,在多线程的交互下,处于UI中的DOM节点就可能成为一个临界资源,假设存在两个线程同时操作一个DOM,一个负责修改一个负责删除,那么这个时候就需要浏览器来裁决如何生效哪个线程的执行结果。
当然我们可以通过锁来解决上面的问题。但为了避免因为引入了锁而带来更大的复杂性,Javascript在最初就选择了单线程执行。
这也解释了为什么GUI线程和JS引擎是互斥的。
当JS引擎执行时GUI线程会被挂起,GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。
为了多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
web worker可以用在什么场景中
在worker线程中你可以运行任何你喜欢的代码,不过有一些例外情况。比如:在worker内,不能直接操作DOM节点,也不能使用
window
对象的默认方法和属性。Web Worker 是脱离在主线程之外的,将一些复杂的耗时的活交给它干完成后通过 postMessage 方法告诉主线程。Web worker是一个独立的运行环境,不能操作DOM和BOM。
window.postMessage
这个方法可以实现跨域:就是将该窗口的访问数据传递到另一个窗口。两个窗口中localstorage存储的数据不能相互访问。
单点登录sso
SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过
passport
,子系统本身将不参与登录操作
当一个系统成功登录以后,
passport
将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被passport
授权以后,会建立一个局部会话,在一定时间内可以无需再次向passport
发起认证当在一个项目登录后,它将通过window.postMessage({token}, origin)发送token到其他项目中,实现token的跨端获取。然后其他项目后端接收到token后,将去登录的项目上验证token的有效性。
// http://localhost:8080页面 // 验证postMessage localStorage.setItem("token", "=============") // 这里window.parent表示的是接收消息的窗口对象。这里就需要通过iframe元素了。iframe表示的是将该页面嵌入到其他页面中,**这里的代码也会在其他页面进行执行**,所以这里的window.parent在另一个页面中表示的就是另一个页面的全局对象。 window.parent.postMessage({ token: "==================" }, "http://localhost:8081") // http://localhost:8081页面 // 这里必须的创建一个iframe元素。因为localhost:8080页面需要获取到当前页面的window对象。 let iframe = document.createElement("iframe"); iframe.width = 0; iframe.height = 0; iframe.style.display = 'none' iframe.src = 'http://localhost:8080' document.body.appendChild(iframe) // 验证postMessage传参 window.addEventListener("message", (e) => { // console.log("iframe传递数据") // console.log("e", e) if (e.origin === 'http://localhost:8080') { localStorage.setItem("token", e.data.token) } })
2021-9-18
介绍原型和原型链到底是什么东西
原型: 一个对象,名为prototype为原型对象
原型的作用: 共享方法或属性
原型链:它的作用是提供对象的属性和方法的查找方式。对象自己的属性和方法 ===> 构造函数的原型 ===> Object的原型。一次查找需要的属性和方法,如果没有查找到将会报错。
原型链是实现继承的主要方法。 其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。每个构造函数都有一个prototype属性,指向原型对象。原型对象都包含一个指向构造函数的指针(constructor)。把构造函数的prototype属性修改成另一个构造函数的实例,此时原型对象就将包含指向另一个原型的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是原型链的概念。
通过 new 构造函数创建实例对象,在构造函数上声明一个方法,跟直接给实例对象声明一个方法有什么区别吗?
构造函数原型上声明一个方法:由构造函数创建的对象,他们共享该函数,执行同一个地址。而直接给实例对象声明一个方法,他们都是各自的方法,不能实现共享。这样比较占用内存。
什么时候在原型上声明方法,什么时候给实例对象声明方法?(使用的场景)?
共享: 原型上声明。
对象独有的:实例对象。
项目中有用到原型的一些思想吗?
好像没用过
发布订阅者模式
他会区分谁是发布者,谁是订阅者
// 发布订阅者模式 class EventEmitter { constructor() { this.events = [] } // 订阅事件 on (fnName, fn) { if (!this.events.hasOwnProperty(fnName)) { this.events[fnName] = [] } this.events[fnName].push(fn) } // 触发事件 emit (fnName, ...args) { if (this.events.hasOwnProperty(fnName)) { this.events[fnName].forEach(item => { item(...args) }) } } // 移除事件 off (fnName, callback) { if (this.events.hasOwnProperty(fnName)) { this.events[fnName] = this.events[fnName].filter(item => item !== callback) } } // 移除指定类型的所有事件 allOff (fnName) { if (this.events.hasOwnProperty(fnName)) { delete this.events[fnName] } } // 指触发一次 once (fnName, fn) { function fn1 () { // 当触发事件后调用一次就将其删除 fn() this.off(fnName, fn) } this.on(fnName, fn1) } }
简单的观察者模式
观察者和被观察者之间有联系,但是发布订阅者,发布者(emit)和订阅者(on)之间没有关系。
// 观察者模式: 观察者和被观察者之间存在关系。并且内部也实现了发布订阅者模式。 // 他与发布订阅者的区别在于:被观察者内部会自动告诉观察者状态改变,通知其作出相应变化。 class SubObject { //被观察者 constructor(name) { this.name = name; this.observerArr = [] // 保存观察者实例 this.state = "现在的状态" } attach (observe) { // 将观察者和被观察者结合。相当于订阅者 this.observerArr.push(observe) } setState (newState) { this.state = newState // 状态改变通知观察者。相当于发布者 this.observerArr.forEach(fn => { fn.update(this) }) } } class Observer { // 观察者 constructor(name) { this.name = name } update (s) { // 更新观察到的状态 console.log(this.name + ": 被观察者" + s.name + "现在的状态是: " + s.state) } } const o1 = new Observer("观察者一") const o2 = new Observer("观察者二") const s = new SubObject("被观察者") s.attach(o1) s.attach(o2) s.setState("被观察者状态改变。。。。")
观察者通过什么方式告诉 watcher 数据发生了更新?
如果被观察者内部状态发生改变,他会自动通知观察者状态改变的。
鼠标点击了 a 标签,然后又弹起这么一个过程,这个过程到底触发了哪些事件?
只触发了click事件。
预编译
- 找函数里面的变量声明和形参,此时赋值为undefined
- 形参和实参相统一,就是给形参赋值
- 找函数声明,如果有与函数同名的变量和函数,函数将覆盖变量
- 然后再按照上下顺序执行代码,遇到相同的变量名和函数名,就相互覆盖
- 然后再找赋值语句对相应变量赋值
多参数的函数柯里化
当传入的参数个数小于需要函数柯里化时的参数个数时,就递归调用curriedFn。最后还是返回柯里化函数调用。
柯里化是把一个多参数函数转化成一个嵌套的一元函数的过程
const curry = function(fn){ // 这个函数只是为了合并参数的。 return function curriedFn(...args){ if(args.length<fn.length){ // 这个函数是为了无限调用的。 return function(){ return curriedFn(...args.concat([...arguments])); } } // 真正调用返回结果。 return fn(...args); } } const fn = (x,y,z,a)=>x+y+z+a; const myfn = curry(fn); console.log(myfn(1)(2)(3)(1));
关于柯里化函数的意义如下:
- 让纯函数更纯,每次接受一个参数,松散解耦
- 惰性执行
组合函数compose
他的特点是从右到左执行
const compose = (...fns)=>val=>fns.reverse().reduce((acc,fn)=>fn(acc),val);
管道函数pipe
他的特点是从左往右执行
const pipe = (...fns)=>val=>fns.reduce((acc,fn)=>fn(acc),val);
组合函数与管道函数的意义在于:可以把很多小函数组合起来完成更复杂的逻辑
千分位逗号分割
通过toLocaleString()函数
str.replace(/(?=\B\d{3}+$)/g, ',')
自定义方法