Vue3组件化源码】树形组件ElTree的内部原理
最近一直在做Element3的Tree组件开发,这里就记录一下Tree组件的思想和内部实现原理,也对想要学习源码的童鞋的一个帮助吧。
设计思想
在设计Tree组件的时候是采用两颗树进行互相映射的
方案进行设计的,一颗树是用户自定义节点构成的树RawNode
,另一颗是内部进行渲染的树TreeNode
。当RawNode
某个节点的值变更后Mapper
就会得到通知,然后通过通知的内容对TreeNode
进行更改。
整个组件的难点就是在Mapper
这里,他需要完成:
- 节点转换与映射
- 变更监听
- 响应更改
而这里需要注意的两点是:
- 在
RawNode
变更后Mapper
要对TreeNode
进行修改,但是在修改TreeNode
后不能在通知变更去修改RawNode
- 在监听节点的时候,需要对存储子节点的数组进行监听(children)
这里就引出了一个重要的问题如何监听一个节点的变化?
这里就要说一下ES6的一个新的类Proxy
,顾名思义就是代理
,他可以对一个对象进行拦截如:
- 读取属性拦截 get
- 修改属性拦截 set
- 删除属性拦截 deleteProperty
- 等等 es6.ruanyifeng.com/#docs/proxy
这里Proxy还有个特性就是,在修改原始对象的时候不会触发拦截,通过这个特性就很好解决了注意点1
Proxy还有一个特性就是,每次在修改前一定会触发get操作,因为他是先获取,在修改
测试先行
TDD 测试先行,我们先想一下需要什么方法,然后先假设有在测试里用一下,然后在去写这个方法
其实很多时候我们内心就是这样的,每次在开发类或者函数之前肯定要先想接口,然后在实现,而TDD只是将内心的活动带到了现实里,这样做有几点好处
1、之后可以自动测试
2、理清楚了接口
3、在你对代码大动刀戈的时候他可以起到一定的指引作用,这一点在开发Tree的时候是感受颇深
- 首先需要一个TreeNode类作为树的节点
- 一个可以监听对象的的工具类 Watcher
- 一个可以事件通知的工具类 Event
- 一个需要对象映射Mapper类
TreeNode Spec
我们需要传入id、label、children来实现节点的创建,这也是TreeNode必须要传入的
describe('TreeNode.js', ()=>{
it('init a node', ()=>{ // 初始化一个节点
const root = new TreeNode(1, 'Node1', [
new TreeNode(2, 'Node2', [])
])
expect(root.id).toBe(1)
expect(root.label).toBe('Node1')
expect(root.children[0].id).toBe(2)
expect(root.children[0].label).toBe('Node')
})
})
复制代码
Watcher Spec
这里我们要将对象的操作进行拦截,这样才能知道这个对象的变化
- 监听节点内哪个属性修改了什么值
- 监听存放孩子节点的数组
- 节点的增加
- 节点的修改(这里需要注意一下,修改就是指针变化)
- 节点的删除
describe('Watcher.js', ()=>{
it('listern a node prop change', ()=>{
const root = {
label: 'Node1',
}
const watcher = new Watcher(root)
const _root = watcher.proxy // 拿到代理后的对象
const changeHandler = jest.fn()
watcher.bindHandler('change', changeHandler)
const addHandler = jest.fn()
watcher.bindHandler('add', addHandler)
_root.label = "Test"
expect(changeHandler).toHaveBeenCalledTimes(1)
_root.disabled = true
expect(addHandler).toHaveBeenCalledTimes(1)
})
it('listen a node children node pointer and length change', ()=>{
const root = {
label: 'Node1',
children:[
{
label: 'Node2'
}
]
}
const watcher = new Watcher(root)
const _root = watcher.proxy // 拿到代理后的对象
const childrenChangeHandler = jest.fn()
watcher.bindHandler('array/change', childrenChangeHandler)
const addHandler = jest.fn()
watcher.bindHandler('array/', addHandler)
_root.label = 'Test'
expect(childrenChangeHandler).toHaveBeenCalledTimes(1)
expect(childrenChangeHandler).toHaveBeenNthCalledWith(1, {
target: root,
key: 'label',
value: 'Test',
currentNode: root
})
_root.disabled = true
expect(addHandler).toHaveBeenCalledTimes(1)
expect(childrenChangeHandler).toHaveBeenNthCalledWith(1, {
target: root,
key: 'disabled',
value: true,
currentNode: root
})
})
})
复制代码
Event Spec
这里要实现简单的事件的监听和发送,你也可以理解为订阅与推送
describe('Event.js', ()=>{
it('listen a event', ()=>{
const event = new Event()
const cb = jest.fn()
event.on('ev1', cb)
event.emit('ev1', 1, 2, 3)
expect(cb).toHaveBeenCalledTimes(1)
expect(cb).toHaveBeenCalledWith(1, 2, 3)
})
})
复制代码
Mapper Spec
这里就是最终将上面的功能都集合到一起
- 节点转换与映射
- 变更监听
- 响应更改
describe('Mapper.js', () => {
it('mapper a tree', () => {
const rawNode = {
text: 'Node1',
childs: [
{
text: 'Node11',
childs: [
{
text: 'Node111',
childs: []
}
]
}
]
}
const mapper = new TreeMapper(rawNode, {
label: 'text',
children: 'childs'
})
const rawNodeProxy = mapper.rawNodeProxy
const treeNodeProxy = mapper.treeNodeProxy
rawNodeProxy.text = "Test"
expect(rawNodeProxy.text).toEqual(treeNodeProxy.label)
expect(rawNodeProxy.childs[0].text).toEqual(treeNodeProxy.children[0].label)
expect(rawNodeProxy.childs[0].childs[0].text).toEqual(
treeNodeProxy.children[0].children[0].label
)
})
...
})
复制代码
实现原理
TreeNode
这个利用ES6的Class简单的实现了一下TreeNode
class TreeNode {
constructor(id, label, children) {
this.id = id
this.label = label;
this.children = children ?? [];
}
}
复制代码
Watcher
这里主要是通过Proxy进行代理拦截,然后通过Event推送出去
class Watcher {
constructor(target) {
this.event = new Event(); // 用于变更通知
this.toProxy = new WeakMap(); // WeakMap 有个特别好的特性,可以自动移除未引用的对象
this.toRaw = new WeakMap();
this.proxy = this.reactive(target, target);
}
reactive(target, lastTarget) { // 嵌套响应式
if (!isObject(target) || this.toRaw.has(target)) { // 如果当前是代理,或者不是对象则返回
return target;
}
if (this.toProxy.has(target)) { // 如果当前对象以及代理则返回代理
return this.toProxy.get(target);
}
const currentNode = isArray(lastTarget) ? target : lastTarget; // 获取当前的节点
const handler = {
get: this.createGetter(),
set: this.createSetter(currentNode),
deleteProperty: this.createDeleteProperty(currentNode),
};
const observer = new Proxy(target, handler);
this.toProxy.set(target, observer); // 建立原始对象和代理
this.toRaw.set(observer, target); // 对象的映射关系
return observer;
}
bindHandler(type, callback) { // 绑定一个通知
this.event.on(type, callback);
}
createGetter() {
return (target, key) => {
const res = Reflect.get(target, key);
return isObject(res) ? this.reactive(res, target) : res;
// 如果是对象,则继续嵌套代理,如果不是对象则返回这个值
};
}
createSetter(currentNode) {
return (target, key, value) => {
if (this.toRaw.has(value)) { // 如果写入的是已经被代理的对象,则先转换为普通对象
value = this.toRaw.get(value);
}
if (isArray(target)) {
if (key === "length") {
this.event.emit("array/changeLength", {
target,
key,
value,
currentNode,
});
} else {
if (Reflect.has(target, key)) {
// 修改
this.event.emit("array/change", {
target,
key,
value,
currentNode,
});
} else {
// 新增
this.event.emit("array/append", {
target,
key,
value,
currentNode,
});
}
}
} else {
if (Reflect.has(target, key)) {
// 修改
this.event.emit("change", { target, key, value, currentNode });
} else {
// 新增
this.event.emit("add", { target, key, value, currentNode });
}
}
return Reflect.set(target, key, value);
};
}
createDeleteProperty(currentNode) {
return (target, key) => {
if (isArray(target)) {
this.event.emit("array/delete", { target, key, currentNode });
} else {
this.event.emit("delete", { target, key, currentNode });
}
return Reflect.deleteProperty(target, key);
};
}
}
复制代码
如果看过Vue3的Reactivity这个库,你会发现我这里的Watcher与Reactivity有些类似,其实Watcher的内部就是借鉴了一些Vue3Reactive的实现。
Event
简单的实现一个事件通知
class Event {
constructor() {
this.events = new Map();
}
on(name, callback) {
if (!this.events.has(name)) {
this.events.set(name, new Set([callback]));
return;
}
this.events.get(name).add(callback);
}
emit(name, ...args) {
if (this.events.has(name)) {
this.events.get(name).forEach((cb) => cb(...args));
}
}
}
复制代码
Mapper
这个组件的实现大概是这几步
1、转换RawNode -> TreeNode
2、对RawNode进行Watcher监听
3、对TreeNode进行Watcher监听
4、当发生变更通知后,对原数据进行修改
class Mapper {
constructor(rawNode, keyMap) {
this.toTreeNode = new WeakMap();
this.toRawNode = new WeakMap();
this.toRawNodeKey = keyMap;
this.toTreeNodeKey = reversalNodeKeyMap(keyMap); // 反向 NodeKey
// 初始化
this.rawNode = rawNode;
this.treeNode = this.convertToTreeNode(rawNode);
// 生成TreeNode
this.rawNodeWatcher = new Watcher(this.rawNode);
this.treeNodeWatcher = new Watcher(this.treeNode);
this.withRawNodeHandler();
this.withTreeNodeHandler();
// 对 rawNode 与 treeNode 分别进行响应式处理
}
convertToTreeNode(rawNode) {
const treeNode = new TreeNode(
rawNode[this.toRawNodeKey.id],
rawNode[this.toRawNodeKey.label],
this.convertToTreeNodes(rawNode[this.toRawNodeKey.children]),
{ isChecked: rawNode[this.toRawNodeKey.isChecked] }
);
this.toTreeNode.set(rawNode, treeNode);
this.toRawNode.set(treeNode, rawNode);
return treeNode;
}
convertToRawNode(treeNode) {
const rawNode = {
[this.toRawNodeKey.id]: treeNode.id,
[this.toRawNodeKey.label]: treeNode.label,
[this.toRawNodeKey.children]: this.convertToRawNodes(treeNode.children),
};
this.toTreeNode.set(rawNode, treeNode);
this.toRawNode.set(treeNode, rawNode);
return rawNode;
}
convertToTreeNodes(rawNodes) {
return rawNodes?.map((node) => this.convertToTreeNode(node));
}
convertToRawNodes(treeNodes) {
return treeNodes?.map((node) => this.convertToRawNode(node));
}
withRawNodeHandler() {
this.rawNodeWatcher.bindHandler(
"array/append",
({ currentNode, value }) => {
const currentTreeNode = this.toTreeNode.get(currentNode);
this.forTreeNodeAppendChild(
currentTreeNode,
this.convertToTreeNode(value)
);
}
);
this.rawNodeWatcher.bindHandler("array/delete", ({ currentNode, key }) => {
const currentTreeNode = this.toTreeNode.get(currentNode);
this.forTreeNodeRemoveChild(currentTreeNode, key);
});
this.rawNodeWatcher.bindHandler(
"array/change",
({ currentNode, key, value }) => {
const currentTreeNode = this.toTreeNode.get(currentNode);
this.forTreeNodeUpdateChild(
currentTreeNode,
key,
this.toTreeNode.get(value) ?? this.convertToTreeNode(value)
);
}
);
this.rawNodeWatcher.bindHandler("change", ({ currentNode, key, value }) => {
const currentTreeNode = this.toTreeNode.get(currentNode);
this.forTreeNodeUpdateValue(
currentTreeNode,
this.toTreeNodeKey[key],
value
);
});
this.rawNodeWatcher.bindHandler("add", ({ currentNode, key, value }) => {
const currentTreeNode = this.toTreeNode.get(currentNode);
this.forTreeNodeUpdateValue(
currentTreeNode,
this.toTreeNodeKey[key],
value
);
});
}
withTreeNodeHandler() {
this.treeNodeWatcher.bindHandler(
"array/append",
({ currentNode, value }) => {
const currentRawNode = this.toRawNode.get(currentNode);
this.forRawNodeAppendChild(
currentRawNode,
this.convertToRawNode(value)
);
}
);
this.treeNodeWatcher.bindHandler("array/delete", ({ currentNode, key }) => {
const currentRawNode = this.toRawNode.get(currentNode);
this.forRawNodeRemoveChild(currentRawNode, key);
});
this.treeNodeWatcher.bindHandler(
"array/change",
({ currentNode, key, value }) => {
const currentRawNode = this.toRawNode.get(currentNode);
this.forRawNodeUpdateChild(currentRawNode, key, value);
}
);
this.treeNodeWatcher.bindHandler(
"change",
({ currentNode, key, value }) => {
const currentRawNode = this.toRawNode.get(currentNode);
this.forRawNodeUpdateValue(
currentRawNode,
this.toRawNodeKey[key],
value
);
}
);
this.treeNodeWatcher.bindHandler("add", ({ currentNode, key, value }) => {
const currentRawNode = this.toRawNode.get(currentNode);
this.forRawNodeUpdateValue(currentRawNode, this.toRawNodeKey[key], value);
});
}
forTreeNodeAppendChild(currentTreeNode, newTreeNode) {
currentTreeNode.children.push(newTreeNode);
}
forTreeNodeUpdateValue(currentTreeNode, key, value) {
if (key === "children") {
currentTreeNode[key] = this.convertToTreeNodes(value);
} else {
currentTreeNode[key] = value;
}
}
forTreeNodeRemoveChild(currentTreeNode, index) {
// TODO: 这里还得对toRawNode 与 toTreeNode 进行处理,不然会内存泄漏
currentTreeNode.children.splice(index, 1);
}
forTreeNodeUpdateChild(currentTreeNode, index, childNode) {
currentTreeNode.children[index] = childNode;
}
forRawNodeAppendChild(currentRawNode, newRawNode) {
currentRawNode[this.toRawNodeKey.children].push(newRawNode);
}
forRawNodeUpdateValue(currentRawNode, key, value) {
if (key === this.toRawNodeKey["children"]) {
currentRawNode[key] = this.convertToRawNodes(value);
} else if (Reflect.has(currentRawNode, key)) {
currentRawNode[key] = value;
}
}
forRawNodeRemoveChild(currentRawNode, index) {
currentRawNode[this.toRawNodeKey.children].splice(index, 1);
}
forRawNodeUpdateChild(currentRawNode, index, childNode) {
currentRawNode[this.toRawNodeKey.children][index] = childNode;
}
}
复制代码
结束
在开发Tree的时候其实,还要比上面写的要复杂,这里是简化了一些操作,实际上在真正的Tree里他还需要将TreeNode进行Vue的reactive响应化,但是在开发的过程中,Vue的响应化和Watcher的响应化有一些冲突,所以会有一些很坑人的事情发生,不过最后也用了一些方法解决了,这里你可以去看Tree的源码学习。这里我说了这么多其实想说的是,当你遇到大坑的时候,将这个大坑解决掉,可以加深对这个坑所对应的问题理解,这就像是摸着坑过河吧。
作者:花果山技术团队
链接:https://juejin.cn/post/6926144123669839880
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。