小程序引擎之--UI树与局部刷新
本章内容介绍小程序页面构造的树结构及调用this.setData()如何进行局部刷新
1 页面结构
1.1 首先,我们来看一个简单的页面布局以及对应的代码
- html代码
<!DOCTYPE html>
<html lang="en" html-identify="CC">
<head>
<meta charset="UTF-8" />
<style type="text/css" media="screen">
@import "example.css";
</style>
</head>
<body>
<singlechildscrollview>
<column>
<container id="item-container" style="color: {{color1}};">
<text style="font-size: 14px; color: white;">文本1文本1文本1文本1文本1文本1文本1文本1文本1文本1文本1</text>
</container>
<container id="item-container" style="color: {{color2}};">
<text style="font-size: 14px; color: white;">文本2</text>
</container>
<container id="item-container" style="color: {{color3}};">
<text style="font-size: 14px; color: white;">文本3</text>
</container>
<container id="item-container" style="color: yellow;">
<raisedbutton style="color: green;" bindtap="onclick">
<text style="font-size: 14px;color: white;">修改颜色</text>
</raisedbutton>
</container>
</column>
</singlechildscrollview>
</body>
</html>
- css代码
.item-container {
height: 150;
margin-top:10;
margin-left: 10;
margin-right: 10;
padding:10;
}
- js代码
Page({
data: {
color1: "red",
color2: "green",
color3: "blue",
},
onclick() {
var result = this.data.color1 === "black" ? "green" : "black";
this.setData({
color1: result,
color2: result,
color3: result
});
},
onLoad(e) {
},
onUnload() {
}
});
1.2 转换成的json
{
"style": {
".item-container": {
"height": "150",
"margin-top": "10",
"margin-left": "10",
"margin-right": "10",
"padding": "10"
}
},
"body": {
"tag": "body",
"innerHTML": "",
"childNodes": [
{
"tag": "singlechildscrollview",
"innerHTML": "",
"childNodes": [
{
"tag": "column",
"innerHTML": "",
"childNodes": [
{
"tag": "container",
"innerHTML": "",
"childNodes": [
{
"tag": "text",
"innerHTML": "5paH5pysMeaWh+acrDHmlofmnKwx5paH5pysMeaWh+acrDHmlofmnKwx5paH5pysMeaWh+acrDHmlofmnKwx5paH5pysMeaWh+acrDE=",
"childNodes": [],
"datasets": {},
"events": {},
"directives": {},
"attribStyle": {
"font-size": "14px",
"color": "white"
},
"attrib": {}
}
],
"datasets": {},
"events": {},
"directives": {},
"attribStyle": {
"color": "{{color1}}"
},
"attrib": {},
"id": "item-container"
},
... 此除省略部分json
],
"datasets": {},
"events": {},
"directives": {},
"attribStyle": {},
"attrib": {}
}
],
"datasets": {},
"events": {},
"directives": {},
"attribStyle": {},
"attrib": {}
}
],
"datasets": {},
"events": {},
"directives": {},
"attribStyle": {},
"attrib": {}
},
"script": "IWZ1bmN0aW9uKGUpe3ZhciByPXt9O2Z1bmN0aW9uIHQobyl7aWYocltvXSlyZXR1cm4gcltvXS5leHBvcnRzO3ZhciBuPXJbb109e2k6byxsOiExLGV4cG9ydHM6e319O3JldHVybiBlW29dLmNhbGwobi5leHBvcnRzLG4sbi5leHBvcnRzLHQpLG4ubD0hMCxuLmV4cG9ydHN9dC5tPWUsdC5jPXIsdC5kPWZ1bmN0aW9uKGUscixvKXt0Lm8oZSxyKXx8T2JqZWN0LmRlZmluZVByb3BlcnR5KGUscix7ZW51bWVyYWJsZTohMCxnZXQ6b30pfSx0LnI9ZnVuY3Rpb24oZSl7InVuZGVmaW5lZCIhPXR5cGVvZiBTeW1ib2wmJlN5bWJvbC50b1N0cmluZ1RhZyYmT2JqZWN0LmRlZmluZVByb3BlcnR5KGUsU3ltYm9sLnRvU3RyaW5nVGFnLHt2YWx1ZToiTW9kdWxlIn0pLE9iamVjdC5kZWZpbmVQcm9wZXJ0eShlLCJfX2VzTW9kdWxlIix7dmFsdWU6ITB9KX0sdC50PWZ1bmN0aW9uKGUscil7aWYoMSZyJiYoZT10KGUpKSw4JnIpcmV0dXJuIGU7aWYoNCZyJiYib2JqZWN0Ij09dHlwZW9mIGUmJmUmJmUuX19lc01vZHVsZSlyZXR1cm4gZTt2YXIgbz1PYmplY3QuY3JlYXRlKG51bGwpO2lmKHQucihvKSxPYmplY3QuZGVmaW5lUHJvcGVydHkobywiZGVmYXVsdCIse2VudW1lcmFibGU6ITAsdmFsdWU6ZX0pLDImciYmInN0cmluZyIhPXR5cGVvZiBlKWZvcih2YXIgbiBpbiBlKXQuZChvLG4sZnVuY3Rpb24ocil7cmV0dXJuIGVbcl19LmJpbmQobnVsbCxuKSk7cmV0dXJuIG99LHQubj1mdW5jdGlvbihlKXt2YXIgcj1lJiZlLl9fZXNNb2R1bGU/ZnVuY3Rpb24oKXtyZXR1cm4gZS5kZWZhdWx0fTpmdW5jdGlvbigpe3JldHVybiBlfTtyZXR1cm4gdC5kKHIsImEiLHIpLHJ9LHQubz1mdW5jdGlvbihlLHIpe3JldHVybiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwoZSxyKX0sdC5wPSIiLHQodC5zPTApfShbZnVuY3Rpb24oZSxyKXtQYWdlKHtkYXRhOntjb2xvcjE6InJlZCIsY29sb3IyOiJncmVlbiIsY29sb3IzOiJibHVlIn0sb25jbGljaygpe3ZhciBlPSJibGFjayI9PT10aGlzLmRhdGEuY29sb3IxPyJncmVlbiI6ImJsYWNrIjt0aGlzLnNldERhdGEoe2NvbG9yMTplLGNvbG9yMjplLGNvbG9yMzplfSl9LG9uTG9hZChlKXt9LG9uVW5sb2FkKCl7fX0pfV0pOwovLyMgc291cmNlTWFwcGluZ1VSTD1leGFtcGxlLmJ1bmRsZS5qcy5tYXA=",
"config": {
"navigationBarTitleText": "",
"backgroundColor": "#eeeeee",
"enablePullDownRefresh": true
}
}
1.3 对应的页面树结构图
1.4 在flutter中对应的树结构
从下面图片我们可以看到,绿色框标出的就是我们在html里面写的标签组件,那么红色框里面的是什么呢?这个稍后我们介绍如何进行局部刷新会做详细说明。
2 页面刷新
- 先看下效果图
- 代码解析
点击“修改颜色”按钮触发onclick函数回调,通过this.setData()修改数据并触发页面刷新
onclick() {
var result = this.data.color1 === "black" ? "green" : "black";
this.setData({
color1: result,
color2: result,
color3: result
});
}
3 局部刷新
我们先思考下,怎么样做到局部刷新呢?
从上面flutter中对应的树结构图知道,目前我们用到的组件SingleChildScrollView、Container、Text等等这些组件在 flutter 中都是 StatelessWidget,也就意味着我们不能直接对其进行刷新。
第一个想法是不是可以
把所有的StatelessWidget组件都套一层,都继承StatefulWidget,那么就可以进行刷新,但是经过一番试验过后发现, StatefulWidget的组件在build之后,当前的_state会被赋值为null,所以不能通过外部保存state来进行刷新,除非每一个组件都赋值一个GlobalKey,通过全局保存state实例来进行刷新,但是这种方式官方不推荐,GlobalKey资源稀缺,所以这种方式行不通。 (ps : 代码如下)
class ContainerStateful extends StatefulWidget {
ContainerStateful(this._child) {}
@override
State<StatefulWidget> createState() {
return _ContainerState();
}
}
class _ContainerState extends State<ContainerStateful> {
_ContainerState(Widget child) {
}
@override
Widget build(BuildContext context) {
return Container(child: _child);
}
}
- 换一种方式,官方提供了一种刷新StatelessWidget方式,通过ValueListenableBuilder来做刷新,这个就是我们上面flutter中对应的树结构图里面红框标出的内容。在对应需要修改的属性套一层ValueListenableBuilder,通过保存其实例,对其value进行修改赋值,就可以触发对StatelessWidget进行刷新。
- 虽然有了刷新方案,但是同样问题来了,我们是否对每个组件的属性都套一层ValueListenableBuilder来做监听修改呢?显然不太实际,因为每个组件的属性太多了,如果每个都手动做监听,那么代码量将非常大,这里我想了一个方案,只对child(一些组件是children)进行监听修改,也就是说当检查组件有属性变化,我们是找到对应的父组件,对齐child(或者children)进行替换来达到刷新效果。(ps : 代码如下)
class ContainerStateless extends BaseWidget {
ValueNotifier<List<BaseWidget>> children;
ContainerStateless(BaseWidget parent, ...) {
this.parent = parent;
this.children = children;
...
}
@override
Widget build(BuildContext context) {
...
return Container(
...
child: ValueListenableBuilder(
builder:
(BuildContext context, List<BaseWidget> value, Widget child) {
return value.length > 0 ? value[0] : null;
},
valueListenable: children));
}
}
- 既然方案有了,我们如果刷新呢?请继续往下看。
3.1 第一种方式
这种方式比较简单粗暴,每次点击“修改颜色”按钮,我们直接生成一颗新的UI数,直接遍历对比两棵新旧UI树,检查节点每个属性是否发生变化,发生变化就对其父节点的children进行替换。
时间复杂度O(N)、空间复杂度O(N),N为Component节点数
-
图解
代码
void compareTreeAndUpdate(BaseWidget oldOne, BaseWidget newOne) {
var same = true;
if (oldOne.component.tag != newOne.component.tag) {
if (null != oldOne.parent) {
same = false;
} else {
same = false;
}
} else {
oldOne.component.properties.forEach((k, v) {
if (!newOne.component.properties.containsKey(k)) {
same = false;
} else if (newOne.component.properties[k].getValue() != v.getValue()) {
same = false;
}
});
if (oldOne.children.value.length != newOne.children.value.length) {
same = false;
}
if (oldOne.component.innerHTML.getValue() != newOne.component.innerHTML.getValue()) {
same = false;
}
}
if (same) {
for (var i = 0; i < oldOne.children.value.length; i++) {
compareTreeAndUpdate(oldOne.children.value[i], newOne.children.value[i]);
}
} else {
oldOne.updateChildrenOfParent(newOne.parent.children);
}
}
abstract class BaseWidget extends StatelessWidget {
String pageId;
Component component;
MethodChannel methodChannel;
BaseWidget parent;
ValueNotifier<List<BaseWidget>> children;
void setChildren(ValueNotifier<List<BaseWidget>> children) {
this.children = children;
}
void updateChildrenOfParent(ValueNotifier<List<BaseWidget>> newChildren) {
if (null != parent && parent.children.value != newChildren.value) {
newChildren.value.forEach((it) {
it.parent = parent;
});
parent.children.value = newChildren.value;
}
}
}
3.2 第二种方式
单点更新,不重新生成新的Component Tree 跟 Widget Tree,也不进行整棵树遍历,具体实现如下
- 增加一个js表达式变量监听,变量改动触发更新
- 收集所有节点存入map中,通过id作为key进行存储
- 难点问题,for(复制)出来的组件处理
时间复杂度O(1)、空间复杂度O(N),N为Component节点数
- js变量监听
/**
* 观察者,用于观察data对象属性变化
* @param data
* @constructor
*/
class Observer {
constructor() {
this.currentWatcher = undefined;
this.collectors = [];
this.watchers = {};
this.assembler = new Assembler();
}
/**
* 将data的属性变成可响应对象,为了监听变化回调
* @param data
*/
observe(data) {
if (!data || data === undefined || typeof (data) !== "object") {
return;
}
for (const key in data) {
let value = data[key];
if (value === undefined) {
continue;
}
this.defineReactive(data, key, value);
}
}
defineReactive(data, key, val) {
const property = Object.getOwnPropertyDescriptor(data, key);
if (property && property.configurable === false) {
return
}
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = data[key];
}
let that = this;
let collector = new WatcherCollector(that);
this.collectors.push(collector);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(data) : val;
// 在这里将data的数据与对应的watcher进行关联
if (that.currentWatcher) {
collector.addWatcher(that.currentWatcher);
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(data) : val;
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
if (setter) {
setter.call(data, newVal);
} else {
val = newVal;
}
collector.notify(data);
}
});
}
addWatcher(watcher) {
if (this.watchers[watcher.id] === undefined) {
this.watchers[watcher.id] = [];
}
this.watchers[watcher.id].push(watcher);
}
removeWatcher(ids) {
if (ids) {
let keys = [];
ids.forEach((id) => {
if (this.watchers[id]) {
this.watchers[id].forEach((watcher) => {
keys.push(watcher.key());
});
this.watchers[id] = undefined;
}
});
if (this.collectors) {
this.collectors.forEach((collector) => {
keys.forEach((key) => {
collector.removeWatcher(key)
});
});
}
}
}
}
- 有了监听后,我们调用this.setData()收集到的变动如下:
[
{
"id":"container-397771684",
"type":"property",
"key":"color",
"value":"black"
},
{
"id":"container-328264404",
"type":"property",
"key":"color",
"value":"black"
},
{
"id":"container-416353772",
"type":"property",
"key":"color",
"value":"black"
}
]
- 那么有了组件id跟变更属性内容,我们就可以单点更新了
上面我们提到,我们实现局部刷新的方式是更新child(children)节点,在其上面包装一层ValueListenableBuilder,那么现在我们要单点更新某个属性,我们将在整个widget外层包装一层ValueListenableBuilder,将其属性跟child(children)封装到一个监听变量Data中:
- Data代码
class Data {
Map<String, Property> map;
List<BaseWidget> children;
Data(this.map);
}
- Container Widget代码
class ContainerStateless extends BaseWidget {
ContainerStateless(
BaseWidget parent,
String pageId,
MethodChannel methodChannel,
Component component) {
this.parent = parent;
this.pageId = pageId;
this.methodChannel = methodChannel;
this.component = component;
this.data = ValueNotifier(Data(component.properties));
}
@override
Widget build(BuildContext context) {
return ValueListenableBuilder(
builder: (BuildContext context, Data data, Widget child) {
var alignment = MAlignment.parse(data.map['alignment'],
defaultValue: Alignment.topLeft);
return Container(
key: ObjectKey(component),
alignment: alignment,
color: MColor.parse(data.map['color']),
width: MDouble.parse(data.map['width']),
height: MDouble.parse(data.map['height']),
margin: MMargin.parse(data.map),
padding: MPadding.parse(data.map),
child: data.children.isNotEmpty ? data.children[0] : null);
},
valueListenable: this.data);
}
}
每个map里面的属性或者child(children)发生变化都会触发重新build一个widget,component是不变的,由于key的关系,所以会复用之前的widget,不用担心性能消耗。来看下刷新的帧率跟耗时:
- 难点问题,for(复制)出来的组件处理,这部分比较复杂,有兴趣的同学去看下源码
- 源码地址:传送门
- 系列文章: