san-view
san体系视图层框架
san体系视图层框架
License: MIT License
san体系视图层框架
特别是做类Flux结构的时候,如果给IoC留出了空间,那么是有机会证IoC来管理Store并和组件进行整合的,而组件可以这么做:
import {watch} from 'storeUtil';
class Foo extends Componet {
onAttach(state) {
// store由IoC提供
watch(this.store, ('foo', foo => this.setState({foo}));
}
}
当然还有很多其它的业务场景下,IoC是非常有用的,因此组件体系留出这个空间是很有必要的
当前的组件事件处理函数是返回一个Promise<Action>
,然后执行这个Action来获取新的状态
对于Optimistic UI,可以让事件处理函数返回[Promise<Action>, Action]
的一个tuple,其中后面的那个Action会立即被执行获取新的状态更新界面,而Promise在resolve之后的Action则再次被应用原来的状态获取真正的结果更新界面
从设计上可以得出,当前的组件设计并不妨碍后续Optimistic UI的改造,所以这个事项作为长远规划处理
在组件树中,数据流自上而下,数据由owner传递给各个关联的组件
数据传递为主动机制,不依赖任何的事件(即不是由子组件监听owner的change事件之类的方式)
Immutable在数据流中的作用参考 #1 ,不再强调
一个组件维护自己和其下面组件的绑定关系,当setState
被调用时,会自动通过绑定信息推送数据
pushStateUpdate({previousState, newState}) {
for (let {target, property, path} of this.bindings) {
let newValue = visit(newState, path);
binding.target.setState({[property]: newValue});
}
}
这个过程不作任何相等的判断,所有的判断都由patchState
完成,这导致数据流非常的清晰简单
通知流在组件树中自下而上
通知使用事件还是回调暂无定论,待讨论
在模板上,对于状态变化的通知有特殊的语法糖,如下:
<san-post on-like-change="onLikeChange"></san-post>
效果为监听�状态变化的通用事件,并从中过滤出like属性的变化调用onLikeChange
方法
双向绑定由数据流和通知流组成,如下声明:
<san-post bindx-like="likeCount"></san-post>
等效于:
<san-post bind-like="like" on-like-change="_syncLike"></san-post>
_syncLike(event) {
this.setState({like: event.newValue});
}
这种模式下双向绑定是一种语法糖,而不是内置的逻辑功能,保持了流的简单清晰(只有数据和通知两条流,不为其他模式开后门)
不分state和prop,所有状态可访问性为公开,称为state
组件的状态是不可变的,即每次修改状态都会创建一个新的state
对外暴露的方法为patchState(partialState)
另外还有replaceState(newState)
,用于更新整个状态,且replaceState(newState)
还会被用于数据流传递
patchState(partialState) {
let patch = Object.entries(partialState).reduce(
(patch, [key, value]) => {
if (value === this.state[key]) {
return patch;
}
return Object.assign(patch, {[key]: value});
},
{}
);
if (isEmpty(patch)) {
return {previousState: this.state, newState: this.state};
}
let newState = Object.assign({}, this.state, patch);
return this.replaceState(newState);
}
replaceState(newState) {
if (this.state === newState) {
return {previousState: this.state, newState: this.state};
}
let previousState = this.state;
this.state = newState;
let change = {previousState, newState};
this.pushStateUpdate(change); // 将状态同步到其它组件
return change;
}
基于Immutable的设计下,组件收到一个新的状态(通过replaceState(newState)
调用)时,只要检查引用是否与当前状态相等就行,如果不相等则直接开始进行数据的向下传递,不对状态进行任何深层的判断,这保证了数据流的简单,简单又进一步带动可靠,这是san-view的主要目的之一
组件内部可通过protected
的方法notifyStateChange({previousState, newState})
对外通知状态的变化,这一方法会触发相关的事件/回调
每一个事件处理函数接收当前的状态,并返回一个Promise,该Promise在resolve时提供一个函数(后称为Action),Action接收当前组件的状态并返回状态的补丁(参考上文patchState
)
同时事件处理函数和Action均接收一个output
参数,该参数为一个函数,用于对外发送通知(使用事件或回调的形式,具体由组件控制)
用范式表达为:
async (state, output, ...eventArgs) => (state, output) => newState
一个简单的业务逻辑如下:
async onLike(state, output, event) {
output('startnetwork');
let likeCount = await api.updateLikeCount(state.postId);
output('finishnetwork');
return (state, output) => {
output('likeupdate');
return {like: likeCount};
};
}
注意事件处理函数得到的状态和Action得到的状态可能是不一样的(因为有异步)
事件处理函数的this
为当前组件实例,但非常不建议使用任何带有this
访问的代码,尽可能保持事件处理函数是个纯函数
一个事件相当于如下过程:
Promise<Action>
patchState
更新状态notifyStateChange
通知状态变化需要注意的是通过事件执行的新状态,都会自动通知状态变化,这与外部直接调用patchState
不同
注:最初设想一个事件处理函数是直接返回Promise<newState>
的,但是考虑到异步会导致状态错乱,所以设计成了Promise<Action>
,这会提升一定的复杂性,但更稳健
组件渲染的核心为render(state)
,接收当前的状态进行处理
组件渲染只会由状态变化触发(暂无其它触发手段),调用patchState
后,使用一个所有组件共享的统一调度器进行更新调度
patchState(partialState) {
// ...前面的代码
scheduler.scheduleRender(this, change);
}
调度器全局共享一个,通过异步管理,将所有组件的渲染计划收集起来,一个周期后统一处理,一个组件在周期内有多次状态更新,会由调度器进行合并
这会导致在某些时刻状态和界面是不同步的,但这并不重要
在与函数式的配合上,render方法的返回值当前还未决定
从上文可以看到,组件中最频繁使用核心功能(状态更新、事件处理)都是纯函数,而不是当前社区中其他框架的依赖this.state
等一堆状态
这一特点决定了在san-view体系中,组件事实上是一个“状态的容器”,它本身的逻辑是纯函数,组件实例的存在用来存放状态,而这个存放本身对组件实现者也是透明的
我称这种组件为“纯组件”,与“函数式组件”不同,纯组件的特点是逻辑为纯函数,但本身具备对开发者透明的状态管理,这使得纯组件避开了一般组件状态与生命周期紧耦合难以测试的缺点,也不至于因为实际函数式组件导致数据流过于繁琐
其一个可见的特点为可以针对事件进行测试,可以参考一个简单的 普通组件 与 纯组件 的对比
对于特殊情况,如果一个事件处理函数不能是纯函数,需要用@sideeffect
标记,这个标记本身没啥用,但配合一系列工具会有特殊的效果(待定)
配合Flux架构,再提供诸如san-flux之类的框架做整合
基本思路依旧是单Store + reducer(mutation)的方式
类似Redux的架构中,一个很大的问题是没有提供将“负责界面和交互的组件”和“负责数据传递的组件”拆分开来的指导,这导致在部分质量不高的应用中,一个组件呈现出“部分属性与owner关联,部分属性与store关联,部分事件回调至owner,部分事件通过action送至store”这样的情况,这让逻辑流变得相对复杂
在san-flux中,通过一些官方性的指导,让开发者更倾向于控制一个组件只做一件事:
这样做可以得到的好处有:
容器组件的事件处理函数与展示组件不同,最后的Action返回为空(null
吧),即表示不直接产生新的状态,而是去对store进行操作(通过reducer/mutation之类的),再由store通知变化过来进行状态更新
组件是对一个state
的处理函数,而模板的语法限制必然会导致state
不能仅包含最小的状态,通常会有一些额外计算的状态要包含其中
那么这些额外计算的状态需要在整个状态更新的流程中有一个明确的点来做,比如这样:
class Component {
onReceiveState(newState, previousState) {
return newState;
}
}
具体组件可以通过重写这个方法,返回一个新的状态来添加一些属性,比如:
class Greeting extends Compnent {
onReceiveState(newState, previousState) {
return {
...newState,
message: `Hello ${newState.name}`,
diff: computeStringDiff(previousState.name, newState.name)
};
}
}
框架不负责追踪哪些属性是原始的状态,哪些是由onReceiveState
计算得出的,因此在整个组件的生命周期中,更多的时候state
里始终有计算出的属性,当状态更新而onReceiveState
未处理时,其中这些计算出的属性的值有可能是错误的,这就要求onReceiveState
的实现要足够小心,避免把这种错误的值继续传递下去,一个合理的方法是所有的计算而得的属性都重新计算
对于在UI树中自下而上的通知,使用事件还是callback是一个重点考虑的点,下面简单说下各自的优势,优势等于另一个的劣势
preventDefault
等接口多播经常发生在以下2个场景下:
<san-draggable><san-foo on-mouse-down="bar" /></san-draggable>
,draggable本身也需要内部组件的mousedown
事件对于业务的逻辑流,使用callback的形式(主线),对于其它场景,同时提供一个多播的事件模型
这样混用可以拆开主业务逻辑和其它框架逻辑,看上去很美好,但不确定会不会反而更混乱了
由于 #3 初步采取单store的flux架构,因此可以参考现有redux dev tool实现类似功能,基本包含:
基于命令行的工具,检测一个组件是不是定义中的“纯组件”,即检测各种事件处理函数是否为纯函数,对于非纯组件进行报告,并一定程度上指导开发者进行修改
在设想中,san-flux应该有一个工具,可以在选择组件树中的一个组件后,看到这个组件上的数据和通知的流入流出情况
在图形上表现为中间一个节点,在这个节点上:
这样的一个图能看到一个组件上的逻辑流,如果一个组件连接着多个来源则可以看到逻辑流比较复杂,基于此去思考设计的优化会很有帮助
如果需要有Server Render等功能,同时让组件更接近纯组件的概念,那么render
方法就必须有返回值,而不是直接基于template
去进行渲染
如果render
有返回值,那么必定是一个虚拟DOM的节点,这个东西应该称为Element
,那么对应方法签名比较合适的应该是:
{Element} render({Object} state, {Function} createElement)
具体代码就可能是这样的:
class Foo extends Component {
render(state, createElement) {
return createElement(
'div',
{
attributes: {className: ['ui-foo'].concat(state.customClass)}
},
[
{
Bar, // Custom component
{
state: {value: 123}
}
}
]
);
}
}
当使用template
的时候,就是编译成这个结构
在后续版本时,框架可以支持Optimistic UI,基本方法如下:
每个事件处理函数返回一个[Promise<Action>, Action]
,前者负责真正更新状态,后者负责产生Optimistic State。
每次应用Optimistic State的时候,记录以下内容:
比如有以下顺序:
初始状态state0,此时队列为[state0]
开始事件1,产生[Promise<Action11>, Action12]
应用Action12,此时队列为:
[
state0,
{state1: Action12(state0), Promise<Action11>, Action12, pending: true}
]
使用state1更新UI
开始事件2,产生[Promise<Action21>, Action22]
取最后的Optimistic State即state1应用Action22,此时队列为:
[
state0,
{state1: Action12(state0), Promise<Action11>, Action12, pending: true},
{state2: Action22(state1), Promise<Action21>, Action22, pending: true}
]
使用state2更新UI
假设事件2比事件1更早结束,则获取到Action21,使用在队列中Action21所在项之前的最后的Optimistic State即state1并应用Action21,但同时还要保留Action21(因为前面还有未完成的事件,这个状态并不是真正的最终状态),此时队列为:
[
state0,
{state1: Action12(state0), Promise<Action11>, Action12, pending: true},
{state3: Action21(state1), Promise<Action21>, Action22, pending: false} // 这里变成false了
]
使用state3更新UI
事件1结束,获取到Action11,使用队列中Action11所在项之前最后的Optimistic State,此时没有这样的状态,因此不做任何操作,把Action11相关的项的pending票房为false
[
state0
{state1: Action12(state0), Promise<Action11>, Action12, pending: false}, // 这里也变成false了
{state3: Action21(state1), Promise<Action21>, Action22, pending: false} // 这里变成false了
]
当有第7步(找不到最近的Optimistic State)时,在队列中从头到尾找所有pending为false的,一个一个应用到正式的State上,直到碰到pending为true的停下来,则队列会变成:
[
state4: Action21(Action11(state0))
]
将state4作为正式State并更新UI
从框架层面上,只要支持判断事件处理函数返回的是Promise
还是数组就行,因此看上去不会产生兼容性破坏
有模板的组件特指内部内容由外部决定的那些,模板分为一个默认模板和多个命名模板,比如
<san-toggle-panel>
<??? for="head">
Hello
</???>
<???>
<san-foo />
<san-bar />
</???>
</san-toggle-panel>
需要明确的是:
<???>
到底应该是什么,用<template>
/<script>
这种允许字符串的东西,还是干脆用<slot>
这种表意明确且和shadow dom规范相符的东西TogglePanel
内部怎么实现对这些模板的引用和渲染TogglePanel
要head部分的点击事件要如何处理(在不额外再包一层的情况下)A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.