小程序引擎之--UI樹與局部刷新css
本章內容介紹小程序頁面構造的樹結構及調用this.setData()如何進行局部刷新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>
複製代碼
.item-container {
height: 150;
margin-top:10;
margin-left: 10;
margin-right: 10;
padding:10;
}
複製代碼
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() {
}
});
複製代碼
{
"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
}
}
複製代碼
從下面圖片咱們能夠看到,綠色框標出的就是咱們在html裏面寫的標籤組件,那麼紅色框裏面的是什麼呢?這個稍後咱們介紹如何進行局部刷新會作詳細說明。react
點擊「修改顏色」按鈕觸發onclick函數回調,經過this.setData()修改數據並觸發頁面刷新git
onclick() {
var result = this.data.color1 === "black" ? "green" : "black";
this.setData({
color1: result,
color2: result,
color3: result
});
}
複製代碼
咱們先思考下,怎麼樣作到局部刷新呢?github
從上面flutter中對應的樹結構圖知道,目前咱們用到的組件SingleChildScrollView、Container、Text等等這些組件在 flutter 中都是 StatelessWidget,也就意味着咱們不能直接對其進行刷新。json
第一個想法是否是能夠 把全部的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);
}
}
複製代碼
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));
}
}
複製代碼
這種方式比較簡單粗暴,每次點擊「修改顏色」按鈕,咱們直接生成一顆新的UI數,直接遍歷對比兩棵新舊UI樹,檢查節點每一個屬性是否發生變化,發生變化就對其父節點的children進行替換。less
時間複雜度O(N)、空間複雜度O(N),N爲Component節點數ide
圖解 函數
代碼
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;
}
}
}
複製代碼
單點更新,不從新生成新的Component Tree 跟 Widget Tree,也不進行整棵樹遍歷,具體實現以下
時間複雜度O(1)、空間複雜度O(N),N爲Component節點數
/** * 觀察者,用於觀察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)
});
});
}
}
}
}
複製代碼
[
{
"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"
}
]
複製代碼
上面咱們提到,咱們實現局部刷新的方式是更新child(children)節點,在其上面包裝一層ValueListenableBuilder,那麼如今咱們要單點更新某個屬性,咱們將在整個widget外層包裝一層ValueListenableBuilder,將其屬性跟child(children)封裝到一個監聽變量Data中:
class Data {
Map<String, Property> map;
List<BaseWidget> children;
Data(this.map);
}
複製代碼
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(複製)出來的組件處理,這部分比較複雜,有興趣的同窗去看下源碼
源碼地址:傳送門
系列文章: