使用Flutter + V8開發小程序引擎(三)

小程序引擎之--UI樹與局部刷新css

本章內容介紹小程序頁面構造的樹結構及調用this.setData()如何進行局部刷新html

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裏面寫的標籤組件,那麼紅色框裏面的是什麼呢?這個稍後咱們介紹如何進行局部刷新會作詳細說明。react

2 頁面刷新

  • 先看下效果圖

  • 代碼解析

點擊「修改顏色」按鈕觸發onclick函數回調,經過this.setData()修改數據並觸發頁面刷新git

onclick() {
    var result = this.data.color1 === "black" ? "green" : "black";
    this.setData({
        color1: result,
        color2: result,
        color3: result
    });
}
複製代碼

3 局部刷新

咱們先思考下,怎麼樣作到局部刷新呢?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);
  }
}
複製代碼
  • 換一種方式,官方提供了一種刷新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進行替換。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;
    }
  }
}
複製代碼

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開發小程序引擎(二)》

相關文章
相關標籤/搜索