本系列博文从 Shadow Widget 作者的视角,解释该框架的设计要点。本篇讲解双源属性、不可变数据、事件驱动等。
1. React 中的隐式双源
var mainComp = null; function Welcome(props) { return <h1>Hello,{props.name}</h1>; } class DivText extends React.Component { constructor(props) { super(props); this.state = {name:'Wayne'}; mainComp = this; } render() { return ( <div key='main'> <Welcome key='txt' name={this.state.name} /> </div> ); } } ReactDOM.render( <DivText />,document.getElementById('root') ); setTimeout( function() { mainComp.setState({name:'George'}); },5000);
这个例子创建的 component 树如下图,main@H_502_12@ 节点的
state.name@H_502_12@ 传递给
txt@H_502_12@ 节点用作
props.name@H_502_12@。
txt@H_502_12@ 节点初始显示
"Hello,Wayne"@H_502_12@,过 5 秒后切换为
"Hello,George"@H_502_12@。
<root node> +-- main // div | +-- txt // h1
我们研究一下 5 秒后切换都发生了什么,mainComp.setState({name:'George'})@H_502_12@ 一句更改
main@H_502_12@ 节点的
state.name@H_502_12@,然后系统触发下级
txt@H_502_12@ 节点的
props.name@H_502_12@ 变化,再驱动
txt@H_502_12@ 节点内容刷新。
本处 React 技术实现让初学者很费解,main@H_502_12@ 节点的
render@H_502_12@ 函数用 JSX 返回 Element,并非每次渲染都用
<Welcome>@H_502_12@ 创建子节点。
render() { return ( <div key='main'> <Welcome key='txt' name={this.state.name} /> </div> ); }
而是首次渲染时创建一次,其后 render()@H_502_12@ 调用只对已存在的节点做更新,由
props.name@H_502_12@ 变化驱动子节点内容刷新。所以,上面
txt@H_502_12@ 节点的
。由 props.name@H_502_12@ 对节点自身来说,是不变量,但对父节点来说,是可变量
state.xxx@H_502_12@ 驱动刷新与
props.xxx@H_502_12@ 驱动刷新本质是一回事,只不过 React 编程模型在表面弄了一点限制。
props.xxx@H_502_12@ 驱动的刷新是一个源头,
state.xxx@H_502_12@ 驱动的刷新是另一个源头,合起来是 "双源驱动"。
2. 改造双源驱动
由于 React 限定本节点 props.xxx@H_502_12@ 是只读的,我们通过改造,让一个节点既接受
props.xxx@H_502_12@ 驱动,也接受
state.xxx@H_502_12@ 驱动。让 React 隐式的双源驱动,变成显式的双源驱动,如下:
var txtComp = null; class Welcome extends React.Component { constructor(props) { super(props); this.state = {name:props.name}; this.oldName = props.name; txtComp = this; } render() { var name = this.state.name; if (this.oldName !== this.props.name) name = this.state.name = this.oldName = this.props.name; return <h1>Hello,{name}</h1>; } }
这样,在 txt@H_502_12@ 节点,既可用
txtComp.setState({name:'George'})@H_502_12@ 驱动刷新,也可由父节点传入的
props.name@H_502_12@ 变化来驱动刷新。我们额外要做的是,在
txt@H_502_12@ 节点用
this.oldName@H_502_12@ 记录
props.name@H_502_12@ 旧值,由
this.oldName !== this.props.name@H_502_12@ 来识别传入
props.name@H_502_12@ 是否变化了。
这么改造的意义在于:
自身状态变迁与父级驱动变迁,是两种普遍存在的现象,我们引用正规的 "双源驱动" 概念,便于将两种源头归一,如后面叙述,用
this.duals.xxx@H_502_12@ 表达,归一后才能构造事件发布与订阅的机制。
React 让 props 属性只读的设计有点尴尬,有违普遍认知。
如前面介绍,它不是不可变,而是限定本级与下级不可修改,这个规则对保障单向数据传递有利。但大众对 DOM 节点的认知是这样的,以<input>@H_502_12@ 为例,
type='button'@H_502_12@ 这个属性可以用
props.type@H_502_12@ 表达,因为生存周期里它不该有变化,而
title='for test'@H_502_12@ 属性应让本节点参与管理,生存期内可变。
让自身节点管理类似 props.name,props.title@H_502_12@ 的属性,大致有两种方法,其一,采取上面介绍的方法,让两个源头归一,再驱动本节点输出。其二,按严格的单向数据流要求,把代码写成下面样子:
class Welcome extends React.Component { constructor(props) { super(props); } setName(newName) { mainComp.setState({name:newName}); } render() { return <h1>Hello,{this.props.name}</h1>; } }
也就是借助父节点的 setState()@H_502_12@ 实现刷新,理论上,这也是单向数据流,理解有点别扭,自身节点的属性不能直接管理,非要到父节点跑一圈。
Shadow Widget 双源驱动的优点在于 "让 DOM 节点功能回归本原",让 props.xxx@H_502_12@ 服务于生存周期中不变量,让
duals.xxx@H_502_12@ 服务于可变量,
state.xxx@H_502_12@ 也服务于可变量,但倾向于用来表达自身节点的私有状态。
reflux 为实现 React flux 机制,仿 component 接口设计了 store,如果没有上述 props.xxx@H_502_12@ 限制,我相信把 component 与 store 合一远优于现有设计。回归原本的设计好处是潜在的,因为倾斜的地基会导致上层建筑更加倾斜。
3. 数据侦听机制
Shadow Widget 将双源驱动归一后,用 duals.attr@H_502_12@ 存取属性,而且系统内部对读写
duals.attr@H_502_12@ 做了封装,"读属性" 自动转从
state.attr@H_502_12@ 读值,"写属性" 则封装成事件驱动机制,等效于调用
comp.setState({attr:value})@H_502_12@,但它所做的事远不止这个,还包括:
用户可以调用
comp.defineDual(attr,setterFunc)@H_502_12@ 注册自定义的 setter 函数,甚至对同一
duals.attr@H_502_12@ 多次注册不同 settrer 函数,比如基类定义一个 setter 函数,继承类中再定义另一个 setter,两个 setter 会依顺被调用。即
duals.attr@H_502_12@ 的 setter 也具有一种可继承的机制。
duals.attr@H_502_12@ 可被侦听,被侦听后源头
duals.attr@H_502_12@ 若发生变化,相应的侦听函数将自动被调起。
对某节点的
duals.attr@H_502_12@ 赋值,会导致多种联动响应,如果导致本节点其它双源属性更新,更新将在同一周期立即进行,如果导致其它节点的双源属性更新,将在下一周期在其它节点
render()@H_502_12@ 时进行,如果触发侦听事件,也在下一周期调用侦听函数。Shadow Widget 对
duals.attr@H_502_12@ 赋值的设计,已兼顾考虑了本节点内双源属性递归回调的效率,也保证了数据流传递的单向性。
在各节点注册
duals.attr@H_502_12@ 的 setter 函数、侦听函数,能自动适应它的生存周期。比如 B 节点侦听 A 节点的
duals.attr@H_502_12@,无论 A 节点,还是 B 节点先被卸载,侦听链都会自动断开。
sourceComp.listen('attr',targetComp,'attrMethod'); sourceComp.listen('attr',function(value,oldValue){});
第 1 行写法的效果是:sourceComp.duals.attr 发生变化后,自动触发 targetComp['attrMethod'] 的函数调用。第 2 行则触发由参数指定的回调函数。
4. 数据更新的判断依据
Shadow Widget 采用 "恒等比较" 的方式判断两个数值是否更改为,在 comp.duals.attr = value@H_502_12@ 与
comp.setState({attr:value})@H_502_12@ 语句中,当所赋新值(
value@H_502_12@)与旧值恒等(即
===@H_502_12@),则视作数据未更新,也就不会触发相应的 setter 调用或 listen 调用。
Shadow Widget 已为各构件配置 shouldComponentUpdate()@H_502_12@ 与
componentWillReceiveProps()@H_502_12@ 缺省处理,除非有特别理由,您不应改变缺省 "以各属性新旧值是否恒等" 的判断方式。
至于如何对 Array 或 Object 快速构造新数据,以便被系统判断为 "非恒等",我们建议用 React addon 提供的 update@H_502_12@ 接口,Shadow Widget 已缺省内置该函数,即
ex.update()@H_502_12@,请参考 Shadow Widget 的 API 手册。
5. 自动定义的双源属性
双源属性一般要调用 comp.defineDual()@H_502_12@ 注册后才使用,但对于 DOM 节点内置属性是例外,如
title,id,name@H_502_12@ 等,这些属性只要节点在创建时,传入的
props@H_502_12@ 用到了,就会被系统自动注册为双源属性。《Shadow Widget 用户手册》的 "2.2.2 定义双源属性" 有这些属性的完整列表。
另外,命名为 data-*,aria-*,dual-*@H_502_12@ 的属性,也自动注册为双源属性。
自动注册双源属性的设计目的是为了简化编程,如果遇到不想变成双源属性却自动注册了的情况,不使用 duals.xxx@H_502_12@ 即可。
(本文完)
本专栏历史文章: