面试官: 你为什么使用前端框架?

简介:

面试官系列(5): 你为什么使用前端框架?


往期


前言

自 Backbone 之后前端框架就如同雨后春笋般出现,我们已经习惯了用各种框架进行开发,但是前端框架出现的意义是什么?我们为什么要选择前端框架进行开发呢?

提前声明: 我们没有对传入的参数进行及时判断而规避错误,仅仅对核心方法进行了实现.


文章目录

  1. 前端框架的根本意义
  2. 一个完整的前端框架是怎样的
  3. 基于Proxy如何实现响应式系统

1.前端框架的根本意义

1.1 前端框架的好处

最开始学习前端框架的时候(我第一个框架是 React)并不理解框架能带来什么,只是因为大家都在用框架,最实际的一个用途就是所有企业几乎都在用框架,不用框架就 out 了.

随着使用的深入我逐渐理解到框架的好处:

  1. 组件化: 其中以 React 的组件化最为彻底,甚至可以到函数级别的原子组件,高度的组件化可以是我们的工程易于维护、易于组合拓展。
  2. 天然分层: JQuery 时代的代码大部分情况下是面条代码,耦合严重,现代框架不管是 MVC、MVP还是MVVM 模式都能帮助我们进行分层,代码解耦更易于读写。
  3. 生态: 现在主流前端框架都自带生态,不管是数据流管理架构还是 UI 库都有成熟的解决方案。

1.2 前端框架的根本意义

上一节我们只说了前端框架的好处,但是并没有指出根本问题,直到我看到这篇文章(中文版)。

简单来说,前端框架的根本意义是解决了UI 与状态同步问题

在 Vue 中我们如果要在todos中添加一条,只需要app4.todos.push({ text: '新项目' }),这时由于 Vue 内置的响应式系统会自动帮我们进行 UI 与状态的同步工作.

<div id="app-4">
  <ol>
    <li v-for="todo in todos">
      {{ todo.text }}
    </li>
  </ol>
</div>
复制代码
var app4 = new Vue({
  el: '#app-4',
  data: {
    todos: [
      { text: '学习 JavaScript' },
      { text: '学习 Vue' },
      { text: '整个牛项目' }
    ]
  }
})
复制代码

如果我们用 JQuery 或者 JS 进行操作,免不了一大堆li.appendChilddocument.createElement等 DOM 操作,我们需要一长串 DOM 操作保证状态与 UI 的同步,其中一个环节出错就会导致 BUG,手动操作的缺点如下:

  1. 频繁操作 DOM 性能低下.
  2. 中间步骤过多,易产生 bug且不易维护,而且心智要求较高不利于开发效率

不管是 vue 的数据劫持、Angular 的脏检测还是 React 的组件级 reRender都是帮助我们解决 ui 与状态同步问题的利器。

这也解释了Backbone作为前端框架鼻祖在之后落寞的原因,Backbone只是引入了 MVC 的思想,并没有解决 View 与 Modal 同步的问题,相比于现代的三大框架直接操作 Modal 就可以同步 UI 的特性, Backbone 仍然与 JQuery 绑定,在 View 里操作 Dom来达到同步 UI 的目的,这显然是不符合现代前端框架设计要求的。


2.Vue 如何保证 UI 与状态同步

UI 在 MVVM 中指的是 View,状态在 MVVM 中指的是 Modal,而保证 View 和 Modal 同步的是 View-Modal。

Vue 通过一个响应式系统保证了View 与 Modal的同步,由于要兼容IE,Vue 选择了 Object.defineProperty作为响应式系统的实现,但是如果不考虑 IE 用户的话,Object.defineProperty并不是一个好的选择,具体请看面试官系列(4): 基于Proxy 数据劫持的双向绑定优势所在

我们将用 Proxy 实现一个响应式系统。

建议阅读之前看一下面试官系列(4): 基于Proxy 数据劫持的双向绑定优势所在中基于Object.defineProperty的大致实现。

2.1 发布订阅中心

一个响应式系统离不开发布订阅模式,因为我们需要一个 Dep保存订阅者,并在 Observer 发生变化时通知保存在 Dep 中的订阅者,让订阅者得知变化并更新视图,这样才能保证视图与状态的同步。

发布订阅模式请阅读面试官系列(2): Event Bus的实现

/**
 * [subs description] 订阅器,储存订阅者,通知订阅者
 * @type {Map}
 */
export default class Dep {
  constructor() {
    // 我们用 hash 储存订阅者
    this.subs = new Map();
  }
  // 添加订阅者
  addSub(key, sub) {
    // 取出键为 key 的订阅者
    const currentSub = this.subs.get(key);
    // 如果能取出说明有相同的 key 的订阅者已经存在,直接添加
    if (currentSub) {
      currentSub.add(sub);
    } else {
      // 用 Set 数据结构储存,保证唯一值
      this.subs.set(key, new Set([sub]));
    }
  }
  // 通知
  notify(key) {
  // 触发键为 key 的订阅者们
    if (this.subs.get(key)) {
      this.subs.get(key).forEach(sub => {
        sub.update();
      });
    }
  }
}

