Object.freeze() 冻结对象
可作用于长列表性能优化 禁止vue劫持数据
- 不能添加新属性
- 不能删除已有属性
- 不能修改已有属性的值
- 不能修改原型
- 不能修改已有属性的可枚举性、可配置性、可写性
new 的实现原理
- 创建一个新对象
- 链接到原型 将构造函数的 prototype 赋值给新对象的 _ proto_
- 绑定 this 构造函数中的this指向新对象并且调用构造函数
- 返回新对象
原型与原型链:
- 每个函数都有 prototype 属性,除了 Function.prototype.bind() ,该属性指向原型
- 每个对象都有 __ proto__ 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 prototype ,但是 prototype 是内部属性,我们并不能访问到,所以使⽤ __ proto__ 来访问
- 对象可以通过 __ proto__ 来寻找不属于该对象的属性, __ proto__ 将对象连接起来组 成了原型链
关系: instance.constructor.prototype = instance.__ proto__
作用域与作用域链:
- 作⽤域就是变量与函数的可访问范围,即作⽤域控制着变量与函数的可⻅性和⽣命周期,也可理解为该上下⽂中声明的变量和声明的作⽤范围,可分为块级作⽤域和函数作⽤域
- 作⽤域链可以理解成包含⾃身变量对象和上级变量对象的列表,通 过 Scope 属性查找上级变量
- 作⽤域链的作⽤是保证执⾏环境⾥有权访问的变量和函数是有序的,作⽤域链的变量只能向上访问,变量访问到 window 对象即被终⽌,作⽤域链向下访问变量是不被允许的
vue中diff算法实现流程
- 在内存中构建虚拟dom树
- 将内存中虚拟dom树渲染成真实dom结构
- 数据改变的时候,将之前的虚拟dom树结合新的数据生成新的虚拟dom树
- 将此次生成好的虚拟dom树和上一次的虚拟dom树进行一次比对(diff算法进行比对),来更新只需要被替换的DOM,而不是全部重绘。在Diff算法中,只平层的比较前后两棵DOM树的节点,没有进行深度的遍历。
- 会将对比出来的差异进行重新渲染
react中diff算法实现流程
- DOM结构发生改变-----直接卸载并重新create
- DOM结构一样-----不会卸载,但是会update变化的内容
- 所有同一层级的子节点.他们都可以通过key来区分-----同时遵循1.2两点 (其实这个key的存在与否只会影响diff算法的复杂度,换言之,你不加key的情况下,diff算法就会以暴力的方式去根据一二的策略更新,但是你加了key,diff算法会引入一些另外的操作)
React会逐个对节点进行更新,转换到目标节点。而最后插入新的节点,涉及到的DOM操作非常多。diff总共就是移动、删除、增加三个操作,而如果给每个节点唯一的标识(key),那么React优先采用移动的方式,能够找到正确的位置去插入新的节点。
vue会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。而对于React而言,每当应用的状态被改变时,全部组件都会重新渲染,所以react中会需要shouldComponentUpdate这个生命周期函数方法来进行控制。
diff 策略
- Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计
- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
- 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分
tree diff
- React 对树进行分层比较,两棵树只会对同一层次的节点进行比较
- React 通过 updateDepth 对 Virtual DOM 树进行层级控制 , 只会对同一父节点下所有子节点进行比较。 当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较
component diff
- 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree
- 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点
- 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff
element diff
当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)
- INSERT_MARKUP,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作
- MOVE_EXISTING,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点
- REMOVE_NODE,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作
mounted拿到数据可以后可以直接获取dom吗
异步获取dom
nextTick的实现原理
vue 中 由原生异步api封装而成的异步方法
它对于浏览器异步API的选用规则如下 Promise.than --> MutationObserver --> setImmediate --> setTimeout
同步代码执行完毕之后,优先执行微任务,其次才会执行宏任务
setTimeOut执行需要满足两个条件
- 主进程必须是空闲的状态,如果到时间了,主进程不空闲也不会执行你的回掉函数
- 这个回掉函数需要等到插入异步队列时前面的异步函数都执行完了,才会执行
JS 异步解决方案的发展历程以及优缺点
-
回调函数(callback)
优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队 等着,会拖延整个程序的执行。)
缺点:回调地狱,不能用 try catch 捕获错误,不能 return
-
Promise 优点:解决了回调地狱的问题 缺点:无法取消 Promise ,错误需要通过回调函数来捕获
-
Async/await
优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使 用 await 会导致性能上的降低。
双向数据绑定
vue2 使用 Object.defineProperty 对象以及对象属性的劫持+发布订阅模式
Object.defineproperty 能监听到对象的新增删除属性吗?
不能,需要开发者主动调用相应的方法去更新 :Vue.set(), Vue.delete(),由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持
Object.defineproperty 能监听到数组的添加删除操作吗?
vue 2.0使用数组重写的方法实现了数组的响应,7个方法分别为 push pop shift unshift splice sort reverse
改变数组索引的值能不能被监听?
Object.defineProperty 能监听到 但由于对数组更新可能触发到枚举/遍历 性能损耗过大 vue无法检测数组的变动。 Object.defineProperty()是深度监听,需要递归到底。一次性计算量很大
vue3 使用 proxy 实现数据响应式
Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。因为 proxy 是对整个对象进行代理,所以可以监听对象某个属性值的变化,还可以监听对象属性的新增和删除,而且还可以监听数组
vue挂载和卸载父子组件生命周期钩子执行顺序
加载渲染过程:
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate
-> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
子组件更新过程:
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
父组件更新过程:
父 beforeUpdate -> 父 updated
销毁过程:
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
jsx
setState 是同步还是异步的
只要你进入了 react
的调度流程,那就是异步的。只要你没有进入 react
的调度流程,那就是同步的。什么东西不会进入 react
的调度流程? setTimeout
setInterval
,直接在 DOM
上绑定原生事件等。这些都不会走 React
的调度流程,你在这种情况下调用 setState
,那这次 setState
就是同步的。 否则就是异步的
React 的事件系统组成
- 事件注册
- 事件监听
- 事件合成
- 事件派发
React 的事件系统流程
- 在
React
代码执行时,内部会自动执行事件的注册 - 第一次渲染,创建
fiberRoot
时,会进行事件的监听,所有的事件通过addEventListener
委托在id=root
的DOM元素上进行监听 - 在我们触发事件时,会进行事件合成,同类型事件复用一个合成事件类实例对象
- 最后进行事件的派发,执行我们代码中的事件回调函数
React 的事件合成
React
合成事件是将同类型的事件找出来,基于这个类型的事件,React
通过代码定义好的类型事件的接口和原生事件创建相应的合成事件实例,并重写了preventDefault
和stopPropagation
方法。
这样,同类型的事件会复用同一个合成事件实例对象,节省了单独为每一个事件创建事件实例对象的开销,这就是事件的合成
React 捕获和冒泡
事件派发分为两个阶段执行, 捕获阶段和冒泡阶段。
React
会根据事件触发的fiber
节点向上查找,将上面的同类型事件添加到队列中,这样天然就有了一个冒泡的顺序,从最底层向上冒泡。如果倒序过来遍历就是捕获的顺序。
所以,React
实现冒泡和捕获就很简单了,只需要根据事件派发的阶段,判断是冒泡阶段还是捕获阶段来决定是正序遍历listeners
还是倒序遍历就行了。
useEffect 和 useLayoutEffect 的区别
useEffect 在渲染时是异步执行,并且要等到浏览器将所有变化渲染到屏幕后才会被执行。
useLayoutEffect 在渲染时是同步执行,其执行时机与 componentDidMount,componentDidUpdate 一致
对于 useEffect 和 useLayoutEffect 哪一个与 componentDidMount,componentDidUpdate 的是等价的
useLayoutEffect,因为从源码中调用的位置来看,useLayoutEffect的 create 函数的调用位置、时机都和 componentDidMount,componentDidUpdate 一致,且都是被 React 同步调用,都会阻塞浏览器渲染。
useEffect 和 useLayoutEffect 哪一个与 componentWillUnmount 的是等价的
useLayoutEffect 的 detroy 函数的调用位置、时机与 componentWillUnmount 一致,且都是同步调用。useEffect 的 detroy 函数从调用时机上来看,更像是 componentDidUnmount (注意React 中并没有这个生命周期函数)
为什么建议将修改 DOM 的操作里放到 useLayoutEffect 里,而不是 useEffect
当DOM 已经被修改,但浏览器渲染线程依旧处于被阻塞阶段,所以还没有发生回流、重绘过程。由于内存中的 DOM 已经被修改,通过 useLayoutEffect 可以拿到最新的 DOM 节点,并且在此时对 DOM 进行样式上的修改,这些修改会和 react 做出的更改一起被一次性渲染到屏幕上,依旧只有一次回流、重绘的代价。
如果放在 useEffect 里,useEffect 的函数会在组件渲染到屏幕之后执行,此时对 DOM 进行修改,会触发浏览器再次进行回流、重绘,增加了性能上的损耗
useMemo 与 useCallback 的区别
useCallback返回一个函数,当把它返回的这个函数作为子组件使用时,可以避免每次父组件更新时都重新渲染这个子组件
useMemo返回的的是一个值,用于避免在每次渲染时都进行高开销的计算
- useEffect:会在组件渲染完毕后执行,和渲染无关的副作用。
- useMemo:会在在组件首次加载和重渲染期间执行,执行的函数需要和渲染相关的。
- useCallback:会在渲染期间执行,返回一个函数。
react hooks的优缺点
- 更容易复用代码 更容易拆分组件
- 清爽的代码风格 代码量更少
- 更容易发现无用的状态和函数
- 状态不同步 每个函数都有一份父级作用域
- 副作用代码由主动式变成响应式
PureComponent和Component的区别
PureComponent会自动执行shouldComponentUpdate函数,通过浅层的比较,实现react的性能优化。而Component必须要通过自己去调用生命周期函数shouldComponentUpdate来实现react组件的优化
类组件和纯函数组件的区别
函数式组件是一个纯函数,它是需要接受props参数并且返回一个React元素就可以了。类组件是需要继承React.Component的,而且class组件需要创建render并且返回React元素,语法上来讲更复杂
简述webpack打包过程
初始化参数: 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
开始编译: 根据我们的webpack配置注册好对应的插件调用 compile.run 进入编译阶段,在编译的第一阶段是 compilation,他会注册好不同类型的module对应的 factory
编译模块: 进入 make 阶段,会从 entry 开始进行两步操作:
第一步是调用 loaders 对模块的原始代码进行编译,转换成标准的JS代码, 第二步是调用 acorn 对JS代码进行语法分析,然后收集其中的依赖关系。每个模块都会记录自己的依赖关系,从而形成一颗关系树。 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表
输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
从输入url到页面展示的过程
- 浏览器根据请求的URL交给DNS域名解析,找到真实IP,向服务器发起请求;
- 服务器交给后台处理完成后返回数据,浏览器接收文件(html,js,css,图像等);
- 浏览器对加载到的资源(html,js,css等)进行语法解析,建立相应的内部数据结构(如HTML的DOM);
- 载入解析到的资源文件,渲染页面,完成。
简述javascript原型、原型链?有什么特点
原型:每一个构造函数都有一个prototype属性指向一个对象,这个对象就是构造函数实例的原型
原型链:每一个实例都有一个__proto__属性执行原型对象,来获取原型对象上的属性和方法,原型对象也有一个__proto__属性指向另外一个原型对象,以此类推,直到原型链的最终端null为止,这个串成链的过程就是原型链
特点:实现继承 一个对象可以拿到另一个对象上的属性和方法
构造函数都有一个prototype属性指向原型对象
原型对象都有一个consttuctor属性指向构造函数
构造函数new实例化实例对象
实例对象上有__proto属性指向原型
json
谈谈this对象的理解,call()和apply()的区别
call和apply的区别在于传入参数的不同; 第一个参数都是,指定函数体内this的指向;
第二个参数开始不同,apply是传入带下标的集合,数组或者类数组,apply把它传给函数作为参数,call从第二个开始传入的参数是不固定的,都会传给函数作为参数。
简述js继承的方式
- 混入式继承:把父类的所有方法都拷贝到子类上
- 原型式继承:只继承父类原型上的属性和方法
- 原型链继承:继承父类构造函数里边的属性和方法,也继承父类原型上的属性和方法 缺点--不能向父类传参数
- 借用构造函数继承:可以父类传递参数 缺点--继承不了父类原型对象的方法
- 组合继承:借用构造函数继承+原型链继承
深拷贝和浅拷贝的区别
浅拷贝(shallowCopy)
只是增加了一个指针指向已存在的内存地址,
深拷贝(deepCopy)
是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存.
浅复制
:仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
深复制
:在计算机中开辟一块新的内存地址用于存放复制的对象。
XMLhttprequest对象
Ajax的核心是JavaScript对象XmlHttpRequest。该对象在Internet Explorer 5中首次引入,它是一种支持异步请求的技术。简而言之,XmlHttpRequest使您可以使用JavaScript向服务器提出请求并处理响应,而不阻塞用户。通过XMLHttpRequest对象,Web开发人员可以在页面加载以后进行页面的局部更新
简述Chome盒模型与IE盒模型的区别
每个元素在页面中占位大小 = content + padding + margin + border
Chrome盒模型内容大小等于content大小;
IE盒模型内容大小等于content + padding + border的总和
简述优雅降级与渐进增强
1、渐进增强(progressive enhancement)
针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进和追加功能达到更好的用户体验。
2、优雅降级(graceful degradation)
一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。
3、区别:
a. 优雅降级是从复杂的现状开始,并试图减少用户体验的供给;渐进增强则是从一个非常基础的,能够起作用的版本开始,并不断扩充,以适应未来环境的需要。
b. 渐进增强观点认为应该关注于内容本身,这使得渐进增强成为一种更为合理的设计范例;优雅降级观点认为应该针对那些最高级、最完善的浏览器来设计网站。
清除浮动的方法
- 使用空标签清除浮动
- 使用after伪对象清除浮动
- 溢出隐藏
- 浮动外部元素
如何让一个盒子在页面垂直水平居中
方法一:已知宽高
div{
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 0;
margin: auto;
}
css
方法二:未知宽高
div{
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%,-50%);
}
css
前端性能优化方案
1、减少DOM操作 2、部署前,图片压缩,代码压缩 3、优化js代码结构,减少冗余代码 4、减少http请求,合理设置HTTP缓存 5、使用内容分发cdn加速 6、静态资源缓存 7、图片延迟加载
css选择器优先级顺序
- ID 选择器, 如 #id
- 类选择器, 如 .class
- 属性选择器, 如 a[href="segmentfault.com"]
- 伪类选择器, 如 :hover
- 伪元素选择器, 如 ::before
- 标签选择器, 如 span
- 通配选择器, 如 *
浏览器是如何渲染UI的
- 浏览器获取HTML文件,然后对文件进行解析,形成DOM Tree
- 与此同时,进行CSS解析,生成Style Rules
- 接着将DOM Tree与Style Rules合成为 Render Tree
- 接着进入布局(Layout)阶段,也就是为每个节点分配一个应出现在屏幕上的确切坐标
- 随后调用GPU进行绘制(Paint),遍历Render Tree的节点,并将元素呈现出来
less和sass的区别
- Less是基于JavaScript,是在客户端处理的;Sass是基于Ruby的,是在服务器端处理的。
- 关于变量在Less和Sass中的唯一区别就是Less用@,Sass用$。
- 输出设置,Less没有输出设置,Sass提供4中输出选项:nested, compact, compressed 和 expanded。
- Sass支持条件语句,可以使用ifelse,for循环等等,而Less不支持
Vuex
1、state
Vuex 使用单一状态树,即每个应用将仅仅包含一个store 实例,但单一状态树和模块化并不冲突。存放的数据状态,不可以直接修改里面的数据。
2、getters
类似vue的计算属性,主要用来过滤一些数据。
3、mutations
mutations定义的方法动态修改Vuex 的 store 中的状态或数据。
4、actions
actions可以理解为通过将mutations里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操作数据。view 层通过 store.dispath 来分发 action。
5、modules
项目特别复杂的时候,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理。
keep-alive
keep-alive
包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染
Vue 组件 data 为什么必须是函数
data是一个函数的话,这样每复用一次组件,就会返回一份新的data
,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份data
,就会造成一个变了全都会变的结果
nextTick 是做什么的
$nextTick
是在下次 DOM
更新循环结束之后执行延迟回调,在修改数据之后使用 $nextTick
,则可以在回调中获取更新后的DOM
$route 和 $router 的区别
$router
为 VueRouter
实例,想要导航到不同 URL
,则使用 $router.push
方法
$route
为当前 router
跳转对象里面可以获取 name
、 path
、 query
、 params
等
MVC
mvc是模型(model)-视图(view)-控制器(controller)`的缩写,一种软件设计典范使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。`MVC对应Html,CSS,js。
json
MVVM
MVVM是Model-View-ViewModel
缩写,也就是把MVC
中的Controller
演变成ViewModel
。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。
单向数据流和双向数据流
单向数据流(Unidirectional data flow)方式使用一个上传数据流和一个下传数据流进行单向数据通信,两个数据流之间相互独立。单向数据流指只能从一个方向来修改状态。vuex(组件data -> action -> state->组件data)
与单向数据流对对应的是双向数据流(也叫双向绑定)。在双向数据流中,Model(可以理解为状态的集合) 中可以修改自己或其他Model的状态, 用户的操作(如在输入框中输入内容)也可以修改状态。这使改变一个状态有可能会触发一连串的状态的变化,最后很难预测最终的状态是什么样的。使得代码变得很难调试。
vite和webpack的区别:
vite是按需加载,他的优势在开发环境,启动是不打包,即不需要分析模块依赖,也不需要编译,启动速度就快,动态编译模块缩短了编译的时间
webpack是全部加载,在启动开发服务器时会先打包再启动开发服务器
接口和类型别名type有什么区别
接口可以继承,还可以重复申明,当有多个命名一样的接口是他们被定义的类型会发生合并,不支持联合/交叉类型
类型别名不可以继承,也不可以重复定义,支持使用联合类型和交叉类型
React 组件的生命周期方法
componentWillMount() – 在渲染之前执行,在客户端和服务器端都会执行。
componentDidMount() – 仅在第一次渲染后在客户端执行。
componentWillReceiveProps() – 当从父类接收到 props 并且在调用另一个渲染器之前调用。
shouldComponentUpdate() – 根据特定条件返回 true 或 false。如果你希望更新组件,请返回true 否则返回 false。默认情况下,它返回 false。
componentWillUpdate() – 在 DOM 中进行渲染之前调用。
componentDidUpdate() – 在渲染发生后立即调用。
componentWillUnmount() – 从 DOM 卸载组件后调用。用于清理内存空间。
什么是高阶组件(HOC)?它可以做什么?
高阶组件是重用组件逻辑的高级方法,是一种源于 React 的组件模式。 HOC 是自定义组件,在它之内包含另一个组件。它们可以接受子组件提供的任何动态,但不会修改或复制其输入组件中的任何行为。你可以认为 HOC 是“纯(Pure)”组件。
HOC可用于许多任务,例如:
- 代码重用,逻辑和引导抽象
- 渲染劫持
- 状态抽象和控制
- Props 控制