哈罗面试

278 阅读16分钟

1. webpack的实现原理

webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:

  1. 初始化参数:从配置文件和Shell语句中读取与合并参数,得出最终的参数;
  2. 开始编译: 用上一步得到的参数初始化Complier对象,加载所有配置的插件,执行对象的run方法开始执行编译;
  3. 确定入口: 根据配置中的entry找出所有入口文件;
  4. 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤知道所有入口依赖的文件都经过了本步骤的处理;
  5. 完成模块编译: 在经过第4步使用Loader翻译完所有模块后,得到了每个模块被翻译后的最终内容以及他们之间的依赖关系;
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
  7. 输出完成: 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

Webpack的构建流程可以分为以下三个阶段:

  1. 初始化:启动构建,读取与合并配置参数,加载Plugin,实例化Complier.
  2. 编译:从Entry出发,针对每个Module串行调用对应的Loader去翻译文件内容,再找到该Module依赖的Module,递归地进行编译处理。
  3. 输出: 对编译后的Module组合成Chunk,把Chunk转换成文件,输出到文件系统。
    如果只执行一次构建,以上阶段将会按照顺序各执行一次。但在开启监听模式下,流程将变为如下

image.png

参考

2. vuex的实现原理

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,

  • state,驱动应用的数据源;
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化。

Vuex 和单纯的全局对象有以下两点不同:

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。(也就是所谓的MVVM)
  • 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutations参考

2.1. vuex是如何实现双向数据绑定的

vuex实际是自己内部有一个vue组件 
把state 和getter写到这个vue的data和computer里。
然后整个store实例是挂载到外部vue项目的$store里。

vue 的双向绑定 是对他自己的data通过objectProperty() 添加get set做监视

class Store {
  constructor(options) {
    this.vm = new Vue({
      data() {
        return {
          temp_state: options.state
        };
      }
    });
  }
  get state() {
    return this.vm.temp_state;
  }
}

参考 参考2

3. vue3用什么实现双向数据绑定?跟2.0的区别?

  1. VUE2.0通过Object.defineProperty来劫持对象属性的getter和setter操作,当数据发生变化时发出通知

  2. VUE3.0通过Proxy来劫持数据,当数据发生变化时发出通知

Object.defineProperty的缺陷:
1)无法检测到对象属性的新增或删除
    由于js的动态性,可以为对象追加新的属性或者删除其中某个属性,
    这点对经过Object.defineProperty方法建立的响应式对象来说,
    只能追踪对象已有数据是否被修改,无法追踪新增属性和删除属性,
    这就需要另外处理。
2)不能监听数组的变化(对数组基于下标的修改、对于 .length 修改的监测)
   vue在实现数组的响应式时,它使用了一些hack,
   把无法监听数组的情况通过重写数组的部分方法来实现响应式,
   这也只限制在数组的push/pop/shift/unshift/splice/sort/reverse七个方法,
   其他数组方法及数组的使用则无法检测到, 
解决方法主要是使用proxy属性,这个proxy属性是ES6中新增的一个属性,
    proxy属性也是一个构造函数,他也可以通过new的方式创建这个函数,
    表示修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种元编程
    proxy可以理解为在目标对象之前架设一层拦截,外界对该对象的访问,都必须经过这层拦截,
    因此提出了一种机制,可以对外界的网文进行过滤和改写,proxy这个词是代理,
    用来表示由他代理某些操作,可以译为代理器

    Proxy,字面意思是代理,是ES6提供的一个新的API,用于修改某些操作的默认行为,
    可以理解为在目标对象之前做一层拦截,外部所有的访问都必须通过这层拦截,
    通过这层拦截可以做很多事情,比如对数据进行过滤、修改或者收集信息之类。
    借用proxy的巧用的一幅图,它很形象的表达了Proxy的作用。
proxy代理的特点:
    proxy直接代理的是整个对象而非对象属性,
    proxy的代理针对的是整个对象而不是像object.defineProperty针对某个属性,
    只需要做一层代理就可以监听同级结构下的所有属性变化,
    包括新增的属性和删除的属性
proxy代理身上定义的方法共有13种,其中我们最常用的就是set和get,但是他本身还有其他的13种方法

proxy的劣势:
    兼容性问题,虽然proxy相对越object.defineProperty有很有优势,但是并不是说proxy,就是完全的没有劣势,主要表现在以下的两个方面:
        1)proxy有兼容性问题,无完全的polyfill:
            proxy为ES6新出的API,浏览器对其的支持情况可在w3c规范中查到,通过查找我们可以知道,
            虽然大部分浏览器支持proxy特性,但是一些浏览器或者低版本不支持proxy,
            因此proxy有兼容性问题,那能否像ES6其他特性有polyfill解决方案呢?,
            这时我们通过查询babel文档,发现在使用babel对代码进行降级处理的时候,并没有合适的polyfill
        2)第二个问题就是性能问题,proxy的性能其实比promise还差,
        这就需要在性能和简单实用上进行权衡,例如vue3使用proxy后,
        其对对象及数组的拦截很容易实现数据的响应式,尤其是数组

        虽然proxy有性能和兼容性处理,但是proxy作为新标准将受到浏览器厂商重点持续的性能优化,
        性能这块会逐步得到改善
