vue - 對於 elementUI 中 el-tree 的初次探索


零、資料

  elementUI el-tree 源碼,詳情移步官網和 github。

 

一、引言

  手頭需要開發權限結構,首先想起的就是 el-tree,但是最終的表現的樣式和 el-tree 完全不一樣,因此想着先看一看大佬們是怎樣封裝這種復雜類型的組件的,順便復習下樹結構(偽),於是有了本篇的閱讀筆記和代碼片段。

  實現功能:節點選擇取消(包括全選、半選)、禁用、異步更新。
 

二、片段

(一) js 部分

1. Node 節點對象
import { markNodeData, NODE_KEY, objectAssign } from './utils';

// 作為 自定義子節點的 id
let nodeIdSeed = 0;

// 獲取當前節點中子節點的狀態
export const getChildState = node => {
  let all = true;
  let none = true;
  let allWithoutDisable = true;
  for (let i = 0, j = node.length; i < j; i++) {
    const n = node[i];
    
    if (n.checked !== true || n.indeterminate) {
      all = false;
      if (!n.disabled) allWithoutDisable = false;
    }

    if (n.checked !== false || n.indeterminate) {
      none = false;
    }
  }

  return { all, none, allWithoutDisable, half: !all && !none };
}

// 根據檢索當前節點的狀態並通知父節點
export const reInitChecked = function (node) {
  if (node.childNodes.length === 0) return;

  const {all, none, half} = getChildState(node.childNodes);

  if (all) {
    node.checked = true;
    node.indeterminate = false;
  } else if (half) {
    node.checked = false;
    node.indeterminate = true;
  } else if (none) {
    node.checked = false;
    node.indeterminate = false;
  }

  const parent = node.parent;
  if (!parent || parent.level === 0) return;

  if (!node.store.checkStrictly) {
    reInitChecked(parent);
  }
}

// 根據 store.props 處理傳入的 this.data 與 eltree 中固有 key 的關系
const getPropertyFromData = function(node, prop) {
  // 初始化 store 時傳入的 props 
  const props = node.store.props;
  const data = node.data || {};

  const config = props[prop]; // 用戶在 data 中自定義的 key

  if (typeof config === 'function') {
    return config(data, node);
  } else if (typeof config === 'string') {
    return data[config];
  } else if (typeof config === 'undefined') {
    const dataProp = data[prop];
    return dataProp === undefined ? '' : dataProp;
  }
}

class Node {
  constructor(options) {
    // 注意和 this.data 中的 id 區分開來
    this.id = nodeIdSeed++;
    this.text = null;
    this.checked = false;
    this.indeterminate = false;

    // 這個字段 保存 當前節點的數據(不包含父節點的, 父節點的在 this.parent 字段中)
    this.data = null; // options 也有個, 這個待會會被 options 的覆蓋掉

    this.parent = null;
    this.visible = true; // 估計是為了 root: Node 准備的
    this.isCurrent = false;

    // 把傳入的參數混入到 當前的 Node 對象中去
    for (const option in options) {
      if (options.hasOwnProperty(option)) {
        this[option] = options[option];
      }
    }

    // internal 的一些參數
    this.level = 0;
    this.load = false; // 這個估計是為了懶加載准備的
    this.loading = false; // 這個估計是為了懶加載准備的
    this.childNodes = [];
    
    // 標示節點的等級
    if (this.parent) this.level = this.parent.level + 1;

    const store = this.store;
    if (!store) throw new Error('[Node]store 對象未構建!');

    // 在 store.nodesMap 注冊這個節點, 便於后期查找
    store.registerNode(this);

    // const props = store.props;
    if (store.lazy !== true && this.data) {
      this.setData(this.data);
    }

    if (!Array.isArray(this.data)) {
      markNodeData(this, this.data);
    }

    if (!this.data) return;
  }

  /**
   * @param {*} data 每個相應子節點的 data 數據(用戶傳進來的)
   * @memberof Node
   */
  setData(data) {
    if (!Array.isArray(data)) { // 注意不是數組的時候會走這里!!!
      // 傳遞 this, 主要是取 節點(this) 自定義的 id
      markNodeData(this, data);
    }
    
    this.data = data;
    this.childNodes = [];

    let children;
    if (this.level === 0 && this.data instanceof Array) {
      children = this.data
    } else {
      children = getPropertyFromData(this, 'children') || [];
    }

    // 循環把 this.data 中的 children 數據也變成 Node 節點
    for (let i = 0, l = children.length; i < l; i++) {
      this.insertChild({ data: children[i] });
    }
  }

  /**
   * 把當前節點下的 children 轉換成 Node 節點
   * @param {*} child 
   * @param {*} index 
   * @param {*} batch ques 存疑,源碼中只有一個地方的調用(doCreateChildren)傳入了true
   */
  insertChild(child, index, batch) {
    if (!child) throw new Error('[node]子節點插入失敗,必須要傳入所需的數據!');

    if (!(child instanceof Node)) { // child 不是我們的 節點類型
      if (!batch) { // ques 存疑,源碼中只有一個地方的調用(doCreateChildren)傳入了true
        const children = this.getChildren(true);
        
        if (children.indexOf(child.data) === -1) { // children 數組中找不到 child
          if (typeof index === 'undefined' || index < 0) {
            children.push(child.data);
          } else {
            children.splice(index, 0, child.data);
          }
        }
      }
      // 淺合並對象(足夠)
      objectAssign(child, {
        parent: this,
        store: this.store,
      });
      child = new Node(child);
    }

    child.level = this.level + 1;

    if (typeof index === 'undefined' || index < 0) {
      this.childNodes.push(child);
    } else {
      this.childNodes.splice(index, 0, child);
    }

  }

  /**
   * 獲取 this.data 下面的 children(或開發映射成 children) 字段的 value 
   * 返回值帶扶正處理
   * 這里是從 源數據 取的值,而不是 node 節點對象中 - 與 getPropertyFromData 的區別
   * @param {boolean} [forceInit=false]
   * @returns Array
   * @memberof Node
   */
  getChildren(forceInit = false) { // this is data
    if (this.level === 0) return this.data;

    const data = this.data;
    if (!data) return null;

    const props = this.store.props;
    
    let children = props ? props.children : 'children';
    
    if (data[children] === undefined) data[children] = null;
    
    // 強制初始化 && data[children] 為空
    if (forceInit && !data[children]) data[children] = [];

    return data[children];
  }

  /**
   * 設置 節點的 checked 狀態
   * @param {*} value
   * @param { boolean } deep
   * @param {*} recursion 遞歸
   * @param {*} passValue
   * @memberof Node
   */
  setChecked(value, deep, recursion, passValue) {
    this.indeterminate = value === 'half';
    this.checked = value === true;

    if (this.store.checkStrictly) return;

    // 這個 檢索 子節點 的 checked 狀態
    // if (!(this.shouldLoadData() && !this.store.checkDescendants)) { // 這里 shouldLoadData 與 lazy 相關, 結合本例看源碼,shouldLoadData() 一定返回 false
    if (!(false && !this.store.checkDescendants)) {
      let { all, allWithoutDisable } = getChildState(this.childNodes);


      if (!this.isLeaf && (!all && allWithoutDisable)) {
        this.checked = false;
        value = false;
      }

      const handleDescendants = () => {
        if (deep) {
          const childNodes = this.childNodes;

          for (let i = 0, j = childNodes.length; i < j; i++) {
            const child = childNodes[i];
            passValue = passValue || value !== false;
            const isCheck = child.disabled ? child.checked : passValue;
            child.setChecked(isCheck, deep, true, passValue);
          }
          const { half, all } = getChildState(childNodes);

          if (!all) {
            this.checked = all;
            this.indeterminate = half;
          }
        }
      };

      // if (this.shouldLoadData()) {
      if (false) {
        // Only work on lazy load data. so i don't need to write
      } else {
        handleDescendants();
      }
    }
    

    const parent = this.parent;
    if (!parent || parent.level === 0) return;

    // 這里應該會通知父節點自己的狀態
    if (!recursion) reInitChecked(parent)
  }

  /**
   * 這個函數的作用是返回 初始化 store 時傳入的 key 字段值
   * @readonly
   * @memberof Node
   */
  get key() {
    const nodeKey = this.store.key;
    if (this.data) return this.data[nodeKey];
    return null;
  }

  /**
   * 這個函數的作用是返回 當前節點的 label 字段值
   * @readonly
   * @memberof Node
   */
  get label() {
    return getPropertyFromData(this, 'label');
  }

  /**
   * 這個函數的作用是返回 當前節點的 disabled 狀態
   * @readonly
   * @memberof Node
   */
  get disabled() {
    return getPropertyFromData(this, 'disabled');
  }
}

export default Node;
View Code

 

2. Store 狀態樹對象以及整個樹系統的入口(全局只會產生一個該對象)

import Node from './Node';

class Store {
  constructor(options) {
    this.currentNode = null;
    this.currentNodeKey = null;

    // 把傳入的參數混入到 store 對象中去
    for (let option in options) {
      if (options.hasOwnProperty(option)) {
        this[option] = options[option];
      }
    }

    // 方便查詢所有的子節點
    this.nodesMap = {}

    this.root = new Node({
      data: this.data,
      store: this,
    });

    if (this.lazy && this.load) {
      // 本例中沒有,所以不寫了
    } else {
      this._initDefaultCheckedNodes();
    }
  }


  /**
   * 如其名,在 this.nodesMap 注冊這個節點, 便於后期查找
   * @param { Node } node 
   */
  registerNode(node) {
    // this.key, 初始化 store 對象時傳入的 參數,string
    const key = this.key;
    if (!key || !node || !node.data) return;

    // node.key, 會調用 Node 中的 get key 方法
    const nodeKey = node.key;
    if (nodeKey !== undefined) this.nodesMap[node.key] = node;
  }

  // 初始化默認選中的節點們
  _initDefaultCheckedNodes() {
    const defaultCheckedKeys = this.defaultCheckedKeys || [];
    const nodesMap = this.nodesMap;

    defaultCheckedKeys.forEach(checkedKey => {
      const node = nodesMap[checkedKey];

      if (node) {
        node.setChecked(true, !this.checkStrictly);
      }
    });
  }

  /**
   * 獲取選中的節點的 keys (不包括半選狀態下的)
   * @param {boolean} [leafOnly=false] 跟懶加載有關,本例用不到
   * @returns {Array} 
   * @memberof Store
   */
  getCheckedKeys(leafOnly = false) {
    return this.getCheckedNodes(leafOnly).map(data => (data || {})[this.key]);
  }

  /**
   * 獲取選中的節點
   * @param {boolean} [leafOnly=false] 跟懶加載有關,本例用不到
   * @param {boolean} [includeHalfChecked=false] 需要包含 半選 的節點
   * @returns {Array[Node]}
   * @memberof Store
   */
  getCheckedNodes(leafOnly = false, includeHalfChecked = false) {
    const checkedNodes = [];
    const traverse = function (node) {
      const childNodes = node.root ? node.root.childNodes : node.childNodes;

      childNodes.forEach(child => {
        if ((child.checked || (includeHalfChecked && child.indeterminate)) && (!leafOnly || (leafOnly && child.isLeaf))) {
          checkedNodes.push(child.data);
        }

        traverse(child);
      });
    };

    traverse(this);

    return checkedNodes;
  }

  /**
   * 獲取 半選擇 狀態下的節點的 keys
   * @param {boolean} [leafOnly=false] 跟懶加載有關,本例用不到
   * @returns {Array[]}
   * @memberof Store
   */
  getHalfCheckedKeys(leafOnly = false) {
    return this.getHalfCheckedNodes(leafOnly).map((data) => (data || {})[this.key]);
  }

  /**
   * 獲取 半選擇 狀態下的節點
   * @returns {Array[Node]}
   * @memberof Store
   */
  getHalfCheckedNodes() {
    const nodes = [];
    const traverse = function (node) {
      const childNodes = node.root ? node.root.childNodes : node.childNodes;

      childNodes.forEach(child => {
        if (child.indeterminate) {
          nodes.push(child.data);
        }

        traverse(child);
      });
    };
    traverse(this);
    return nodes;
  }

  /**
   * 設置默認選中的節點
   * @param {Array} newValue
   * @memberof Store
   */
  setDefaultCheckedKey(newValue) {
    if (newValue !== this.defaultCheckedKeys) {
      this.defaultCheckedKeys = newValue;
      this._initDefaultCheckedNodes();
    }
  }

  /**
   * 異步數據的更新
   * @memberof Store
   */
  setData(newVal) {
    const instanceChanged = newVal !== this.root.data;

    if (instanceChanged) {
      this.root.setData(newVal);
      this._initDefaultCheckedNodes();
    }
  }
}

export default Store;
View Code

 

3. utils.js

export const NODE_KEY = '$treeNodeId';

// 給對象新增個屬性 $treeNodeId
export const markNodeData = function(node, data) {
  if (!data || data[NODE_KEY]) return;

  Object.defineProperty(data, NODE_KEY, {
    value: node.id,
    enumerable: false,
    configurable: false,
    writable: false,
  });
}

// merge object
export const objectAssign = function(target) {
  for (let i = 1, j = arguments.length; i < j; i++) {
    let source = arguments[i] || {};
    for (let prop in source) {
      if (source.hasOwnProperty(prop)) {
        let value = source[prop];
        if (value !== undefined) {
          target[prop] = value;
        }
      }
    }
  }

  return target;
};

export const getNodeKey = function(key, data) {
  if (!key) return data[NODE_KEY];
  return data[key];
}
View Code

 

(二) 組件部分

1. 自定 CheckBox.vue

<template>
  <div class="checkbox-container">
    <el-checkbox 
      v-model="node.checked"
      :indeterminate="node.indeterminate"
      :disabled="!!node.disabled"
      @click.native.stop
      @change="handleCheckChange"
    >{{node.label}}</el-checkbox>
  </div>
</template>

<script>
export default {
  name: 'yourCheckBoxName',
  props: {
    node: {
      props: Object,
      default() {
        return {}
      }
    },
  },
  data() {
    return {
      tree: null, // vue component
    }
  },
  created() {
    const parent = this.$parent;

    if (parent.isTreeTable) {
      this.tree = parent;
    } else {
      this.tree = parent.tree;
    }
  },
  methods: {
    handleCheckChange(value, ev) {
      this.node.setChecked(ev.target.checked, !this.tree.checkStrictly);
    },
  }
}
</script>
View Code

 

2. 外殼組件核心內容

<script>
import TableCheckbox from './ckeckbox';
import Store from './utils/store';
import { getNodeKey } from './utils/utils'

export default {
  name: 'TreeTable',
  components: {TableCheckbox},
  props: {
    data: {
      type: Array,
    },
    nodeKey: String,
    props: {
      default() {
        return {
          children: 'children',
          label: 'label',
          disabled: 'disabled'
        };
      }
    },
    showCheckbox: {
      type: Boolean,
      default: true
    },
    defaultCheckedKeys: Array,
  },
  data() {
    return {
      store: null,
      root: null, // store 上的一個屬性, 這個對象就是我們的 Node 樹系統
    }
  },
  watch: {
    defaultCheckedKeys(newValue) {
      this.store.setDefaultCheckedKey(newValue);
    },
    data(newVal) {
      this.store.setData(newVal);
    },
  },
  created() {
    this.isTreeTable = true;

    this.store = new Store({
      key: this.nodeKey,
      data: this.data,
      lazy: false,
      props: this.props,

      checkStrictly: false,
      checkDescendants: false,
      defaultCheckedKeys: this.defaultCheckedKeys,
    });

    this.root = this.store.root;
  },
  methods: {
    getNodeKey(node) {
      return getNodeKey(this.nodeKey, node.data);
    },
    getCheckedKeys(leafOnly) {
      return this.store.getCheckedKeys(leafOnly);
    },
    getHalfCheckedKeys() {
      return this.store.getHalfCheckedKeys();
    },
  },

}
</script>
View Code

 

三、思路和感悟

  體會了數據與視圖分離的思想。

  代碼大致的執行先后順序: 外殼組件 created => 初始化並生成 Store (狀態)樹(唯一) => 初始化並遞歸生成 Node 樹(按照數據結構形成多個 Node 對象) => 自定義的 Checkbox 組件與節點樹一一對應(渲染) => ...

  核心方法是 Node 中的自定義的 setChecked, 半核心方法 Checkbox.vue 中的 handleCheckChange,需要注意的是,由於在 Checkbox 中 el-checkbox 組件與對應的 Node 節點中的checked 的值是存在映射關系的,所以如果我們在 setChecked 方法首行打印該 Node 對象會發現其狀態值已經改變,而我們自定的 setChecked 方法會根據其他條件進行判斷和第二次修正,同理,handleCheckChange 也是對 Node 狀態的第二次修正。
 
  比較精彩的是子節點的狀態經過 setChecked 修正后與父組件的狀態變更,這里並沒有直接調用父節點的 setChecked 方法(否則會形成死循環),而是通過 reInitChecked(parent) 方法,通知父節點,讓父節點循環檢測下其下子節點的狀態(並不需要去檢測孫節點),並直接修改自己的 checked 字段值,接着,由父節點再遞歸往上通知, 從而完成整個狀態值改變邏輯。
 
  目前的片段已基本滿足需求,因此后續的高級功能抽空(並不)再研究。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM