使用Flutter + V8开发小程序引擎(三)

·  阅读 2481

小程序引擎之--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(复制)出来的组件处理,这部分比较复杂,有兴趣的同学去看下源码

  • 源码地址:传送门

  • 系列文章:

《使用Flutter + V8开发小程序引擎(一)》

《使用Flutter + V8开发小程序引擎(二)》

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改