复制代码

vue 2.0

// 触发更新视图
function updateView() {
    console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        updateView() // 触发视图更新
        oldArrayProperty[methodName].call(this, ...arguments)
        // Array.prototype.push.call(this, ...arguments)
    }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度监听
                observer(newValue)

                // 设置新值
                // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 污染全局的 Array 原型
    // Array.prototype.push = function () {
    //     updateView()
    //     ...
    // }

    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

    // 重新定义各个属性(for in 也可以遍历数组)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度监听
    },
    nums: [10, 20, 30]
}

// 监听数据
observer(data)

// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组

vue 3.0

// const data = {
//     name: 'zhangsan',
//     age: 20,
// }
const data = ['a', 'b', 'c']

const proxyData = new Proxy(data, {
    get(target, key, receiver) {
        // 只处理本身(非原型的)属性
        const ownKeys = Reflect.ownKeys(target)
        if (ownKeys.includes(key)) {
            console.log('get', key) // 监听
        }

        const result = Reflect.get(target, key, receiver)
        return result // 返回结果
    },
    set(target, key, val, receiver) {
        // 重复的数据,不处理
        if (val === target[key]) {
            return true
        }

        const result = Reflect.set(target, key, val, receiver)
        console.log('set', key, val)
        // console.log('result', result) // true
        return result // 是否设置成功
    },
    deleteProperty(target, key) {
        const result = Reflect.deleteProperty(target, key)
        console.log('delete property', key)
        // console.log('result', result) // true
        return result // 是否删除成功
    }
})

4. 前端微服务的实现原理?iframe架构的缺点?

image.png single-spa微前端方案结合了MPA和SPA的优势,可以在单个页面内集成多个应用,并且是技术栈无关的

资源模块加载器: 用来加载子项目初始化资源。我们将子项目的入口js构建成umd格式,然后使用模块加载器远程加载,通常会使用SystemJs(不是必须)通用模块加载器来进行加载。

子应用资源配置表: 用来记录各个子应用的入口资源url信息,以便在切换不同子应用时使用模块加载器去远程加载。因为每次子应用更新后入口资源的hash通常会变化,所以需要服务端定时去更新该配置表,以便框架能及时加载子应用最新的资源。

注意: single-spa本身是不支持子应用资源列表的,每个子应用只能将自己所有初始化资源打包到一个入口js中。如果子应用初始化资源有多个文件(可以通过webpack-manifest-plugin生成应用初始化资源清单),就需要按照上述方式来添加额外处理。

在微前端架构中,各个子应用可以基于不同的技术框架,这也是它最大的优势之一。这是因为各个子应用是独立编译和部署的,而基座应用是在运行时动态加载的子应用,由于在启动子应用时已经经历过编译阶段,所以基座应用加载的都是原生JavaScript代码,自然与子应用所用的技术框架无关(qiankun甚至能加载jQuery编写的页面)。

概念性地讲,在微前端架构中,各个子应用将一些特定的业务功能封装在一个业务黑箱中,只对外暴露少量生命周期方法;基座应用根据路由地址变化,动态地加载对应的业务黑箱,并将其渲染到指定的占位DOM元素上。与Vue应用一样,微前端也可以一次加载多个业务黑箱,这称为多实例模式(类似于vue-router的命名视图)。

iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。

  1. url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
  2. UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
  3. 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
  4. 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。

5. 谈一下原型链

image.png 其实原型链就是多个对象通过 __proto__ 的方式连接了起来。为什么 obj 可以访问到 valueOf 函数,就是因为 obj 通过原型链找到了 valueOf 函数。

总结:

  • Object 是所有对象的爸爸,所有对象都可以通过 __proto__ 找到它
  • Function 是所有函数的爸爸,所有函数都可以通过 __proto__ 找到它
  • Function.prototype 和 Object.prototype 是两个特殊的对象,他们由引擎创建出来的
  • 除了以上两个特殊对象,其他对象都是通过构造器 new 出来的
  • 函数的 prototype 是一个对象,也就是原型
  • 对象的 __proto__ 指向原型, __proto__ 将对象和原型连接起来组成了原型链

参考

var a={
     x:1,
     cal:function(z){
         return this.x+this.y+z;}
};
var b=Object.create(a,{y:{value:3}});
//对象a是对象b的原型对象,且b元素添加自身属性y,属性y的值设为3.

1. 检测对象是否有某个属性(对象自身+原型链)  typeof
typeof b.x+" "+typeof b.y+" "+typeof b.cal+" "+typeof b.get;
//输出 "number number function undefined"

2.只检测对象本身是否有某个属性(不回溯其原型链)  hasOwnProperty()
b.hasOwnProperty('cal')+" "+b.hasOwnProperty('x')+" "+b.hasOwnProperty('y')+b.hasOwnProperty('z');
//输出  false false true false 

