随笔|实现“数据驱动”在原生js中的应用,只需两段核心代码

93 阅读4分钟

不论是vue、还是react,还是其他的现代前端框架,无论他们打着什么样的旗号进行宣传,更快的构建速度、更加简洁的API、更加优秀的性能,更加的方便的维护等等。

他们的核心其实无外乎两件事:单项数据流数据驱动

单项数据流说的是:组件数据的流转从外到内,从父组件到子组件

数据驱动说的是:组件的渲染依赖组件自身状态或着全局状态,当状态发生变化时,组件渲染的内容页跟着发生变化

尽管vue、react已经基本上是开发者的必选框架,但是依然有一些它们无法覆盖到的场景。

比如我们需要开发一款能够兼容市面上几乎所有机型的app,为了节约成本,提高开发效率以及迭代效率,webapp必然是我们首选的技术。

而webapp又有很多基于vue、react的框架,它们虽然能够快速迭代,单兼容性不够好。

需要我们做很多对js的兼容性进行处理,比如用pollify ,用babel做降级处理,甚至我们做了这些处理之后,依然有很多不可控的bug出现。比如:页面白屏,依赖过多导致加载缓缓等等。

所以,对于一些特定webapp的开发,原生js、html、css 依然是最好的技术选型

对于使用原生js进行webapp的开发,又很容易陷入一种堆叠代码的困境之中。

有时候当一个节面的业务过于复杂,需要根据不同的状态展示不同的UI,不同的UI又有不同的事件绑定,甚至组件之间还有一些耦合业务需要处理。

这时候,这个页面的js代码就会随着业务的迭代变的越来越臃肿,尤其是当多个人维护这个文件时,每个人的代码习惯都可能不一样,就会加速项目的不可维护性,进而增加项目出现生成事故的风险。

那么有没有一种可以借鉴vue、react的开发方式,使项目整体具有很好的维护性,又能够使用原生js增加项目整体兼容性的开发方式呢?

有,这里我提供一种思路供大家进行参考。

我的想法如下:

首先,使用原生js封装一个基类,实现组件化。

function BaseComponent(container, stateManager) {
  this.container = container;
  this.stateManager = stateManager;
  this.dom = null;
  this.children = [];
  this.init();
}

BaseComponent.prototype.init = function () {
  this.stateManager.subscribe(this);
  // this.dom = this.createDOM();
  this.render();
  this.bindEvents();
};

BaseComponent.prototype.createDOM = function (template) {
  // 使用模板字符串创建 DOM 元素
  // const template = this.getTemplate();
  const wrapper = document.createElement("div");
  wrapper.innerHTML = template;
  console.log("wrapper", wrapper, wrapper.firstChild);
  return wrapper;
};

BaseComponent.prototype.onStateUpdate = function () {
  this.render();
  this.updateDynamicParts();
};

BaseComponent.prototype.render = function () {
  // 抽象方法,由子类实现
  // 清空容器
  this.container.innerHTML = "";

  // 渲染自身 DOM
  if (this.dom) {
    this.container.appendChild(this.dom);
  }

  // 递归渲染子组件
  this.children.forEach((child) => {
    child.render();
  });
};

BaseComponent.prototype.updateDynamicParts = function () {
  // 局部更新逻辑
};

BaseComponent.prototype.bindEvents = function () {
  // 使用事件委托
  // this.container.addEventListener("click", (e) => {
  //   e.preventDefault() || e.stopPropagation();
  //   this.handleEvent(e);
  // });
  // 确保只添加一个事件监听器
  if (!BaseComponent.eventBound) {
    document.body.addEventListener("click", (e) => {
      e.preventDefault() || e.stopPropagation();
      this.handleEvent(e);
    });
    BaseComponent.eventBound = true;
  }
};

BaseComponent.prototype.handleEvent = function (e) {
  // 事件分发逻辑
};

// 添加子组件的方法
BaseComponent.prototype.addChild = function (childComponent) {
  this.children.push(childComponent);
  childComponent.container = this.container; // 设置子组件的容器
  childComponent.init(); // 初始化子组件
};

