阅读视图

发现新文章,点击刷新页面。

js上下文

概述

js程序执行的时候,必然涉及到执行上下文那么什么叫“执行上下文”呢?

本文介绍如下

  • 执行上下文的类型
  • 执行上下文特点
  • 执行栈
  • 执行上下文的生命周期

概念

举个例子,生活中,相同的话在不同的场合说可能会有不同的意思,而这个说话的场合就是我们说话的语境。

同样对应在编程中, 对程序语言进行“解读”的时候,也必须在特定的语境中,这个语境就是javascript中的执行上下文。

一句话概括:

执行上下文就是javascript代码被解析和执行时所在环境的抽象概念。

执行上下文的类型

在js中,执行上下文分为以下三种:

  • 全局执行上下文:只有一个,也就是浏览器对象(即window对象),this指向的就是这个全局对象。
  • 函数执行上下文:有无数个,只有在函数被调用时才会被创建,每次调用函数都会创建一个新的执行上下文。
  • Eval函数执行上下文:js的eval函数执行其内部的代码会创建属于自己的执行上下文, 很少用而且不建议使用。

执行上下文的特点

  1. 单线程,只在主线程上运行;
  2. 同步执行,从上向下按顺序执行;
  3. 全局上下文只有一个,也就是window对象;
  4. 函数执行上下文没有限制;
  5. 函数每调用一次就会产生一个新的执行上下文环境。

JS如何管理多个执行上下文

通过上面介绍,我们知道了js代码在运行时可能会产生无数个执行上下文,那么它是如何管理这些执行上下文的呢?

同时由于js是单线程的,所以不能同时干两件事,必须一个个去执行,那么这么多的执行上下文是按什么顺序执行的呢?

执行栈

接下来就对上面的问题做出解答,管理多个执行上下文靠的就是执行栈,也被叫做调用栈

特点:后进先出(LIFO)的结构。

作用:存储在代码执行期间的所有执行上下文。

(LIFO: last-in, first-out,类似于向乒乓球桶中放球,最先放入的球最后取出)

js在首次执行的时候,会创建一个全局执行上下文并推入栈中。

每当有函数被调用时,引擎都会为该函数创建一个新的函数执行上下文然后推入栈中。

当栈顶的函数执行完毕之后,该函数对应的执行上下文就会从执行栈中pop出,然后上下文控制权移到下一个执行上下文。

例子:

var a = 1; // 1. 全局上下文环境
function bar (x) {
    console.log('bar')
    var b = 2;
    fn(x + b); // 3. fn上下文环境
}
function fn (c) {
    console.log(c);
}
bar(3); // 2. bar上下文环境

如下图:

image.png

执行上下文的生命周期

执行上下文的生命周期也非常容易理解, 分为三个阶段:

  1. 创建阶段
  2. 执行阶段
  3. 销毁阶段

创建阶段

创建阶段, 主要有是有这么几件事:

  1. 确定this的值, 也就是绑定this (This Binding);
  2. 词法环境(LexicalEnvironment) 组件被创建;
  3. 变量环境(VariableEnvironment) 组件被创建.

伪代码

ExecutionContext = {  
  ThisBinding = <this value>,     // 确定this 
  LexicalEnvironment = { ... },   // 词法环境
  VariableEnvironment = { ... },  // 变量环境
}

This Binding

通过上面的介绍我们知道实际开发主要用到两种执行上下文为全局函数, 那么绑定this在这两种上下文中也不同.

  • 全局执行上下文中, this指的就是全局对象, 浏览器环境指向window对象, nodejs中指向这个文件的module对象.
  • 函数执行上下文较为复杂, this的值取决于函数的调用方式. 具体有: 默认绑定、隐式绑定、显式绑定、new绑定、箭头函数.

词法环境

如上图, 词法环境是由两个部分组成的:

  1. 环境记录: 存储变量和函数声明的实际位置;
  2. 对外部环境的引用: 用于访问其外部词法环境.

同样的, 词法环境也主要有两种类型:

  1. 全局环境: 拥有一个全局对象(window对象)及其关联的所有属性和方法(比如数组的方法splice、concat等), 同时也包含了用户自定义的全局变量. 但是全局环境中没有外部环境的引用, 也就是外部环境引用为null.
  2. 函数环境: 用户在函数中自定义的变量和函数存储在环境记录中, 包含了arguments对象. 而对外部环境的引用可以是全局环境, 也可以是另一个函数环境(比如一个函数中包含了另一个函数).

变量环境

变量环境其实也是一个词法环境, 因此它具有上面定义的词法环境的所有属性.

在 ES6 中,词法 环境和 变量 环境的区别在于前者用于存储函数声明和变量( let 和 const )绑定,而后者仅用于存储变量( var ) 绑定。

变量提升

在创建阶段,函数声明存储在环境中,而变量会被设置为 undefined(在 var 的情况下)或保持未初始化(在 let 和 const 的情况下)。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。

执行阶段

执行阶段主要做三件事情:

  1. 变量赋值
  2. 函数引用
  3. 执行其他的代码

注意

如果 Javascript 引擎在源代码中声明的实际位置找不到 let 变量的值,那么将为其分配 undefined 值。

销毁阶段

执行完毕出栈,等待回收被销毁

结合Worker通知应用更新

概述

项目部署上线后,特别是网页项目,提示正在操作系统的用户去更新版本非常 important。一般我们都会用“刷新大法”来清理缓存,但是对于正在操作网页的用户,不造系统更新了,请求的还是老版本的资源。

为了确保用户能够及时获得最新的功能和修复的 bug,我们需要通知用户刷新页面获取最新的代码。

方案

每次打包时,都生成一个时间戳,作为系统的伪版本,放到JSON文件中,通过对比文件的响应头Etag判断是否有更新。具体步骤如下:

1: 在public文件夹下加入manifest.json文件,里面存放两个字段:更新内容、更新时间戳

2: 前端打包的时候向manifest.json写入当前时间戳信息

3: 在入口文件main.js中引入检查版本更新的逻辑,有更新则提示更新。

路由守卫router.beforeResolve(vite+vue),检查更新,对比manifest.json文件的响应头Etag判断是否有更新

通过Worker轮询,检查更新,对比manifest.json文件的响应头Etag判断是否有更新。Worker线程并不影响其他线程的逻辑。

流程如下:

实现

注意:以下为vite+vue为例

新建manifest.json

public目录下新建manifest.json

{
  "timestamp":21312321311,
  "msg":"更新内容如下:\n--1.添加系统更新提示机制"
}

vite.config.js配置

配置vite打包输出更新mainfest.json

import { defineConfig, type AliasOptions } from 'vite'
import { fileURLToPath, URL } from 'node:url';

import vue from '@vitejs/plugin-vue'

import path from 'path'
import { readFile, writeFile } from 'fs'

// 获取路径
const filePath = path.resolve(`./public`, 'manifest.json')
// 读取文件内容
readFile(filePath, 'utf8', (err, data) => {

  if (err) {
    console.error('读取文件时出错:', err)
    return
  }
  // 将文件内容转换JSON
  const dataObj = JSON.parse(data)
  //修改时间戳
  dataObj.timestamp = new Date().getTime()
  // 将修改后的内容写回文件
  writeFile(filePath, JSON.stringify(dataObj), 'utf8', err => {
    if (err) {
      console.error('写入文件时出错:', err)
      return
    }
  })
})


const alias: AliasOptions = {
  '@': fileURLToPath(new URL('./src', import.meta.url)),
};

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      ...alias,
    },
  },
})

新建src/checkUpdate.js

检查更新文件

  1. 客户端发起请求,请求中包含上次获取的资源的ETag。
  2. 服务器收到请求后,比较客户端提供的ETag与当前资源的ETag是否一致。 5.如果一致,则返回HTTP 304 Not Modified响应,表示资源未发生变化,客户端可以使用缓存的版本。
  3. 如果不一致,服务器返回最新的资源内容,同时更新ETag。
  4. 客户端收到响应后,更新本地缓存的资源内容和ETag。
import router from "./router/index.ts";
//上次的Etag
let lastEtag = "";
//是否更新
let hasUpdate = false;
//创建worker线程
// const worker = new Worker();
const worker =  new Worker(new URL("./checkUpdate.worker.js", import.meta.url))


console.log(worker, "worker");



//检查版本更新
async function checkUpdate() {
  try {
    // 检测前端资源是否有更新
    let response = await fetch(`/manifest.json?v=${Date.now()}`, {
      method: "head",
    });
    // 获取最新的etag
    let etag = response.headers.get("etag");
    hasUpdate = lastEtag && etag !== lastEtag;
    lastEtag = etag;
    console.log((lastEtag = etag), "lastEtag = etag");
  } catch (e) {
    return Promise.reject(e);
  }
}

async function confirmReload(msg = "", lastEtag) {
  worker &&
    worker.postMessage({
      type: "pause",
    });
  try {
    console.log("版本更新了");
  } catch (e) {}
}
// 路由拦截
router.beforeEach(async (to, from, next) => {
  next();
  try {
    await checkUpdate();
    if (hasUpdate) {
      worker.postMessage({
        type: "destroy",
      });
      location.reload();
    }
  } catch (e) {}
});

worker.postMessage({
  type: "check",
});

worker.onmessage = ({ data }) => {
  console.log(data, "data");
  if (data.type === "hasUpdate") {
    hasUpdate = true;
    confirmReload(data.msg, data.lastEtag);
  }
};

新建src/checkUpdate.worker.js

let lastEtag
let hasUpdate = false
let intervalId = ''
async function checkUpdate() {
 
  try {
    // 检测前端资源是否有更新
    let response = await fetch(`/manifest.json?v=${Date.now()}`, {
      method: 'get'
    })
    // 获取最新的etag和data
    let etag = response.headers.get('etag')
    let data = await response.json()
    hasUpdate = lastEtag !== undefined && etag !== lastEtag
  
    if (hasUpdate) {
      postMessage({
        type: 'hasUpdate', 
        msg: data.msg,
        lastEtag: lastEtag,
        etag: etag
      })
    }
    lastEtag = etag
  } catch (e) {
    return Promise.reject(e)
  }
}

// 监听主线程发送过来的数据
addEventListener('message', ({ data }) => {
  console.log(data,'消息')
  if (data.type === 'check') {  
     console.log('checkcheckcheck')
    // 每5分钟执行一次
    // 立即执行一次,获取最新的etag,避免在setInterval等待中系统更新,第一次获取的etag是新的,但是lastEtag还是undefined,不满足条件,错失刷新时机
    // checkUpdate()
    intervalId = setInterval(()=>{
      checkUpdate()
      console.log('检查版本更新')
    },  3 * 1000)
  }
  if (data.type === 'recheck') {
    // 每5分钟执行一次
    hasUpdate = false
    lastEtag = data.lastEtag
    intervalId = setInterval(()=>{
      checkUpdate()
      console.log('检查版本更新')
    },  3 * 1000)
    console.log('recheckrecheckrecheck')
  }
  if (data.type === 'pause') {
    clearInterval(intervalId)
  }
  if (data.type === 'destroy') {
    clearInterval(intervalId)
    close()
  }
})


入口文件

示例:main.ts

import "./checkUpdate.js"

注意

在工程化项目中,由于要打包后部署,因此work线程文件需要处理资源路径

//创建worker线程
// const worker = new Worker();
const worker =  new Worker(new URL("./checkUpdate.worker.js", import.meta.url))

文件操作:showDirectoryPicker

概述

在传统的前端开发中,处理用户文件一直是个棘手的问题。虽然 <input type="file"> 提供了基本的文件选择功能,但当需要处理整个目录结构时,开发者往往需要借助复杂的后端支持或Electron等桌面框架。showDirectoryPicker() 的出现彻底改变了这一局面,它为Web应用提供了原生的目录访问能力,文件操作体验。

showDirectoryPicker

showDirectoryPicker() 是 File System Access API 的核心方法之一,它允许Web应用通过用户授权的方式访问整个目录结构,而不仅仅是单个文件。这意味着开发者现在可以在浏览器中实现以前只能在桌面应用中才能完成的文件操作。

核心特性

  • 完整的目录访问:读取、遍历、修改目录内容
  • 权限持久化:用户授权后可保存访问权限
  • 安全沙箱:在严格的用户控制下运行
  • 现代化API:基于Promise的异步设计

浏览器支持情况

只有部分浏览器支持,并且版本比较新,希望以后能够更好支持 image.png

基础使用与语法

 基本调用方式

async function selectDirectory() {
  try {
    const directoryHandle = await window.showDirectoryPicker();
    console.log('选择的目录:', directoryHandle.name);
    return directoryHandle;
  } catch (err) {
    if (err.name === 'AbortError') {
      console.log('用户取消了选择');
    } else {
      console.error('发生错误:', err);
    }
  }
}

配置选项

const options = {
  id: 'projectFolder', // 标识符,用于记住用户选择
  mode: 'readwrite',   // 权限模式:read 或 readwrite
  startIn: 'documents' // 起始目录:desktop, documents, downloads等
};

const directoryHandle = await showDirectoryPicker(options);

权限模式

  • read :仅读取权限,可以列出文件和读取内容
  • readwrite :读写权限,可以创建、修改、删除文件

实际应用场景

遍历文件内容

async function* walkDirectory(directoryHandle, path = '') {
  for await (const entry of directoryHandle.values()) {
    const entryPath = `${path}/${entry.name}`;
    
    if (entry.kind === 'directory') {
      yield* await walkDirectory(entry, entryPath);
    } else {
      yield {
        handle: entry,
        path: entryPath,
        kind: 'file'
      };
    }
  }
}

// 使用示例
async function printDirectoryTree() {
  const directoryHandle = await showDirectoryPicker();
  
  for await (const entry of walkDirectory(directoryHandle)) {
    console.log(entry.path);
  }
}

printDirectoryTree()

解析本地文件夹可视化到线上

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>

    <button>打开文件</button>


    <script>

        const btn = document.querySelector('button');

        btn.addEventListener('click', async () => {

            async function readDirectoryRecursive(directoryHandle, path = '') {
                const results = {
                    path: path || directoryHandle.name,
                    files: [],
                    directories: [],
                    complete: false,
                    totalFiles: 0
                };

                try {
                    for await (const entry of directoryHandle.values()) {
                        const entryPath = `${path}/${entry.name}`;

                        if (entry.kind === 'file') {
                            results.files.push({
                                name: entry.name,
                                path: entryPath,
                                handle: entry
                            });
                            results.totalFiles++;

                        } else if (entry.kind === 'directory') {
                            // 递归读取子目录
                            const subDirResult = await readDirectoryRecursive(entry, entryPath);
                            results.directories.push(subDirResult);
                            results.totalFiles += subDirResult.totalFiles;
                        }
                    }

                    results.complete = true;
                    return results;

                } catch (error) {
                    results.error = error;
                    results.complete = false;
                    throw error;
                }
            }

            // 使用示例
            async function main() {
                const directoryHandle = await showDirectoryPicker();
                const result = await readDirectoryRecursive(directoryHandle);

                if (result.complete) {
                    console.log(`递归读取完成,共找到 ${result.totalFiles} 个文件`);
                    console.log('目录结构:', result);

                    processFile(result);
                    async function processFile(result) {
                        if (result.files && result.files.length) {
                            for (let i = 0; i < result.files.length; i++) {
                                const fileHandle = result.files[i].handle;
                                const file = await fileHandle.getFile();
                                console.log("file----", file);
                                const fileReader = new FileReader();

                                fileReader.onload = (e) => {
                                    console.log("fileReader.result---", e.target.result);
                                }
                                fileReader.readAsText(file);
                            }
                        }

                        if (result.directories && result.directories.length) {
                            for (let i = 0; i < result.directories.length; i++) {
                                const dirRes = result.directories[i];
                                processFile(dirRes);
                            }
                        }

                    }
                }
            }
            main()




        });
    </script>
</body>

</html>

深度封装tree公共组件

概述

树组件在企业有多常用我就不用说了,特别是做管理系统相关的,如果能够将组件库的树组件经过二次高度封装后,其他地方类似就不用重复自己去写一遍,其实一个公共树组件封装得很完善的话还是挺麻烦的,因此,以下组件主要封装项目常见的树组件公共操作,减少项目实际使用还要额外去花很多时间单独封装,见下面详解

实现效果

image.png

演示

动画.gif

代码仓库

github.com/vgnip/vue3-… 使用demon:

image.png

image.png

组件功能清单

二次高度封装的数包含如下核心功能:

  • 节点获取
  • 节点拖拽
  • 懒加载和全量加载切换
  • 空状态配置
  • 异常状态配置
  • 可层级节点分页
  • 可定位任意等级节点(展开并高亮)
  • 树刷新
  • 树节点增删改操作
  • 树节点菜单配置
  • ...

技术栈

vue3+elementplus+ tiny-emitter

注意:多数api操作通过事件回调触发。

核心功能使用解释

上述树组件demon在源代码/component/test-tree/index.vue下,下面方法可以在对应文件中查询到

1、切换懒加载

默认情况使用全量加载数据,如果数据需要懒加载,需要配置

 /** 树懒加载切换 */
  function lazyChange(val: boolean | string | number) {
    if (!val) {
      tagId.value = '';
      nodeNum.value = '5';
      nodeLevel.value = '5';
    } else {
      tagId.value = '';
      nodeNum.value = '';
      nodeLevel.value = '';
    }
    refreshTree(true);
  }
  // 懒加载和全量加载函数
  function treeLoad(data: any, node: any): Promise<any> {
    if (node?.level === 0) {
      treeId.value = -1;
    }
    return new Promise((resolve, inject) => {
      setTimeout(() => {
        if (isError.value) {
          // eslint-disable-next-line prefer-promise-reject-errors
          inject();
          return;
        }
        let list = [];
        if (lazy.value) {
          list = isEmpty.value
            ? []
            : Array.from({ length: nodeTagNum.value }).fill(1).map(() => {
              treeId.value++;
              return {
                label: `节点点_${treeId.value}`,
                id: `${treeId.value}`,
                isLeaf: node?.level >= nodeTagLevel.value - 1,
              };
            });
        } else {
          list = isEmpty.value ? [] : initTreeData(nodeNum.value, nodeLevel.value);
        }
        console.log("tree-list", list)
        resolve(list);
      }, 300);
    });
  }

2、空状态和异常状态切换

异常状态

image.png 空状态

默认数据为空即可

image.png

3、节点分页

有些情况下,就算树是懒加载了,但是某个节点的数据依然很多,就需要在某个节点下进行二次分页(常见的就是机构下用户的情况)

image.png

props

 :node-page="isNodePage" // 是否开启分页
  :node-page-size="nodePageSize" //分页每页条数大小
   :node-page-local="isLocalPage" // 是否本地分页

4、显示checkbox

某些情况需要选择某些节点,包含懒加载

image.png

props

 :show-checkbox="showCheckBox"

获取选中节点

  eventBus.emit(
      TREE_EVENT_BUS + treeName,
      TreeEventTypes.getTree,
      (treeRefs: any) => {
      console.log("选中节点为", treeRefs.getCheckedNodes())
      },
    );

5、层级定位

事件名为toTagNodeByIdList和setCurrentKey,需要从父级开始到叶节点的id数组。

image.png

 /** node 定位 */
  function toTagNode() {
    if (tagId.value) {
      const ids = tagId.value.split(',');
      const tagName
        = ids.length > 1 ? TreeEventTypes.toTagNodeByIdList : TreeEventTypes.setCurrentKey;
      const data = ids.length > 1 ? ids : ids[0];
      eventBus.emit(TREE_EVENT_BUS + treeName, tagName, data);
    }
  }

6、刷新树

  /** 刷新树 */
  function refreshTree(val?: any) {
    if (!val) {
      eventBus.emit(TREE_EVENT_BUS + treeName, TreeEventTypes.reloadTreeData);
      return;
    }
    treeId.value = -1;
    treeKey.value++;
  }

7、节点获取

image.png

/** 获取node */
  function getNode() {
    eventBus.emit(
      TREE_EVENT_BUS + treeName,
      TreeEventTypes.getNodeById,
      nodeId.value,
      (node: any) => {
        console.log('获取结果:', node?.data);
        nodeData.value = cloneDeep(node?.data);
        delete nodeData.value?.children;
      },
    );
  }

8、节点操作

可对树节点进行crud操作

image.png 增加

  /** 添加子节点/编辑节点 */
  function addChild(isEdit = false, data?: any, isBefore?: boolean) {
    const obj = newNodeObj();
    const list = newNodeStr.value.split(',');

    try {
      const strObj: any = {};
      list.forEach((el) => {
        const item = el.split('=');
        strObj[item[0]] = item[1];
      });
      if ('id' in strObj) {
        obj.data.id = strObj.id;
        obj.data.label = `new_${strObj.id}`;
      }
      if ('label' in strObj) {
        obj.data.label = strObj.label;
      }
      if ('toActive' in strObj) {
        obj.toActive = strObj.toActive === 'true';
      }
      if ('tagId' in strObj) {
        obj.tagId = strObj.tagId;
        if (isEdit) {
          obj.data.id = strObj.tagId;
        }
      }
      if (data) {
        obj.tagId = data.id;
      }
    } catch {
      console.error('JSON数据格式错误');
    }
    let eventType = isEdit ? TreeEventTypes.editNode : TreeEventTypes.append;
    if (isBefore === true || isBefore === false) {
      eventType = isBefore ? TreeEventTypes.insertBefore : TreeEventTypes.insertAfter;
    }
    console.log('TREE_EVENT_BUS + treeName--', eventType, obj.data, obj.tagId, obj.toActive)
    eventBus.emit(TREE_EVENT_BUS + treeName, eventType, obj.data, obj.tagId, obj.toActive);
  }

9、节点拖拽

开启拖拽,可进行节点拖拽,并且对应事件处理

props

:draggable="true"

事件

拖拽完成通过dropEvent可以传递回调,具体返回参数可参考源码所示返回

    :dropEvent="dropEvent"
    
    
    /** 节点拖拽完成执行函数 */
function nodeDrop(draggingNode: any, dropNode: any, dropType: TREE_DRAG_TYPE) {
  if (props.dropEvent && typeof props.dropEvent === "function") {
    props.dropEvent(
      instance.exposed,
      draggingNode,
      dropNode,
      dropType,
      nodeDropEvent
    );
  } else {
    nodeDropEvent(instance.exposed, draggingNode, dropNode, dropType);
  }
  
}

10、树节点下拉

树节点添加下拉应该是非常常见的需求,如果自己去写,还是挺麻烦的,因此上面树组件内置集成通过配置式方式添加树节点下拉相关配置处理

image.png

配置下拉数据

//下拉数据
export const ConfigData = {
  options: [
    { label: '新增分类', type: 'add' },
    { label: '新增其他类', type: 'other' },
    { label: '弹窗高亮node', type: 'dialog', isDialog: true },
    { label: '置顶', type: 'toTop' },
    { label: '置低', type: 'toBot', class: 'disabled' },
    { label: '删除', type: 'del' },
  ],
};

控制节点显示逻辑

  /** 节点操作过滤 */
  const optionsFilter: any = (data: TreeNodeBaseInfoBO, node: any, options: TreeItemOptions[]) => {
    return options.filter((el) => {
      return Number(data.id) % 2 === 0
        ? !['toTop', 'toBot', 'dialog', 'add'].includes(el.type)
        : true;
    });
  };

props

 :options="ConfigData.options"//下拉数据
 :options-filter="optionsFilter"//控制每个节点显示逻辑

附组件prop

  treeName?: string // 组件引用名称 用于事件区分
  nodeKey?: string // 唯一索引
  nodeLabel?: string // 显示字段
  showOption?: boolean // 展示操作
  closePopOnClick?: boolean // 点击下拉项时关闭pop
  class?: string // 树组件样式
  eventBus?: any // 事件监听
  fetch?: (data?: TreeFetchBaseData, node?: any) => Promise<any[]> // 数据加载 异步请求
  lazy?: boolean // 是否懒加载
  nodePage?: boolean // 是否分页加载 默认false
  nodePageLocal?: boolean // 是否本地分页加载(所在层级是否本地) 默认true
  nodePageSize?: number // 分页大小 默认50
  options?: TreeItemOptions[] // 下拉项集
  optionWidth?: number // 下拉浮框宽度
  dynamicOptionWidth?: (node: any, data: TreeNodeBaseInfoBO) => number // 动态下拉框宽度
  optionIconWidth?: number // 下拉悬浮图标宽度
  optionClass?: string // 下拉框浮框样式类
  bagColor?: string // 高亮背景颜色
  dropEvent?: any // 拖拽自定义事件
  addChild?: any // 新增子点自定义
  props?: any // 树组件props
  allowDrop?: (draggingNode: any, dropNode: any, type: TREE_DRAG_TYPE) => boolean // 允许放置
  allowDrag?: (node: any) => boolean // 允许拖拽
  optionsFilter?: (
  data: TreeNodeBaseInfoBO,
  node: any,
  options?: TreeItemOptions[]
  ) => TreeItemOptions[] // 下拉项过滤

附组件支持的事件

以下事件都通过eventBus.emit()触发,可参考源码查看对应参数示例

export enum TreeEventTypes {
  refreshOperationPo = 'refreshOperationPo', // 更新右侧操作图标的定位 (会进行3次300mm延迟的定位,可频繁重复调用)
  closePop = 'closePop', // 关闭节点下拉
  closeDialog = 'closeDialog', // 弹窗关闭
  getTree = 'getTree', // 获取树组件
  getNodeById = 'getNodeById', // 根据id获取 node
  getCurrentKey = 'getCurrentKey', // 获取当前选中节点key
  setCurrentKey = 'setCurrentKey', // 设置当前选中节点key 默认会进行定位
  navigateCurrentNode = 'navigateCurrentNode', // 定位到当前选中节点
  toTagNodeByIdList = 'toTagNodeByIdList', // 根据id集 进行链路懒加载定位 根据id集 进行链路懒加载定位 父 -> 子 最后一个为目标节点
  reloadTreeData = 'reloadTreeData', // 重新加载树数据
  reBackDragNode = 'reBackDragNode', // 恢复上一次拖拽
  append = 'append', // 添加子节点
  insertBefore = 'insertBefore', // 指定节点之前插入
  insertAfter = 'insertAfter', // 指定节点之后插入
  remove = 'remove', // 删除节点
  editNode = 'editNode', // 修改节点
  setChecked = 'setChecked', // 设置节点选中状态
}

注意事项

  1. 节点分页加载按钮占用 node 节点
  2. 未展示出来的节点不可以定位
  3. 未展示出来的节点无法通过 id 进行获取
  4. 本地分页时加载按钮会存储本地未展示的 list 数据
  5. 非本地分页时当加载的节点数据>=nodePageSize 时加载按钮会展示出来
  6. 新增节点默认排在最后的不建议开启分页功能(节点默认追加在更多操作节点前,需要自行处理其余情况)
  • 更多详细的用法见 src/components/tree/bo 下的 event 和 treeBO 文件
❌