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;
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;
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]; }
(二) 组件部分
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>
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>
三、思路和感悟
体会了数据与视图分离的思想。
代码大致的执行先后顺序: 外壳组件 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 字段值,接着,由父节点再递归往上通知, 从而完成整个状态值改变逻辑。
目前的片段已基本满足需求,因此后续的高级功能抽空(并不)再研究。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· 单线程的Redis速度为什么快?
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码