其次,使用js封装状态管理基类,实现状态管理,即数据驱动试图进行更新

代码如下:

function StateManager(initialState) {
  this.state = initialState || {};
  this.subscribers = [];
}

StateManager.prototype.subscribe = function (callback) {
  this.subscribers.push(callback);
};

StateManager.prototype.unsubscribe = function (callback) {
  this.subscribers = this.subscribers.filter((cb) => cb !== callback);
};

StateManager.prototype.setState = function (newState) {
  this.state = { ...this.state, ...newState };
  this.notifySubscribers();
};

StateManager.prototype.getState = function () {
  return this.state;
};

StateManager.prototype.notifySubscribers = function () {
  this.subscribers.forEach((component) => component.onStateUpdate());
};

这样一来,基本上就实现了类似vue中的状态管理,以数据驱动的方式进行原生js的项目开发

同时,可以实现组件化,只需要对BaseComponent基类进行扩展即可。

设想的项目整体架构如下:

src/
├── core/
│   ├── StateManager.js    // 状态管理中心
├── components/
│   ├── BaseComponent.js   // 组件基类
│   ├── order/
│   │   ├── index.js
│   │   └── style.css
│   
├── utils/
│   ├── tools.js           // 工具方法
│   └── api.js             // api请求
├── views/
│   ├── index.html  
└── app.js                 // 可以一些全局的东西

order/index.js的实现:

// 订单信息
function OrderInfo(container, stateManager) {
  BaseComponent.call(this, container, stateManager);
  this.init();
}

OrderInfo.prototype = Object.create(BaseComponent.prototype);
OrderInfo.prototype.constructor = OrderInfo;

OrderInfo.prototype.init = function () {
  BaseComponent.prototype.init.call(this);
};

OrderInfo.prototype.render = function () {
  const template = `<div class="cardOrderWrap flex">
                      <a href="javascript:;" class="OrderItem flex" data-tplName="js_order"><img src="../../../static/images/index_v2/order1.png" alt=""><span>原始订单</span></a>
                      <div class="cardLine"></div>
                      <a href="javascript:;" class="OrderItem flex" data-tplName="js_bill"><img src="../../../static/images/index_v2/order2.png" alt=""><span>原始账单</span></a>
                      </div>
                    `;
  this.dom = this.createDOM(template);
  this.container.replaceChildren(this.dom);
};

OrderInfo.prototype.handleEvent = function (e) {
  console.log("state---order", this.stateManager);
  let test = {
    showAmount: 8000,
  };
  this.stateManager.setState({
    ...this.stateManager.state,
    ...test,
  });
  // e.preventDefault() || e.stopPropagation();
};

index.html中的js

// index.js
const stateManager = new StateManager({
  showAmount7000,
});

let orderInfo = new OrderInfo(
  document.getElementById("orderInfo"),
  stateManager
);
let canloan = new CanLoan(document.getElementById("canlon"), stateManager);

这样一来,当orderInfo组件调用setState更新状态后,CanLoan组件如果引用了这个showAmount属性,则会触发组件的更新方法从而进行渲染,无需手动进行dom操作。

这种设计的优点如下:

  1. 可以让原生js开发产生使用现代框架类似的组件化和模块化
  2. 基于发布订阅模式的状态更新,加入了状态管理机制,状态快照机制保证数据不可变
  3. 组件职责分明、样式与结构分离、基类具有很好的扩展性

可以大大提升开发效率和项目整体的可维护性。

这种设计的缺点如下:

  1. html文件中需要导入大量的js 文件,后续如果能实现按需加载,则可以解决到这个问题。
  2. 虽然组件基类中添加了递归渲染子组件的逻辑,但无法像vue那样直接嵌套使用,稍显麻烦。

关注我,带你解锁更多前端技能!

注:如果你需要建站、小程序之类,随时欢迎联系

公众号《前端那些年》

(完)

记得点赞、关注、评论、转发一下。