• vue


    零、资料

      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 字段值,接着,由父节点再递归往上通知, 从而完成整个状态值改变逻辑。
     
      目前的片段已基本满足需求,因此后续的高级功能抽空(并不)再研究。
  • 相关阅读:
    黑盒测试实践——每日例会记录(一)
    《高级软件测试》—如何计算团队成员贡献分
    TestLink学习——第一周使用小结
    BugkuCTF 你必须让他停下
    BugkuCTF 域名解析
    BugkuCTF web3
    BugkuCTF 矛盾
    BugkuCTF web基础$_POST
    BugkuCTF web基础$_GET
    BugkuCTF 计算器
  • 原文地址:https://www.cnblogs.com/cc-freiheit/p/12958130.html
Copyright © 2020-2023  润新知