5.1 获取当前原型上的所有属性

  • Object.keys返回该对象上所有可枚举的属性,不包括原型链
  • getOwnProperyNames返回可枚举和不可枚举的属性;返回对象上的属性为自身的属性;不会获取原型链上的值
  • hasOwnPropery返回可枚举的属性;返回一个布尔值,判断对象是否包含特定的自身(非继承)属性
  • for..in...返回所有可遍历枚举的属性,包括原型上
  • for...in循环对象的所有枚举属性,然后再使用hasOwnProperty()方法来忽略继承属性
function enumerable(obj) {
     if(typeof obj !== 'object') return
     var arr = [ ]
     if(Object.keys) {
            arr = Object.keys(obj)
     } else {
             for(var key in obj) {
             //javaScript 并没有保护 `hasOwnProperty` 属性名,因此,可能存在于一个包含此属性名的对象,有必要使用一个可扩展的`hasOwnProperty`方法来获取正确的结果:
             //或者({}).hasOwnProperty.call(obj, key);
                      if(Object.prototype.hasOwnProperty.call(obj, key )){
                              arr.push(key )
                      }
         }
   }
  return arr
 
}

参考

6. 谈一下node

node.js是一个异步的事件驱动的JavaScript运行时

  • 优点:“事件驱动、异步、非阻塞”。
  • 缺点: 单线程, 每次只能服务一个访问,多核的CPU利用的不充分。

7. 手写npm、cli

8. vue-router的实现原理

通过改变 URL,在不重新请求页面的情况下,更新页面视图

  1. hash 利用 URL 中的hash("#")
1. hash 虽然出现在 url 中,但不会被包括在 http 请求中,它是用来指导浏览器动作的,对服务器端完全无用,因此,改变 hash 不会重新加载页面。
2. 可以为 hash 的改变添加监听事件:
  window.addEventListener("hashchange",funcRef,false)
3. 每一次改变 hash(window.localtion.hash),都会在浏览器访问历史中增加一个记录。

利用 hash 的以上特点,就可以来实现前端路由"更新视图但不重新请求页面"的功能了。
两个方法:HashHistory.push() 和 HashHistory.replace()
  1. history 利用History interface ** 在HTML5中新增的pushState()、replaceState()方法
History interface 是浏览器历史记录栈提供的接口,通过back()、forward()、go()等方法,我们可以读取浏览器历史记录栈的信息,进行各种跳转操作。

从 HTML5开始,History interface 提供了2个新的方法:pushState()、replaceState() 使得我们可以对浏览器历史记录栈进行修改(当调用他们修改浏览器历史栈后,虽然当前`url`改变了,但浏览器不会立即发送请求该`url`):
window.history.pushState(stateObject,title,url)
window.history,replaceState(stateObject,title,url)

1.push
与hash模式类似,只是将window.hash改为history.pushState
2.replace
与hash模式类似,只是将window.replace改为history.replaceState
3.监听地址变化
在HTML5History的构造函数中监听popState(window.onpopstate

两种模式比较

  1. pushState设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,故只可设置与当前同文档的URL
  2. pushState通过stateObject可以添加任意类型的数据到记录中;而hash只可添加短字符串
  3. pushState可额外设置title属性供后续使用
  4. history模式则会将URL修改得就和正常请求后端的URL一样,如后端没有配置对应/user/id的路由处理,则会返回404错误

参考

setTimeout、setInterval 和 requestAnimationFrame 之间的区别

计时器一直是 JavaScript 动画的核心技术,而编写动画循环的核心是要知道延迟时间多长合适。一方面循环间隔必须足够短,这样才能让不同的动画效果显得平滑顺畅,另一方面循环间隔要足够的长,这样才能保证浏览器有能力渲染产生的变化。 大多数电脑显示器的刷新频率是60Hz,大概相当于每秒重绘60次。大多数浏览器都会对重绘操作加以限制,不会超出显示器的重绘频率。

setTimeout 和 setInterval 的缺点是他们都不够精确。它们内在的运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器 UI 线程队列中等待执行的时间,如果队列中已经加入了其他任务,那么动画的执行要等前面的任务结束之后才会执行。

requestAnimationFrame采用的是系统时间间隔,保证了最佳绘制效率。不会因间隔时间过短,造成过度绘制,增加开销;也不会因时间间隔太长,造成动画卡顿。它能够让各种网页动画有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。

  • requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。
  • 在隐藏或不可见的元素中,requsetAnimationFrame 将不会进行重绘或回流,这就意味着更少的 CPU、GPU 和内存使用量。
  • requestAnimationFrame是浏览器专门提供的 api,在运行时浏览器会自动优化方法的调用,并且页面不是激活状态下,动画会暂停执行,有效节省 CPU 开销。
  1. 执行次数:setInterval执行多次,setTimeout、requestAnimationframe执行一次

  2. 性能:setTimeout会出现丢帧、卡顿现象,setInterval会出现调用丢失情况,requestAnimationframe不会出现这些问题,页面未激活时不会执行动画,减少了大量cpu消耗

  3. 兼容性问题:setInterval,setTimeout在IE浏览器中不支持参数传递,能够在大多数浏览器中正常使用。而requestAnimationframe不兼容IE10以下