美文网首页
使用Flutter + V8/JsCore开发小程序引擎(三)

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

作者: RY_Ray | 来源:发表于2019-09-29 10:08 被阅读0次

    小程序引擎之--UI树与局部刷新

    本章内容介绍小程序页面构造的树结构及调用this.setData()如何进行局部刷新

    1 页面结构

    1.1 首先,我们来看一个简单的页面布局以及对应的代码

    image
    • 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 对应的页面树结构图

    image

    1.4 在flutter中对应的树结构

    从下面图片我们可以看到,绿色框标出的就是我们在html里面写的标签组件,那么红色框里面的是什么呢?这个稍后我们介绍如何进行局部刷新会做详细说明。

    image

    2 页面刷新

    • 先看下效果图
    image
    • 代码解析

    点击“修改颜色”按钮触发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节点数

    • 图解


      image
    • 代码

    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,不用担心性能消耗。来看下刷新的帧率跟耗时:

    image
    • 难点问题,for(复制)出来的组件处理,这部分比较复杂,有兴趣的同学去看下源码

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

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

    相关文章

      网友评论

          本文标题:使用Flutter + V8/JsCore开发小程序引擎(三)

          本文链接:https://www.haomeiwen.com/subject/dayiuctx.html