现代前端框架存在的根本原因
原文地址 The deepest reason why modern JavaScript frameworks exist
我见过很多前端开发者盲目的使用框架,Angular、React、Vue。确实是,这些框架提供了很多有意思的东西,但是,通常人们忽略了这些框架存在的最根本原因。
这个原因并不是因为这些框架:
- 提供了组件式开发
- 有很强的社区
- 可以利用三方库来处理各种各样的问题
- 有三方组件库
- 有完善的浏览器插件方便调试
- 擅长搭建spa
最重要的、根本的、深层愿意按有且只有一个
没错,就是它,为啥呢?
假设你在做一个web应用,用户可以通过邮件邀请其他人。设计师做了如下设计:
- 初始状态下,有一个空白输入框,可以输入用户邮箱地址,下方带着一些帮助文案
- 发起邀请之后,展示已经邀请的用户邮箱地址,并且可以删除
大概长这样
表单数据基本上可以抽象为一个对象数组,数组每一项有一个id和邮箱地址。 初始状态下,数组是空的,当输入邀请人的邮箱地址按下enter键之后,在数组内新增一项并且更新ui。当用户点击删除按钮的时候,删除数组中对应项并且更新ui。
你品,你细品,每次更改数据都需要更新ui界面
BUT,这意味着什么?
我们来一起看下,如果不借助任何框架,使用纯js实现应该怎么做?
<!-- 页面结构 -->
<html>
<body>
<div id="addressList">
<form>
<input>
<p class="help">Type an email address and hit enter</p>
<ul></ul>
</form>
</div>
</body>
</html>
class AddressList {
constructor(root) {
// state variables
this.state = []
// UI variables
this.root = root
this.form = root.querySelector('form')
this.input = this.form.querySelector('input')
this.help = this.form.querySelector('.help')
this.ul = root.querySelector('ul')
this.items = {} // id -> li element
// event handlers
this.form.addEventListener('submit', e => {
e.preventDefault()
const address = this.input.value
this.input.value = ''
this.addAddress(address)
})
this.ul.addEventListener('click', e => {
const id = e.target.getAttribute('data-delete-id')
if (!id) return // user clicked in something else
this.removeAddress(id)
})
}
addAddress(address) {
// state logic
const id = String(Date.now())
this.state = this.state.concat({ address, id })
// UI logic
this.updateHelp()
const li = document.createElement('li')
const span = document.createElement('span')
const del = document.createElement('a')
span.innerText = address
del.innerText = 'delete'
del.setAttribute('data-delete-id', id)
this.ul.appendChild(li)
li.appendChild(del)
li.appendChild(span)
this.items[id] = li
}
removeAddress(id) {
// state logic
this.state = this.state.filter(item => item.id !== id)
// UI logic
this.updateHelp()
const li = this.items[id]
this.ul.removeChild(li)
}
// utility method
updateHelp() {
if (this.state.length > 0) {
this.help.classList.add('hidden')
} else {
this.help.classList.remove('hidden')
}
}
}
const root = document.getElementById('addressList')
new AddressList(root)
上面的代码很好的展示了,使用原生js实现一个稍微有点复杂的ui,所需要的工作量。
使用jquery也基本一样
上述例子中,页面的静态结构时使用HTML创建;动态结构是使用JS创建。那么第一个问题来了:我们需要使用2种方式来构建UI,并且使用JS创建UI可读性并不高。 当然,我们可以使用innerHTML
来提高JS创建UI的可读性,但是这种做法性能不高,并且容易引起XSS攻击。我们也可以使用模板引擎,但是在构建一个超大的DOM时,存在2个问题: 性能问题和事件注册问题。
但是,这都不是最大的问题,最大的问题始终是:状态与UI的同步问题。 每一次修改数据,都需写一堆代码去手动更新UI。 上面例子中,新增一个邮箱地址,状态的更新只需要2行代码,但是由此导致的UI更新需要13行代码来实现。 即使是我们已经很大程度的简化的UI界面,真正的开发环境UI界面往往是更复杂的。
上述展示了手动维护状态与UI的同步问题的难度和复杂度,还有一个更重要的问题: 这种维护方式太脆弱了。 假设我在上述的基础上,增加了一个需求: 邮件列表需要与server端同步。 为了实现这个,我们需要从后端取数据然后比较,这就涉及到对比所有的id和邮件地址数据。
有可能出现本地数据与远端数据不一致的问题
我们需要写很多特定的、无法复用的代码来高效的操作DOM。 一旦我们犯了哪怕很小的一个错误,都会导致数据状态与UI界面的不一致:
- 某些信息丢失
- 展示错误信息
- 对用户的交互没有按照预期的执行,甚至造成误操作
因此,为了维护状态与UI的一致性,我们不得不写很多防御性的代码
救世主: 声明式UI
因此,框架存在的根本原因,并不是繁荣的社区、或是配套工具、或是完善的生态、或是众多的三方库等其他原因
目前为止,这些框架相对原生JS最大的提升就是,他们提供了一套可靠的机制,帮助开发者实现了状态与UI的同步一致
忽略掉这些框架的条条框框的限制
有了这些框架,我们只需要在一个地方构建UI,不需要在每次状态变化的时候动态的去增删一些页面UI结构;然后对每个状态变动都有预期的输出。状态变化之后框架会自动的去更新。
怎么做到的呢?
框架实现状态与UI的同步,一般有2种方案
- 重新渲染整个组件: 代表是React。主要思路就是,当组件的状态更新,在内存中渲染一个DOM,然后与真实DOM对比。 但是因为DOM操作是比较耗费性能的,因此使用了虚拟DOM来替代。在状态更新之后,根据最新状态构建一个虚拟DOM,然后与根据当前页面真实DOM构建的虚拟DOM做对比,计算出变化的部分,然后更新到真正DOM中去。这个过程叫做协调。
- 使用watcher监测变化: angular和vue。框架维护了状态与使用了这些状态的组件依赖关系,一旦状态发生变化,依赖这些状态的组件会被更新。
我们说,
vue的更新粒度比React的更新
,这个说法的本质原因就在此。设计思路不一致,react没有去做依赖收集,状态变化之后,自顶而下的去更新组件
web components 是啥呢?
经常有人拿web components
与React、Angular、Vue.js作比较。这也说明了很多人还是没有理解这些框架的核心功能: 保持状态与UI的同步。 这是web components没有提供的, 它只提供了一个template标签,并没有提供协调相关的机制。如果你使用web components开发web应用,你只能手动的维护应用的内部状态与UI的同步,或者使用像Stencil.js
这样的三方库(它内部也是像React一样使用虚拟DOM)。
再次重申,这些框架的最核心功能并不是组件化,核心始终是状态与UI的一致性。Web components中这个功能并不是开箱即用的,需要配合第三方库或者手动维护。使用原生JS来实现复、高效的web应用是不可能的。 这才是这些现代框架存在的根本原因。
自己实现一个
我喜欢去探究事物的本质,在框架中最核心的就是虚拟DOM的实现。那么,我们可以尝试借助使用现有的虚拟DOM方案来重新实现一下上述的需求,不借助任何现有的框架。
下面是核心的代码,组件的基类。
这里作者使用了
virtual-dom
这个库
// 组件基类
const dom = require('virtual-dom/h');
const diff = require('virtual-dom/diff');
const patch = require('virtual-dom/patch');
const createElement = require('virtual-dom/create-element');
module.exports.Component = class Component {
constructor(root) {
this.root = root;
this.tree = h('div', null);
this.rootNode = createElement(this.tree);
this.root.appendChild(this.rootNode);
this.state = this.getInitialState();
this.update();
this.componentDidMount();
}
setState(state) {
this.state = Object.assign(this.state || {}, state);
}
update() {
const newTree = this.render();
const patches = diff(this.tree, newTree);
this.rootNode = patch(this.rootNode, patches);
this.tree = mewTree;
}
componentDidMount() {}
getInitialState() {
return {};
}
render() {
return {};
}
}
下面是AddressList邮箱地址列表组件的实现,需要使用babel转译来支持JSX语法
const { Component } = require('./component');
class AddressList extends Component {
getInitialState() {
return {
addresses: []
}
}
componentDidMount() {
this.input = this.root.querySelector('input');
// 事件注册
// 添加邮件
this.root.addEventListener('submit', e => {
e.preventDefault();
const address = this.input.value;
this.input.value = '';
this.addAddress(address);
});
// 删除
this.root.addEventListener('click', e => {
const id = e.target.getAttribute('data-delete-id');
if (!id) return;
this.removeAddress(id);
})
}
// 现在增加邮箱之后,只需要更新数据即可,不需要开发者手动去更新ui界面了
addAddress(address) {
const id = String(Date.now());
this.setState({
addresses: this.state.addresses.concat({ address, id });
})
}
removeAddress(id) {
this.setState({
addresses: this.state.addresses.filter(address => address.id !== id);
})
}
render() {
return (
<div>
<form>
<input />
</form>
{
this.state.addresses.length === 0 &&
(<p class="help">
Type an email address and hit enter
<p>)
}
<ul>
{this.state.addresses.map(addr => (
<li key={addr.id}>
<a data-delete-id={addr.id}>delete</a>
<span>{addr.address}</span>
</li>
))}
</ul>
</div>
);
}
}
const root = document.getElementById('addressList');
new AddressList(root);
现在UI就是声明式的了,我们不需要使用任何框架。 我们可以根据逻辑需要随意修改数据状态,而不需要写额外代码来同步UI,问题解决!
至此,除了事件处理逻辑之外,这很像一个react应用,是吧?我们有render()
、componentDidMount()
、setState()
等; 一旦解决了状态同步的问题,其他的就不是问题了。
代码仓库地址 ui-state-sync
结论
- 现代框架解决的主要问题就是:状态与UI的同步问题
- 使用原生JS是不可能实现一个复杂而又高效的web应用
- web component不是用来解决这个问题的(状态与UI的同步问题)
- 借助现有的虚拟DOM方案,可以很容易的实现一个你自己的框架,但是duck不必。