yarn & yarn start
- 什么是 gantt 图 ?
甘特图(Gantt chart)又称为横道图、条状图(Bar chart)。其通过条状图来显示项目,进度,和其他时间相关的系统进展的内在关系随着时间进展的情况。以提出者亨利·L·甘特(Henrry L. Ganntt)先生的名字命名(https://baike.baidu.com/item/%E7%94%98%E7%89%B9%E5%9B%BE)
-
目的 绘制一副反应每日任务(24HR)
-
组成 任务名 Y 图表 X 轴 轴
概要图 | 漫游器 -
哪些功能?
- 图表绘制
- 漫游器 控制左右和 缩放比例
- 节点事件 (hover | click)
- 标识节点中特殊事件
- virtualize 渲染, 数据量再大, 性能依然保持
-
如何实现
- Y 轴
展现任务名
- 特殊点:
- 固定位置, 不会随图表的左右滑动而改变自己的位置 和 X 轴属于同一个 SVG ,
- 宽度对结构的影响不会很大, 但高度:
- 文字始终在中间位置 (计算得出),
-
- 在绘制时, 每一层的高度都已经固定了. 这是因为图表的每一行都是有
deltaY
来确定的
- 如何绘制:
传入的 props: {
names: []string,
h: number // 一行的高度
width: number //宽度
}
所以, 绘制的宽度和高度都是在绘制之前就已经确定,若某个 name 太长, 会延伸到 XAxis 中.
const names = ["任务1", "任务2", "任务3"]; const h = 35; const startX = 3,startY = 3; const YAxis = ({ names, h, width }) => { return ( <g id="yaxis"> {names.map((name, i) => { return ( <React.Fragment key={name}> <rect x={startX} y={startY + h * i} width={width} height={h} /> <text x={23} y={25 + h * i}> {name} </text> </React.Fragment> ); })} </g> ); };
- 特殊点:
- X 轴 (时间轴)
发散思维, 提取共同点 (见: 编码实现)
-
时间轴, 将 24 小时展现为 48列 小rect & 任务数列
-
> 画布整体是 某一天的 [00:00 - 24:00], 宽是固定的(700), 可以通过计算得出 `rect`的 x 和 y, 不同的是缩放比例 1. 绘制辅助线 3. 绘制预期时间段 averageValue 4. 绘制实际花费时间段 { startTime, endTime} 5. 绘制特殊事件 // []highlightPoints 6. 绘制等待时间段 // 与前一个任务相距的时间段 7. 绘制name
-
缩放 & 漫游
- 效果: 起始比例是 1 , 展现完整的宽度 [0, 700]
- 漫游到某一点, 例如 [200, 700], 那么
- x 轴的变化:
- 辅助线 [0, 200] transition( { 200 - 当前 x 位置 } , 0)
- 辅助线 [200, 700] transition ( { 200 - 当前 x 位置 } , 0)
- w 的变化:
- 除了name 外, 其他各个 rect 都是需要改变自己的 width
- 这个计算的模式应该是相同的 [ 给定的 startTime, averageValue 等条件 ], 提取到一个 HOC 中
- 原本是 1 的比例, 现在要变成 500 / 700 : k / 1
- x 轴的变化:
- 测试可行性 :
- 使用 symbol 调整 viewport 的方式
- symbol 包裹 一些 svg 图形, 定义 viewBox
[x , y, w , h]
, 设置每个模板的可见部分应该是什么. 预想是改变 width 的值 - 但不可行的一点是, symbol 中图形在 会根据use 中的
width | height
来 缩放 整个图形从而确保内部的图形是置中的,显而易见的是这些改变会影响图形的高度 - 如果要调整比例, 那么 use 和 symbol的值都需要改变, 所以这个方案是不可行的.
- symbol 包裹 一些 svg 图形, 定义 viewBox
- 直接按比例调整 每个元素的 width 的值
在改变比例时, x 的起点位置同样要变化 如何变化: ~~~1. 根据 比例 计算得出 `rect` 将会改变的 deltaWidth = initialWidth / proption - initialWidth 2. 这个值 / proption 就是 两者之间的距离~~~ deltaX = initialX / proption
- 使用 symbol 调整 viewport 的方式
- 漫游到某一点, 例如 [200, 700], 那么
- 效果: 起始比例是 1 , 展现完整的宽度 [0, 700]
-
响应事件
Svg 是dom 元素, 相比较 canvas , 在事件处理上有优势. (https://www.w3.org/TR/SVG11/interact.html)
- hover 事件 , onMouseEnter, onMouseLeave
- click 事件 , onClick
-
- 漫游器
是一个渲染了微小化的 X轴上元素的, 左右 extend 和 drag两个功能 的 Component 同一个 svg 下, 确保位置计算时
- 渲染和 [3] 一样, 但是initial一次, 后期的 state change 不影响
- 对外接受一个 onChange 回调 prop
3 . 测试:
使用 `symbol` , 会根据 width | height 的大小, 缩放整个 `use` 的 svg图形, 使其在 `width & height` 的中心位置. 但是没有办法去限定 height 的时候修改 width 的长度, 这两个总是会取其一来计算位置. ```javascript const HalfHour = props => { return ( <React.Fragment> <defs> <symbol id="render" viewBox="0 0 750 550"> <ChangeRect {...props} /> <ChangeRect {...props} index={2} /> <Dot {...props} /> </symbol> </defs> <use href="#render" x="0" y="00" width="750" height="550" /> </React.Fragment> ); }; ``` 遇到的问题是 当在其他位置 `use` 这个 symbol 的时候, 它的改变是同步的. 而这是我不想要的 找到一个方式可以阻止其更新状态 ```javascript export const ID = "@@Gantt"; export const ID_READONLY = ID + "-ReadOnly"; class Inner extends React.Component { shouldComponentUpdate(...args) { const { readOnly } = this.props; return !readOnly; } render() { const props = this.props; const id = props.readOnly ? ID + "-ReadOnly" : ID; return ( <symbol id={id} viewBox="0 0 750 550"> <ChangeRect {...props} /> <ChangeRect {...props} index={1} /> <Dot {...props} /> </symbol> ); } } ``` ~~~渲染两个 X 轴到 refs 中,在使用的时候通过 `use` 的 `width` 和 `height` 控制视口的大小~~~ 根据 `readOnly` 属性值 在 calc 时 改变 `SVG` 图形的 高度 从而改变整个 svg 的高度
- Y 轴
展现任务名
-
如何使用
<Gantt {...props} />
-
思考可能的需求
在实现之前, 尽可能多的思考会有的需求, 努力去避免后期沦入
添加功能 -> 引起BUG -> 修复BUG
的境地. 但同时要小心不要将额外的功能点考虑进来, 确保组件功能的SOLID
- 展示型组件, 不考虑修改
data
. 而是由事件将数据传出 - 每一个任务在由一个
g
包裹, 可以由用户添加自定义的props
, 比如onClick
,className
- 任务在某个时间点发生了 特殊事件 , 该
Point
可以被点击和hover
.highlightPoints
中可以为其添加多种props
- 如果 等待时间 和 task 都需要一个
hover
组件, 这个组件需要是可替换的 - waiting | usedTime | avarageValue, 颜色 | 内容 可自定义
- 每行的高度 | YAxis 的宽度 | 整个 组件所占 宽度 | XAxis 的高度
- 展示型组件, 不考虑修改
-
接受的 props
- data required
typescript { id: string, // ... name: string // 展示名, 展示在最高层 usedTime: { startTime: number // 微秒 endTime : number // 微秒 }, YAxis: string // 任务名 highlightPoints: { time: number // 微秒 ...specificProps // 可以传递任意的值, 这些都会 patch 到 绘制的 `ellipse` 上面 }[], avarageValue: number | { } // 微秒, 该任务平均花费的时间, hoverComponent: ( type: 组件的类型(Await | USED | AVARAGE) ) => React.ReactComponent // 可以被 React.cloneElement 所覆盖 , default = (props) => <React.Fragment {...props} />, avarageColor?: string, waitingColor?: string usedColor?: string lineHeight?: number = 50, leftWidth?: number = 100, xAxisWidth?: number = 750, chartHeight?: number = 1000 ...restProps // 可以传递任意的值, 这些都会 patch 到 每个单元 `g` 上面 }[]
- date 当天的日期 required
- data required
-
组件设计
- Root 组件, 由 React.createContext 保存传递 props
- 首先绘制 YAxis 任务名 和 XAxis 上面的 辅助线 , 这两个是固定的高度, XAxis 接受额外的两个 prop -> proption = 1 和 startX = 0
- 绘制辅助线, 会根据 proption 和 startX 变化. 需要绘制 48 列 data[].length 行 个 辅助线
-
data.map((d, i) => { // 1. 拿到 usedTime 需要将 startTime 和 endTime 转化为 x 坐标 和 width 因此需要计算 1. x 轴 原点是 这一天的 起点时间 2. deltaTime = startTime - initialTime 3. 总长度是固定的 xAxisWidth, 所以 x / xAxisWidth = deltaTime / fullDayTime 这里的 x = initialX 4. 同样的方式 拿到 initialWidth 5. AWait 组件的终点是 initialX, 宽度是 此任务和上一个任务的 时间间隔 6. })
- 绘制底部刻度
见
<Graduation />
- slide 漫游器
- 遇到的问题:
- drag 状态的保持. 是只在 drag 时, 浏览器才会 有 列如 鼠标保持 drag 图标
- draging 中, 如何得到 坐标数值从而改变 state
- 在计算位置的时候, 通过哪些方法去计算 改变
slide
的属性 关系到整个控件在使用时得到的是否合理- 比如 计算
leftX
width
时, 这些属性在dragging
时 只根据接受到的offset
去改变leftX
和width
这些属性得到的值, 经过计算是会有一些偏差的. - 避免偏差的出现, 需要
总量
参与进来.xAisWidth
是总量, 而同时leftX
和rightX
在 stretch 的时候 只会有一个被改变, 那么currentWidth
就是总量 - 某一方的改变值 - 另一方的值
- 比如 计算
- 遇到的问题:
-
管理状态使用的是
React.createContext
, 在 Component 中 如果重新render
, 此时 ~~~不论传给Provider
的value
值是什么, 是否是同一个值~~~ 只要 传递给Provider
的value
值不同, 这里要注意的是{...}
es6 中的解构操作, 他会生成一个新的对象, 它的Comsumer
都会重新计算它的children()
-
dragging handler 花费时间过多, 即使是在
build
的环境中 依然会很卡- 代码中, 目前的操作会导致的更新, 主要是
XAxis
中的 每一个svg 图形的style
, 也就是transform
和width
. - 目的是减少 rerender 的操作, 可通过
throttle
或者 更新 style element 等方式
- 代码中, 目前的操作会导致的更新, 主要是
-
修改为 mousemove handle
-
造成这种现象的主要的问题是过度渲染. 在现在的代码中, 渲染的复杂度是 根据
HelpRects
的O(n*column^data.length)
. 但实际上, 达到同样的目的 只需要 渲染column + data.length
条线段即可达到目的 -
在 React 中 渲染同样的数组, 更细粒度的划分组件, 得到的结果性能更优 在
HelpRects
组件中, 如果组件更新时,在render
方法中 都重新生成一组 子组件 和 只更新子组件 相比, 帧数平均要高1~2
帧
这是 优化前的代码:return ( <GanttStateContext.Consumer> {({ proption, transform }) => { const rows = data.length; const originalWidth = xAxisWidth / columns; let rects = []; for (let r = 0; r < rows; r++) { rects.push( <line key={'row - ' + r} {...lineProps} x1="0" x2={xAxisWidth / proption} y1={r * h} y2={r * h} /> ); } for (let c = 0; c < columns; c++) { const x = originalWidth * c / proption; rects.push( <line key={'column - ' + c} {...lineProps} x1={x} x2={x} y1={0} y2={chartHeight} /> ); } return <g transform={transform} className="help-rects"> {rects}</g>; }}
帧数:
这是优化后:
... render() { ... let rects = []; const rows = data.length; const initialWidth = xAxisWidth / columns; for (let r = 0; r < rows; r++) { rects.push( <RowLine key={'row - ' + r} xAxisWidth={xAxisWidth} y={r * h} /> ); } for (let c = 0; c < columns; c++) { rects.push( <ColumnLine key={'column - ' + c} initialWidth={initialWidth} h={chartHeight} i={c} /> ); } return ( <GanttStateContext.Consumer> {({ proption, transform }) => { return <g transform={transform} className="help-rects"> { rects }</g>; }} </GanttStateContext.Consumer> ...
-
React.forwardRef
This is typically not necessary for most components in the application. However, it can be useful for some kinds of components, especially in reusable component libraries
especially in resuable component
, 根据官网上面的例子, 使用这种方式去定义了我的HOC.js
中的组件, 但是没想到这却会引发一个潜在的性能问题(Really?). 更准确的说应该是一个 React 的 bug (Whatttttttt?). 在 React@16.3 中, 官方推出了React.createContext
这个 api , 它的使用方式是这样的:const M = React.createContext({}) ... class App { render(){ return <M.Provider value={this.state}> <Inner> </M.Provider> } } ... <!-- 在内部组件中使用 --> const SomeComponent = () => <M.Consumer> { ({}) => <div>{this.props.children}</div> } </M.Consumer>
借助它, 我们可以不必通过
props
来传递Root
组件的state
.同样在 React@16.3 中推出的还有
React.forwardRef
, 用它包装后的Component
, 传递的ref
不会被直接使用, 而是可以通过回调交给被包装的Component
.<!-- 官网例子: --> function logProps(Component) { class LogProps extends React.Component { componentDidUpdate(prevProps) { console.log('old props:', prevProps); console.log('new props:', this.props); } render() { const {forwardedRef, ...rest} = this.props; return <Component ref={forwardedRef} {...rest} />; } } return React.forwardRef((props, ref) => { return <LogProps {...props} forwardedRef={ref} />; }); }
这两者都是为了解决某个问题而推出的, 从而使
React
变得越来越好, ~~~但这两个结合的时候~~~, 我发现了一个Bug
这里是示例的地址
https://codesandbox.io/s/04393o3k6w
const OtherLogProps = logProps(OtherComponent) class Inner extends React.PureComponent{ render() { <!-- Forbidden 只初始化一次, 会阻止之后的props的更新 --> return <Forbidden> <OtherLogProps> <ManyLevel> <SomeComponent /> </ManyLevel> </OtherLogProps> </Forbidden> } }
此时 只要
Root
组件更新, 同时OtherLogProps
下的某个组件(不论这个组件的层级有多深)使用了进行了setState
, ~~~M.Consumer
来接受状态的话~~~,OtherLogProps
就会接受到新的props
, 从而进行一轮rerender
. 不论这个OtherLogProps
所在的层级是多么深,LogProps
这个节点总是会接受到React.forwardRef
中的回调重新触发所传递的 props.研究它是如何做到的 how-it-occurs
这两者的结合确实的导致了
rerender
的问题, 那么现在如何解决呢? 很简单: 1. 将LogProps
改为继承自React.PureComponent
2. 或者 添加shouldComponentUpdate
到 'LogProps` 中但我相信这并不是
React
设计这两者的初衷, 因为不管如何改变LogProps
,React.forwardRef
中的这个回调总是会被调用(这也是会影响一些性能), 而造成的结果就是隐式触发特定组件的更新. 在我看来,这是违反声明式
这一React
设计准则的, 使得React
混淆了之前清晰的更新策略. 当初context
在官网中特意声明的是这种从父组件"隐式传递" 状态的方式是不对的, 而现在这个是 **父组件更新, 导致某个使用了特定api子组件更新 **, 所以我认为这是一个 bug;issues 已提 (facebook/react#12688)
-
如果使用了
getDerivedStateFromProps
那么 以UNSAFE_
作为开头的lifecycle
方法都不会被调用 -
OnlyRenderOnce
使用shouldComponentUpdate
来避免 状态更新, 但如果在一个很长的列表中, 每一个都去判断, 这也是会造成一些性能上的损失. 可以寻找其他方式替代, 比如改变 他们的parent
的更新策略 -
startX
和proption
的改变, 组件更新耗费了太多的计算资源, 但是这两个的改变只是针对 某个特定Element
的属性, 不需要重新计算非该 属性的值 所以: 优化渲染计算,抽取共通属性, 将更新触发到更细分的组件, 优化前: 优化后:**Recaculate style** , 这一部分计算太多, 导致现在的帧数平均还是在 20-30 之间, 但是整体的 js 计算 已经减少了很多, 下一步优化 `Recaculate style` 这部分.
考虑 2 种方式去解决: 1. 使用共享样式, 目前大部分的更新都是在计算 元素的样式, 可将每一组元素 通过 css的
[data]
选择器进行分类,提取到style
element 中, 从而替换 rerender 来计算style
的工作 2. 通过react-virualize
的更新模式, 每次的渲染只渲染 用户可见的区域, 从而大幅减少rerender
-
减少
Recaculate style
- 分析:
- 目前所有的
SVG
图案的渲染都和proption
有关,transform
只影响最外层的g
节点, 并不会对性能有什么影响. proption
影响到的组件有自己的 style 改变方式, 所以, 把这一部分抽取出来- 每一次
proption
的改变, 不再触发 各个Element
组件的更新, 而是直接改变style Element
属性
- 目前所有的
- 策略:
- 每个
Element
渲染时, 标记它, 并联合它的更新策略放入 一个Map
中, 这个策略会返回它需要的style
- 下一次
state
改变时, 传入proption
, 得到新的css
, 并挂载到style Element
中
- 每个
- 实际:
- 分析:
-
减少
update
方法被调用次数 每一次的moving
都会触发很多次的updateStyleMap
函数. 但需要这么多次吗? 其实我们只需要保证用户的视觉没有感到拖帧
就好 -
当
data
很长时, 即使使用了共享样式, 在每一次style element
重新 patch 时,browser
都需要去计算每个layout
的样式位置, 这部分工作量会随着data
的大小而变化. 所以 需要使用第二种virtualize
来进行控制. 不渲染不需要的组件, 只将用户视口
内的组件进行渲染,视口
可以由已经定义的chartHeight
得到,lineheight
则负责计算 有多少rows
的data
需要 渲染 TODO: 1. Graduation 的上下滑动需要同步 ChartX 2. 计算得出的 要渲染的data
, 需要将 该 data 的样式计算 添加到styleMap
中, 并根据当前的proption
和startX
初始化. 3. 移除styleMap
中 不在视口
中的 但之前渲染过的 样式 - 将stateLess Component
替换为PureComponent
, 在componentWillUnmount
中进行删除 思路: 1. 学习 基于react-virtualize
的vue-virtualize
. 2. 学习react-tiny-virtualize-list
实现:
- Slider
- 单一原则:
- 抽出 Dragger 组件, 处理
drag
事件, 处理回调 - 抽出 Dragging 组件, 处理 Dragger 组件中的回调出来的数值并处理出
startPercent
和percent
- 抽出 Dragger 组件, 处理
- XAxis
- 抽取组件
-
Graduation
-
基于
VirtualizeList
实现列表的绘制, 需要将之前 的组件重构.
- Nodes:
现在不再在
Nodes
中去遍历计算每一个组件的的位置. 在renderItem
中可以拿到style
属性. 通过它就可以知道当前的top
的位置. - HelpRects
绘制时的
RowLine
和ColumnLine
的情况不同ColumnLine
是固定的, 只对proption
反馈 并且渲染时, 不再 从 0 到totalHeight
这样渲染这么长, 这会导致**composite
**花费超多的资源.RowLine
不再根据data.length
来定条数, 而是移到renderItem
中去绘制, 它的宽度也只对proption
反应
- Graduation
在
renderWrapper
中额外渲染一个<defs/>
, 拿到的items
通过React.children
和React.cloneElement
来传入readOnly
prop, 渲染不同高度的Task
- Tasks
在组件 的
key
值是一直在变化的, 所以每一次 Task 都是create
一个新的, 在componentDidMount
方法中去更新元素的style
,chidlren component
在unmount
中移除出styleMap