Applet engine -UI tree and local refresh
This chapter describes the tree structure of applet page construction and how to locally refresh by calling this.setdata ()
1 Page Structure
1.1 First, let’s look at a simple page layout and the corresponding code
- The HTML code
<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;">Text 1 Text 1 Text 1 Text 1 Text 1 Text 1 Text 1 Text 1 Text 1 Text 1 Text 1 Text 1</text>
</container>
<container id="item-container" style="color: {{color2}};">
<text style="font-size: 14px; color: white;">Text 2</text>
</container>
<container id="item-container" style="color: {{color3}};">
<text style="font-size: 14px; color: white;">Text 3</text>
</container>
<container id="item-container" style="color: yellow;">
<raisedbutton style="color: green;" bindtap="onclick">
<text style="font-size: 14px; color: white;">Change the color</text>
</raisedbutton>
</container>
</column>
</singlechildscrollview>
</body>
</html>
Copy the code
- CSS code
.item-container {
height: 150;
margin-top:10;
margin-left: 10;
margin-right: 10;
padding:10;
}
Copy the code
- Js code
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() {
}
});
Copy the code
1.2 Converted to 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"},... In addition to omitting part of json],"datasets": {},
"events": {},
"directives": {},
"attribStyle": {},
"attrib": {}}],"datasets": {},
"events": {},
"directives": {},
"attribStyle": {},
"attrib": {}}],"datasets": {},
"events": {},
"directives": {},
"attribStyle": {},
"attrib": {}},"script": "IWZ1bmN0aW9uKGUpe3ZhciByPXt9O2Z1bmN0aW9uIHQobyl7aWYocltvXSlyZXR1cm4gcltvXS5leHBvcnRzO3ZhciBuPXJbb109e2k6byxsOiExLGV4cG9 ydHM6e319O3JldHVybiBlW29dLmNhbGwobi5leHBvcnRzLG4sbi5leHBvcnRzLHQpLG4ubD0hMCxuLmV4cG9ydHN9dC5tPWUsdC5jPXIsdC5kPWZ1bmN0aW9 uKGUscixvKXt0Lm8oZSxyKXx8T2JqZWN0LmRlZmluZVByb3BlcnR5KGUscix7ZW51bWVyYWJsZTohMCxnZXQ6b30pfSx0LnI9ZnVuY3Rpb24oZSl7InVuZGV maW5lZCIhPXR5cGVvZiBTeW1ib2wmJlN5bWJvbC50b1N0cmluZ1RhZyYmT2JqZWN0LmRlZmluZVByb3BlcnR5KGUsU3ltYm9sLnRvU3RyaW5nVGFnLHt2YWx 1ZToiTW9kdWxlIn0pLE9iamVjdC5kZWZpbmVQcm9wZXJ0eShlLCJfX2VzTW9kdWxlIix7dmFsdWU6ITB9KX0sdC50PWZ1bmN0aW9uKGUscil7aWYoMSZyJiY oZT10KGUpKSw4JnIpcmV0dXJuIGU7aWYoNCZyJiYib2JqZWN0Ij09dHlwZW9mIGUmJmUmJmUuX19lc01vZHVsZSlyZXR1cm4gZTt2YXIgbz1PYmplY3QuY3J lYXRlKG51bGwpO2lmKHQucihvKSxPYmplY3QuZGVmaW5lUHJvcGVydHkobywiZGVmYXVsdCIse2VudW1lcmFibGU6ITAsdmFsdWU6ZX0pLDImciYmInN0cml uZyIhPXR5cGVvZiBlKWZvcih2YXIgbiBpbiBlKXQuZChvLG4sZnVuY3Rpb24ocil7cmV0dXJuIGVbcl19LmJpbmQobnVsbCxuKSk7cmV0dXJuIG99LHQubj1 mdW5jdGlvbihlKXt2YXIgcj1lJiZlLl9fZXNNb2R1bGU/ZnVuY3Rpb24oKXtyZXR1cm4gZS5kZWZhdWx0fTpmdW5jdGlvbigpe3JldHVybiBlfTtyZXR1cm4 gdC5kKHIsImEiLHIpLHJ9LHQubz1mdW5jdGlvbihlLHIpe3JldHVybiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwoZSxyKX0sdC5wPSI iLHQodC5zPTApfShbZnVuY3Rpb24oZSxyKXtQYWdlKHtkYXRhOntjb2xvcjE6InJlZCIsY29sb3IyOiJncmVlbiIsY29sb3IzOiJibHVlIn0sb25jbGljayg pe3ZhciBlPSJibGFjayI9PT10aGlzLmRhdGEuY29sb3IxPyJncmVlbiI6ImJsYWNrIjt0aGlzLnNldERhdGEoe2NvbG9yMTplLGNvbG9yMjplLGNvbG9yMzp lfSl9LG9uTG9hZChlKXt9LG9uVW5sb2FkKCl7fX0pfV0pOwovLyMgc291cmNlTWFwcGluZ1VSTD1leGFtcGxlLmJ1bmRsZS5qcy5tYXA="."config": {
"navigationBarTitleText": ""."backgroundColor": "#eeeeee"."enablePullDownRefresh": true}}Copy the code
1.3 Corresponding page tree structure diagram
1.4 Corresponding tree structure in FLUTTER
As you can see from the picture below, the green box is the tag component that we write in HTML. What is the red box? This will be explained in more detail later when we introduce how to perform a local refresh.
2 Page Refresh
- Take a look at the renderings first
- Code parsing
Clicking the “Modify Color” button triggers the onclick function callback that modifies data with this.setData() and triggers a page refresh
onclick() {
var result = this.data.color1 === "black" ? "green" : "black";
this.setData({
color1: result,
color2: result,
color3: result
});
}
Copy the code
3 Partial Refresh
So let’s think about, how do we do local refresh?
-
We can see from the tree structure diagram of a flutter that the current components SingleChildScrollView, Container, Text, etc. are statelesswidgets in a flutter, which means that we cannot refresh them directly.
-
The first idea was that all StatelessWidget components should have a layer that inherits StatefulWidget, so that they could be refreshed. However, after some experimentation, it was found that after the StatefulWidget component is built, The current _state is assigned to null, so it cannot be refreshed externally by saving state, unless each component is assigned a GlobalKey and refreshed by saving state globally, which is not officially recommended because GlobalKey resources are scarce. (PS: code below)
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) {
returnContainer(child: _child); }}Copy the code
- Instead, ValueListenableBuilder provides a way to refresh the StatelessWidget. This is the content highlighted in the red box in the corresponding tree structure diagram of flutter. In the ValueListenableBuilder layer corresponding to the attribute to be modified, you can trigger the refresh of the StatelessWidget by saving the instance and modifying its value.
- Although there is a refresh solution, the same question arises: Do we have a ValueListenableBuilder layer for each component property to listen for changes? Obviously not practical, because each component has too many properties, if each component manually listens, then the code volume will be very large, here I think of a scheme, only listens for child (some components are children) modification, that is, when checking component has property changes, we will find the corresponding parent component, Replace the aligned child (or children) for the refresh effect. (PS: code below)
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)); }}Copy the code
- Now that we have the scheme, what if we refresh it? Keep reading.
3.1 The first way
This method is relatively simple and rough. Every time we click the “Modify color” button, we directly generate a new UI number, directly walk through and compare the two old and new UI trees, check whether each attribute of the node changes, and replace the children of its parent node.
Time complexity O(N), space complexity O(N), where N is the number of Component nodes
-
The illustration
-
code
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); }}Copy the code
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;
}
}
}
Copy the code
3.2 The second way
Single point update does not regenerate new Component Tree and Widget Tree, nor does it traverse the whole Tree
- Add a js expression variable listener, variable changes trigger updates
- All nodes are collected and stored in the map, and their ids are used as keys for storage
- Difficult problems, for (copy) out of the component to handle
Time complexity O(1), space complexity O(N), where N is the number of Component nodes
- Js variable listener
/** * observer, used to observe changes in the properties of the data object * @param data * @constructor */
class Observer {
constructor() {
this.currentWatcher = undefined;
this.collectors = [];
this.watchers = {};
this.assembler = new Assembler();
}
/** * Turn data's attributes into responsible objects, to listen for changes callback * @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;
// Add watcher to data
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) }); }); }}}}Copy the code
- With a listener, we call this.setData() to collect the following changes:
[{"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"}]Copy the code
- So with the component ID and change property content, we can have a single point of update
As mentioned above, we implement the local refresh by updating the Child (Children) node and wrapping a ValueListenableBuilder layer on top of it, so now we need to single-point update a property, We’ll wrap ValueListenableBuilder around the widget, encapsulating its attributes and Child (children) in a listener variable called Data:
- The Data code
class Data {
Map<String, Property> map;
List<BaseWidget> children;
Data(this.map);
}
Copy the code
- The Container Widget code
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); }}Copy the code
Each map property or child (children) changes will trigger a new widget to be built. The Component will not change, and the widget will be reused because of the key. Take a look at the frame rate and elapsed time of the refresh:
-
Difficult problem, for (copy) out of the component processing, this part is more complex, interested students to look at the source code
-
Source code address: portal
-
Series of articles:
Development of a Small Application Engine with Flutter + V8 (PART 1)
Development of a Small Application Engine with Flutter + V8 (PART 2)