复制代码

2.2 监听者的实现

我们在订阅器 Dep 中实现了一个notify方法来通知相应的订阅这们,然而notify方法到底什么时候被触发呢?

当然是当状态发生变化时,即 MVVM 中的 Modal 变化时触发通知,然而Dep 显然无法得知 Modal 是否发生了变化,因此我们需要创建一个监听者Observer来监听 Modal, 当 Modal 发生变化的时候我们就执行通知操作。

vue 基于Object.defineProperty来实现了监听者,我们用 Proxy 来实现监听者.

Object.defineProperty监听属性不同, Proxy 可以监听(实际是代理)整个对象,因此就不需要遍历对象的属性依次监听了,但是如果对象的属性依然是个对象,那么 Proxy 也无法监听,所以我们实现了一个observify进行递归监听即可。

/**
 * [Observer description] 监听器,监听对象,触发后通知订阅
 * @param {[type]}   obj [description] 需要被监听的对象
 */
const Observer = obj => {
  const dep = new Dep();
  return new Proxy(obj, {
    get: function(target, key, receiver) {
      // 如果订阅者存在,直接添加订阅
      if (Dep.target) {
        dep.addSub(key, Dep.target);
      }
      return Reflect.get(target, key, receiver);
    },
    set: function(target, key, value, receiver) {
       // 如果对象值没有变,那么不触发下面的操作直接返回    
      if (Reflect.get(receiver, key) === value) {
        return;
      }
      const res = Reflect.set(target, key, observify(value), receiver);
      // 当值被触发更改的时候,触发 Dep 的通知方法
      dep.notify(key);
      return res;
    },
  });
};

/**
 * 将对象转为监听对象
 * @param {*} obj 要监听的对象
 */
export default function observify(obj) {
  if (!isObject(obj)) {
    return obj;
  }

  // 深度监听
  Object.keys(obj).forEach(key => {
    obj[key] = observify(obj[key]);
  });

  return Observer(obj);
}
复制代码

2.3 订阅者的实现

我们目前已经解决了两个问题,一个是如何得知 Modal 发生了改变(利用监听者 Observer 监听 Modal 对象),一个是如何收集订阅者并通知其变化(利用订阅器收集订阅者,并用notify通知订阅者)。

我们目前还差一个订阅者(Watcher)

// 订阅者
export default class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm; // vm 是 vue 的实例
    this.exp = exp; // 被订阅的数据
    this.cb = cb; // 触发更新后的回调
    this.value = this.get(); // 获取老数据
  }
  get() {
    const exp = this.exp;
    let value;
    Dep.target = this;
    if (typeof exp === 'function') {
      value = exp.call(this.vm);
    } else if (typeof exp === 'string') {
      value = this.vm[exp];
    }
    Dep.target = null;
    return value;
  }
  // 将订阅者放入待更新队列等待批量更新
  update() {
    pushQueue(this);
  }
  // 触发真正的更新操作
  run() {
    const val = this.get(); // 获取新数据
    this.cb.call(this.vm, val, this.value);
    this.value = val;
  }
}

复制代码

2.4 批量更新的实现

我们在上一节中实现了订阅者( Watcher),但是其中的update方法是将订阅者放入了一个待更新的队列中,而不是直接触发,原因如下:

因此这个队列需要做的是异步去重,因此我们用 Set作为数据结构储存 Watcher 来去重,同时用Promise模拟异步更新。

// 创建异步更新队列
let queue = new Set()

// 用Promise模拟nextTick
function nextTick(cb) {
    Promise.resolve().then(cb)
}

// 执行刷新队列
function flushQueue(args) {
    queue.forEach(watcher => {
            watcher.run()
        })
    // 清空
    queue = new Set()
}

// 添加到队列
export default function pushQueue(watcher) {
    queue.add(watcher)
    // 下一个循环调用
    nextTick(flushQueue)
}

复制代码

2.5 小结

我们梳理一下流程, 一个响应式系统是如何做到 UI(View)与状态(Modal)同步的?

我们首先需要监听 Modal, 本文中我们用 Proxy 来监听了 Modal 对象,因此在 Modal 对象被修改的时候我们的 Observer 就可以得知。

我们得知Modal发生变化后如何通知 View 呢?要知道,一个 Modal 的改变可能触发多个 UI 的更新,比如一个用户的用户名改变了,它的个人信息组件、通知组件等等组件中的用户名都需要改变,对于这种情况我们很容易想到利用发布订阅模式来解决,我们需要一个订阅器(Dep)来储存订阅者(Watcher),当监听到 Modal 改变时,我们只需要通知相关的订阅者进行更新即可。

那么订阅者来自哪里呢?其实每一个组件实例对应着一个订阅者(正因为一个组件实例对应一个订阅者,才能利用 Dep 通知到相应组件,不然乱套了,通知订阅者就相当于间接通知了组件)。

当订阅者得知了具体变化后它会进行相应的更新,将更新体现在 UI(View)上,至此UI 与 Modal 的同步完成了。

完整代码已经在 github 上,目前只实现了一个响应式系统,接下来会逐步实现一个完整的迷你版 mvvm 框架,所以你可以 star 或者 watch 来关注进度.


3.响应式系统并不是全部

响应式系统虽然是 Vue 的核心概念,但是一个响应式系统并不够.

响应式系统虽然得知了数据值的变化,但是当值不能完整映射 UI 时,我们依然需要进行组件级别的 reRender,这种情况并不高效,因此 Vue 在2.0版本引入了虚拟 DOM, 虚拟 DOM进行进一步的 diff 操作可以进行细粒度更高的操作,可以保证 reReander 的下限(保证不那么慢)。

除此之外为了方便开发者,vue 内置了众多的指令,因此我们还需要一个 vue 模板解析器.

下期预告

老掉牙问题,虚拟 DOM, 虚拟 DOM 的实现以及 diff 算法的优化。


作者:寻找海蓝96
链接:https://juejin.im/post/5b16c0415188257d42153bac
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
相关文章
|
4月前
|
缓存 前端开发 中间件
[go 面试] 前端请求到后端API的中间件流程解析
[go 面试] 前端请求到后端API的中间件流程解析
|
1月前
|
缓存 前端开发 JavaScript
"面试通关秘籍:深度解析浏览器面试必考问题,从重绘回流到事件委托,让你一举拿下前端 Offer!"
【10月更文挑战第23天】在前端开发面试中,浏览器相关知识是必考内容。本文总结了四个常见问题:浏览器渲染机制、重绘与回流、性能优化及事件委托。通过具体示例和对比分析,帮助求职者更好地理解和准备面试。掌握这些知识点,有助于提升面试表现和实际工作能力。
66 1
|
4月前
|
前端开发 应用服务中间件 API
"揭秘!面试官必问:你是如何巧妙绕过跨域难题的?前端代理VS服务器端CORS,哪个才是你的秘密武器?"
【8月更文挑战第21天】在软件开发中,尤其前后端分离架构下,跨域资源共享(CORS)是常见的挑战。主要解决方案有两种:一是服务器端配置CORS策略,通过设置响应头控制跨域访问权限,无需改动前端代码,增强安全性;二是前端代理转发,如使用Nginx或Webpack DevServer在开发环境中转发请求绕过同源策略,简化开发流程但不适用于生产环境。生产环境下应采用服务器端CORS策略以确保安全稳定。
60 0
|
7月前
|
存储 缓存 前端开发
web前端常见的面试题总结,2024年最新面试时有哪些技巧
web前端常见的面试题总结,2024年最新面试时有哪些技巧
web前端常见的面试题总结,2024年最新面试时有哪些技巧
|
7月前
|
JavaScript 前端开发
前端面试02(JS)
本文是前端面试系列的第二篇,主要涵盖了JavaScript的基础知识,包括JS的组成(ECMAScript、DOM、BOM)、内置对象(如String、Array、Math、Date等)、数组操作方法、数据类型检测方法(typeof、instanceof、constructor、Object.prototype.toString.call)、闭包的概念及其特点、前端内存泄漏的原因和类型、事件委托的优势、基本数据类型与引用数据类型的差异、原型链的工作原理以及JS实现继承的多种方式(原型链、构造函数、组合继承等)。文章结尾鼓励读者点赞和支持作者。
39 1
|
7月前
|
JavaScript 前端开发 程序员
web前端入门到实战:32道常见的js面试题(1),面试哪些
web前端入门到实战:32道常见的js面试题(1),面试哪些
|
7月前
|
前端开发 JavaScript
web前端JS高阶面试题,2024我的前端大厂面试之旅
web前端JS高阶面试题,2024我的前端大厂面试之旅
|
7月前
|
JavaScript 前端开发
web前端JS高阶面试题(1),高级开发工程师面试
web前端JS高阶面试题(1),高级开发工程师面试
|
7月前
|
前端开发 算法 搜索推荐
Web前端开发之面试题全解析 一(1),2024年最新前端组件化面试
Web前端开发之面试题全解析 一(1),2024年最新前端组件化面试
|
7月前
|
JSON JavaScript 前端开发
web前端入门到实战:32道常见的js面试题,2024年最新秋招是直接面试吗
web前端入门到实战:32道常见的js面试题,2024年最新秋招是直接面试吗
下一篇
DataWorks