Vue 模板编译原理
Vue 中的模板 tempalte 无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML 语法,所以需要将 template 转化成一个 JS 函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML 元素,就可以让试图跑起来,这是一个转化的过程,就成为模板编译。模板编译又分为三个阶段:解析 parse、优化 optimize,生成 genertate,最终生成可执行函数 render
解析阶段:使用大量的正则表达式对 template 字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST
优化阶段:遍历 AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff 比较时,直接跳过这一些静态节点,优化 runtime 性能
生成阶段:将最终的 AST 转化成 render 函数字符串
为什么 useState 返回的是数组而不是对象
如何从 html 元素继承 box-sizing
在大多数情况下我们在设置元素的 border 和 padding 并不希望改变元素的 width、height 值,这个时候我们就可以为该元素设置 box-sizing:border-box;
如果不希望每次都重写一遍,而是希望它是继承过来的,那么我们可以使用如下代码:
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
这样的好处在于它不会覆盖其他组件的 box-sizing 值,又无需为每一个元素重复设置 box-sizing:border-box;
react 的虚拟 dom 是怎么实现的
React 是把真实的 DOM 树转换为 JS 对象树,也就是 Virtual DOM。每次数据更新后,重新计算 VM,并和上一次生成的 VM 树进行对比,对发生变化的部分进行批量更新。除了性能之外,VM 的实现最大的好处在于和其他平台的集成。比如我们一个真实的 DOM 是这样的
<button class="myBtn">
<span>this is a page</span>
</button>
那转化为 VM 是这样的
{
type:'button',
props:{
className:'myBtn',
childer:[{
type:'span',
props:{
type:'text'
children:'this is a page'
}
}]
}
}
了解过 style 上加 scoped 属性的原理吗
什么是 scoped?
在 Vue 组件中,为了使样式私有化(模块化),不对全局造成污染,可以在 style 标签上添加 scoped 属性以表示它的只属于当下的模块,局部有效。如果一个项目中的所有 vue 组件 style 标签全部加上了 scoped,相当于实现了样式的私有化。如果引用了第三方组件,需要在当前组件中局部修改第三方组件的样式,而又不想去除 scoped 属性造成组件之间的样式污染。此时只能通过穿透 scoped 的方式来解决
scoped 的实现原理:
Vue 中的 scoped 属性的效果主要通过 PostCSS 转译实现
即:PostCSS 给所有 DOM 添加了一个唯一不重复的动态属性,然后给 CSS 选择器额外添加一个对应的属性选择器来选择该组件中 DOM,这种做法使得样式私有化
Vue3.0 中 Tree Shaking 特性是什么,并举例进行说明
Tree Shaking 是什么?
Tree Shaking 是一种通过清除多余代码方式来优化项目打包,专业属于叫 Dead code elimination,简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码;如果把代码打包比作制作蛋糕,传统方式是把鸡蛋带壳全部丢进去搅拌,然后放入烤箱,最后把没用的带壳全部挑选并剔除出去,而 tree shaking 则是一开始就把有用的蛋白蛋黄放入搅拌,最后直接制作出蛋糕,也就是说,tree shaking 其实是找出使用的代码
在 vue2 中,无论使用什么功能,最终都会出现在生产代码中。主要原因是 vue 实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到
import Vue from "vue";
Vue.nextTick(() => {});
而 Vue3 源码引入 tree shaking 特性,将全局 API 进行分块,如果你不是用其某些功能,它们将不会包含在您的基础包中
import { nextTick, observable } from "vue";
nextTick(() => {});
Tree Shaking 如何做?
Tree Shaking 是基于 ES6 模板语法(import 和 exports),主要是借助 ES6 模板的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量。
Tree Shaking 无非是做了两件事情:
- 编译阶段利用 ES6 Module 判断哪些模块已经加载
- 判断哪些模块和变量未被使用或引用,进而删除对应代码
Tree Shaking的作用
通过Tree Shaking,Vue3给我们带来的好处是:
- 减少程序体积
- 减少程序执行时间
- 便于将来对程序架构进行优化
HTTP 协议的优点和缺点
Broadcast Channel
顾名思义:广播频道,官方文档里的解释为用于同源不同页面之间完成通信的功能,在其中某个页面发送的消息会被其他页面监听到
需要注意的是:同源,该方法无法完成跨域的数据传输
localStorage
localStorage 是浏览器多个标签共用的存储空间,所以可以用来实现多标签之间的通信(session 是会话级的存储空间,每个标签页都是单独的)
SharedWorker
SharedWorker 可以被多个 window 共同使用,但必须保证这些标签页都是同源的(相同的协议、主机和端口号)
WebSocket 通讯
全双工(full-duplex)通信自然可以实现多个标签之间的通信
定时器 setInterval+cookie
在页面 A 设置一个使用 setInterval 定时器不断刷新,检查 cookies 的值是否发生变化,如果变化就进行刷新的操作
由于 cookies 是在同域可读的,所以在页面 B 审核的时候改变 Cookies 的值,页面 A 自然是可以拿到的
这样做确实可以实现想要的功能,但是这样方式相当浪费资源。虽然在这个性能过盛的时代,浪费不浪费也感觉不出来,但是这种实现方案,确实不够优雅
postMessage
两个需要交互的 tab 页面具有依赖关系。如 A 页面中通过 JavaScript 的 window.open 打开 B 页面,或者 B 页面通过 iframe 嵌入到 A 页面,这种情形最简单,可以通过 HTML5 的 window.postMessage API 完成通信,用于 postMessage 函数是绑定在 window 全局对象下,因此通信的页面中必须有一个页面(如 A 页面)可以获取另一个页面(如 B 页面)的 window 对象,这样才可以完成单向通信;B 页面无需获取 A 页面的 window 对象,如果需要 B 页面对 A 页面的通信,只需要在 B 页面侦听 message 事件,获取事件中传递的 source 对象,该对象即为 A 页面 window 对象的引用:
// B页面
window.addEventLister("message", (e) => {
let { data, source, origin } = e;
source.postMessage("message echo", "/");
});
postMessage 的第一个参数为消息实体,它是一个结构化对象,即可以通过JSON.stringify 和 JSON.parse函数还原的对象;第二个参数为消息发送范围选择器,设置为"/"意味着只发送消息给同源的页面,设置"*"则发送全部页面
javascript 中什么是伪数组?如何转换成真数组
伪数组有以下几个特点:
- 伪数组是一个对象
- 这个对象必须要有 length 属性
- 它终究是个假数组,它没有数组的 splice,concat,pop 等方法
- 如果这个对象的 length 不为 0,那么必须要有按照下标存储的数据
伪数组没有数组 Array.prototype 的属性值,类型是 Object ,而数组类型是 Array
数组是基于索引的实现, length 会自动更新,而对象是键值对
使用对象可以创建伪数组,伪数组可以利用call或者apply很方便的转化为真数组
伪数组转化为真数组
方法 1:
let arr = [];
arr.slice.call(pagis);
方法 2:
let lis = document.querySelectorAll("li");
let arr = Array.prototype.slice.call(lis);
方法 3:
// 遍历
let lis = document.querySelectorAll("li");
let arr = [];
for (let i = 0; i < lis.length; i++) {
arr.push(lis[i]);
}
方法 4:
// call借调
let func = Function.prototype.call.bind(Array.prototype.slice);
let lis = document.querySelectorAll("li");
func(lis);
方法 5:
// 解构赋值
// arguments是伪数组
console.log(arguments);
let newArr = [...arguments];
console.log(newArr);
方法 6:
let lis = document.querySelectorAll("li");
let newArr = Array.from(lis);
请谈一下内存泄漏是什么,以及常见内容泄漏的原因和排查的方法
内存泄漏是指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况
如果内存泄漏的位置比较关键,那么随着处理的进行可能持有越来越多的无用内存,这些无用内存变多会引起服务器响应速度变慢
严重的情况下导致内存达到某个极限(可能是进程的上限,如 V8 的上限;也可能是系统可提供的内存上限)会使得应用程序奔溃。
常见内存泄漏的原因以及情况
全局变量
a = 10;
// 未声明对象
global.b = 11;
// 全局变量引用
// 全局变量直接挂在root对象上,不会被清除
闭包
function out() {
const bigData = new BBuffer(100);
inner = function () {
// ....
};
}
// 闭包会引用到父级函数中的变量,如果闭包未释放,就会导致内存泄漏,上面的例子inner直接挂在root,那么每次执行out函数所产生的bigData都不会释放,从而导致内存泄露
// 需要注意的是,这里举得例子只是简单的将引用挂在全局对象上,实际的业务情况可能是挂在某个可以从root追溯到的对象上导致的
事件监听
nodeJS 的事件监听也可能出现的内存泄漏。例如对同一个事件重复监听,忘记移除将造成内存泄漏。这种情况很容易在复用对象上添加事件时出现,所以事件重复监听可能收到如下警告:
emitter.setMaxListeners() to increase limit
例如,NodeJS 中的 Agent 的 keepAlive 为 true 时,可能造成的内存泄漏。当 Agent keepAlive 为 true 的时候,将会复用之前使用过的 socket,如果在 socket 上添加事件监听,忘记清除的话,因为 socket 的复用,将导致事件重复监听从而产生内存泄漏
原理上与前一个添加事件监听的时候忘记清除是一样的。在使用 nodeJS 的 http 模块时,不通过 KeepAlive 复用是没有问题的,复用了以后就会可能产生内存泄漏。所以,你需要了解添加事件监听的对象的生命周期,并注意自行移除
排查方法
想要定位内存泄漏,通常会有两种情况:
对于只要正常使用就可以重现的内存泄漏,这是很简单的情况只要是在测试环境模拟就可以排查了
对于偶然的内存泄漏,一般会与特殊的输入有关系。想稳定重现这种输入是很耗时的过程。如果不能通过代码的日志定位到这个特殊的输入,那么推荐去生产环境打印内存快照了
需要注意的是:打印内存快照是很耗 CPU 的操作,可能会对线上业务造成影响。快照工具推荐使用 heapdump 用来保存内存快照,使用 devtool 来查看内存快照
使用 heapdump 保存内存快照时,只会有 nodeJS 环境中的对象,不会收到干扰(如果使用 node-inspector 的话,快照中会有前端的变量干扰)
PS:安装 heapdump 在某些 NodeJS 版本上可能出错,建议使用 npm install heapdump -target=Node.js 版本来安装
说一下关于 tree-shaking 的原理
当前端项目到达一定的规模后,我们一般会采用按模块方式组织代码,这样可以方便代码的组织及维护。但会存在一个问题,比如我们有一个utils工具类,在另一个模块中导入它。这会在打包的时候将utils中不必要的代码也打包,从而使得打包体积变大,这时候就需要用到Tree shaking技术了
tree-shaking 是一种通过清除多余代码方式来优化项目打包体积的技术
原理
- 利用ES6模块的特点
- 只能作为模块顶层的语句出现
- import的模块名只能是字符串常量,不能动态引入模块
- import 引入的模块不能再进行修改的 虽然tree-shaking的概念在1990年就提出来了,但是直到ES6的ES6-style模块出现后才真正被利用起来。这是因为tree-shaking只能在静态模块下工作。ES6模块加载是静态的,因此在ES6种使用tree-shaking是非常容易地。而且,tree-shaking不仅支持import/export级别,而且也支持声明级别
在ES6以前,我们可以使用CommonJS引入模块:require(),这种引入是动态地,也意味着我们可以基于条件来导入需要的代码:
let mainModule;
//动态导入
if(condition){
mainModule=require('dog')
}else{
mainModule=require('cat')
}
CommonJS的动态特性意味着tree-shaking不适用。因为它是不可能确定哪些模块实际运行之前是需要的或者是不需要的。在ES6中,进入了完全静态的导入语法:import。
//不可行
if(condition){
mainModule=require('dog')
}else{
mainModule=require('cat')
}
只能通过导入所有的包后再进行条件获取
import dog from 'dog';
import cat from 'cat';
if(condition){
//dog.xxx
}else{
//cat.xxx
}
ES6的import语法可以使用tree-shaking,因为可以在代码不运行的情况下就能分析出不需要的代码。
如何使用?
从webpack2开始支持实现了tree-shaking特性,webpack2正式版本内置支持ES6的模块(也叫harmony模块)和未引用模块检测能力。webpack4正式版本扩展了这个检测能力,通过package.json的sideEffects属性作为标记,向complier提供提示,表明项目中哪些文件是ES6模块,由此可以安全地删除文件中未使用地部分 如果使用的是webpack4,只需要将mode设置为production,就可以开启tree-shaking
entry:'./src/index.js',
mode:'production',
output:{
path:path.resolve(__dirname,'dist'),
filename:'bundle.js'
},
如果使用webpack2,可能你会发现tree-shaking不起作用。因为babel会将代码编译成CommonJS模块,而tree-shaking不支持CommonJS,所以需要配置不转义
options:{presets:[['es2015',{modules:false}]]}
关于副作用
副作用是指那些当import的时候会执行一些动作,但是不一定会有任何export。比如ployfill,ployfills不对外暴露方法给主程序使用
tree-shaking不能自动识别哪些代码属于副作用,因此手动指定这些代码显得非常重要,如果不指定可能会出现一些意想不到的问题
在webpack中,是通过package.json的sideEffects属性来实现的
"name":"tree-shaking",
"sideEffects":false
如果所有的代码都不包含副作用,我们就可以简单地将该属性标记为false来告知webpack,它可以安全地删除未用到的export导出。
如果你的代码确实有一些副作用,那么可以改为提供一个数组:
"name":"tree-shaking",
"sideEffects":[
"./src/public/polyfill.js"
]
总结
- tree-shaking不会支持动态导入(如CommonJS的require()语法),只纯静态的导入(ES6的import/export)
- webpack中可以在项目package.json文件中,添加一个"sideEffects"属性,手动指定副作用的脚本
class 的继承和 prototype 继承 是完全一样的么
ES5 原型链继承与 ES6 class 继承
ES5 prototype 继承
通过原型链实现的继承。子类的 prototype 为父类对象的一个实例。因此子类的原型对象包含指向父类的原型对象的指针,父类的实例属性为子类原型的属性
ES5 的继承,实质是先创造子类的实例对象this上,然后再将父类的方法添加到这个this上----Father.apply(this) hua
ES6 class 继承
子类没有自己的this对象,因此必须再 constructor 中通过 super 继承父类的 this 对象,而后对此this对象进行添加,super关键字在构造函数中表示父类的构造函数,用来新建父类的 this 对象
ES6 的继承机制完全不同,实质是先创造父类的实例对象this---需要提前调用super方法,然后再用子类的构造函数修改this指针
super 可以作为函数和对象使用的。当作为函数使用的时候,只能在子类的构造函数中使用----表示父类的构造函数,但是 super 中的 this 指向的是子类的实例,因此在子类中super()表示的是 Father.prototype.constructor.call(this)。当作为对象使用的时候,super表示父类的原型对象,即表示 Father.prototype
区别和不同
- 类内部定义的方法都是不可枚举的,这个 ES5 不一样
- 类不存在变量提升,这一点与 ES5 完全不同
- 类相当于实例的原型,所有在类中定义的方法都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就成为静态方法
- ES5的继承,实质上是先创造子类的实例对象this,然后再将父类的方法添加到this上面---Father.apply(this)。ES6的继承机制完全不同,实质上是先创造父类的实例对象this--- 前提是先调用super方法,然后再用子类的构造函数修改this。
根据下面 ES6 构造函数的书写方式,要求写出 ES5的
es6 中的class 必须要通过new 来调用,不能当做普通函数调用,否则报错,所以这个校验是不是要加
其次 class 是在严格模式下 这个是不是也要加
还有一点,ES6 中的原型方法是不可被枚举的
以及原型上的方法不允许通过 new 来调用
// ES6
class Example {
constructor(name) {
this.name = name;
}
init() {
const fun = () => { console.log(this.name) }
fun();
}
}
const e = new Example('Hello');
e.init();
//ES5
function Example(name) {
'use strict';
if (!new.target) {
throw new TypeError('Class constructor cannot be invoked without new');
}
this.name = name;
}
Object.defineProperty(Example.prototype, 'init', {
enumerable: false,
value: function () {
'use strict';
if (new.target) {
throw new TypeError('init is not a constructor');
}
var fun = function () {
console.log(this.name);
}
fun.call(this);
}
})
此题考察是否清楚 ES6 的 class 和普通构造函数的区别,记住它们有以下区别,就不会有遗漏:
1.ES6中的class必须通过new来调用,不能当做普通函数调用,否则报错,因此,new.target来判断调用方式
2.ES6的class中的所有代码均处于严格模式之下,因此,无论是构造函数本身,还是原型方法,都使用了严格模式
3.ES6中的原型方法是不可被枚举的,因此,定义原型方法使用了属性描述符,让其不可枚举
4.原型上的方法不允许通过new来调用,因此,原型方法中加入了 new.target来判断调用方式
运行结果
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve();
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
//运行结果:1 2 4 3
setTimeout(() => {
console.log(1);
});
const promise = new Promise((resolve, reject) => {
console.log(2);
resolve();
});
promise.then(() => {
console.log(3);
});
console.log(4);
//运行结果:2 4 3 1
async function m1() {
return 1;
}
async function m2() {
const n = await m1();
console.log(n);
return 2;
}
async function m3() {
const n = m2();
console.log(n);
return 3;
}
m3().then((n) => {
console.log(n);
});
m3();
console.log(4);
// 运行结果: Promise { <pending> } Promise { <pending> } 4 1 3 1
// 同步代码 先执行m3 ,进入m3 =》 执行m2 => 进入m2 => await m1 () => 进入m1 return1 =》 退出m1
=>回到 m2中会把包装成promise , 那么 await m1 下面的代码加入微队列 =》 退出 m2
=> 执行 m3中的 console 打印pending promise => 退出 m3 =》 执行then => 将then中的回调加入到微队列 =》 执行 下一个m3
var a;
var b = new Promise((resolve, reject) => {
console.log('promise1');
setTimeout(() => {
resolve();
}, 1000);
})
.then(() => {
console.log('promise2');
})
.then(() => {
console.log('promise3');
})
.then(() => {
console.log('promise4');
});
a = new Promise(async (resolve, reject) => {
console.log(a);
await b;
console.log(a);
console.log('after1');
await a;
resolve(true);
console.log('after2');
});
console.log('end');
// 运行结果:
promise1
Promise {<pending>}
end
promise2
promise3
promise4
Promise {<pending>}
after1
卡在pending状态没有向下执行
防抖,节流是什么,如何实现
在平时开发的时候,会有很多场景会频繁触发事件,比如说搜索框实时发请求,onmousemove、resize、onscroll 等,有些时候,我们并不能或者不想频繁触发事件,这时候就应该用到函数防抖和函数节流。
函数防抖(debounce),指的是短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。
具体实现:
/**
* 函数防抖
* @param {function} func 一段时间后,要调用的函数
* @param {number} wait 等待的时间,单位毫秒
*/
function debounce(func, wait) {
// 设置变量,记录 setTimeout 得到的 id
let timerId = null;
return function (...args) {
if (timerId) {
// 如果有值,说明目前正在等待中,清除它
clearTimeout(timerId);
}
// 重新开始计时
timerId = setTimeout(() = {
func(...args);
}, wait);
};
}
函数节流(throttle),指连续触发事件但是在 n 秒中只执行一次函数。即 2n 秒内执行 2 次... 。节流如字面意思,会稀释函数的执行频率。
具体实现:
function throttle(func, wait) {
let context, args;
let previous = 0;
return function () {
let now = +new Date();
context = this;
args = arguments;
if (now - previous wait) {
func.apply(context, args);
previous = now;
}
};
}
给定两个数组,求交集
var intersection = function (nums1, nums2) {
let retset = new Set();
let retset1 = new Set(nums1);
for (const num of nums2) {
if (retset1.has(num)) {
retset.add(num);
}
}
return [...retset];
};
var intersection = function (nums1, nums2) {
let i = (j = 0),
len1 = nums1.length,
len2 = nums2.length,
newArr = [];
if (len1 === 0 || len2 === 0) {
return newArr;
}
nums1.sort(function (a, b) {
return a - b;
});
nums2.sort(function (a, b) {
return a - b;
});
while (i < len1 || j < len2) {
if (nums1[i] > nums2[j]) {
j++;
} else if (nums1[i] < nums2[j]) {
i++;
} else {
if (nums1[i] === nums2[j]) {
newArr.push(nums1[i]);
}
if (i < len1 - 1) {
i++;
} else {
break;
}
if (j < len2 - 1) {
j++;
} else {
break;
}
}
}
return newArr;
};
实现一个函数 clone 可以对 Javascript 中的五种主要数据类型(Number、string、 Object、Array、Boolean)进行复制
function clone(obj) {
var o;
switch (typeof obj) {
case 'undefined':
break;
case 'string':
case 'number':
case 'boolean':
o = obj;
break;
case 'object': // object 分为两种情况 对象(Object)或数组(Array)
if (obj === null) {
o = null;
} else {
if (Object.prototype.toString.call(obj).slice(8, -1) === 'Array') {
o = [];
for (var i = 0; i < obj.length; i++) {
o.push(clone(obj[i]));
}
} else {
o = {};
for (var k in obj) {
o[k] = clone(obj[k]);
}
}
}
break;
default:
o = obj;
break;
}
return o;
}