本文最后更新于:2023年11月5日 晚上
如何调试源码
先创建一个vue2的项目,对于直接修改使用npm下载的依赖包中的源码代码进行调试是不行的,因为项目中使用的是编译后的element组件代码,也就是lib文件夹下的代码。
所以正确的调试步骤如下:
- 进入项目文件夹中拉取
element-ui源码,git clone https://github.com/ElemeFE/element.git
- 到源码的文件夹中,使用
npm i完成依赖下载
- 开始调试/修改代码,最后使用
npm run dist打包,看到源码文件夹中生成lib文件夹即可
- 在新项目中使用
npm i element-ui -S ./element,第二个element就是第一步拉取的源码文件夹
对于上面第二点的提示:高版本node会报错,建议使用12.22.12。其他的小报错没有关系,后续能正常使用就行
当正确使用本地的源码后,可以在package.json中查看到element-ui指向为本地源码的文件夹,如下:

后续的更改就只需要修改源码文件夹中的代码,然后npm run dist打包即可
v-clickoutside源码
在源码文件夹下 /src/utils/clickoutside.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| import Vue from 'vue'; import { on } from 'element-ui/src/utils/dom';
const nodeList = []; const ctx = '@@clickoutsideContext';
let startClick; let seed = 0;
!Vue.prototype.$isServer && on(document, 'mousedown', e => (startClick = e));
!Vue.prototype.$isServer && on(document, 'mouseup', e => { nodeList.forEach(node => node[ctx].documentHandler(e, startClick)); });
function createDocumentHandler(el, binding, vnode) { return function(mouseup = {}, mousedown = {}) { if (!vnode || !vnode.context || !mouseup.target || !mousedown.target || el.contains(mouseup.target) || el.contains(mousedown.target) || el === mouseup.target || (vnode.context.popperElm && (vnode.context.popperElm.contains(mouseup.target) || vnode.context.popperElm.contains(mousedown.target)))) return;
if (binding.expression && el[ctx].methodName && vnode.context[el[ctx].methodName]) { vnode.context[el[ctx].methodName](); } else { el[ctx].bindingFn && el[ctx].bindingFn(); } }; }
export default { bind(el, binding, vnode) { nodeList.push(el); const id = seed++; el[ctx] = { id, documentHandler: createDocumentHandler(el, binding, vnode), methodName: binding.expression, bindingFn: binding.value }; },
update(el, binding, vnode) { el[ctx].documentHandler = createDocumentHandler(el, binding, vnode); el[ctx].methodName = binding.expression; el[ctx].bindingFn = binding.value; },
unbind(el) { let len = nodeList.length;
for (let i = 0; i < len; i++) { if (nodeList[i][ctx].id === el[ctx].id) { nodeList.splice(i, 1); break; } } delete el[ctx]; } };
|
先查看工具函数
on 方法
1
| import { on } from 'element-ui/src/utils/dom';
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export const on = (function() { if (!isServer && document.addEventListener) { return function(element, event, handler) { if (element && event && handler) { element.addEventListener(event, handler, false); } }; } else { return function(element, event, handler) { if (element && event && handler) { element.attachEvent('on' + event, handler); } }; } })();
|
isServer:是否为服务端环境,我们可以视为false
补充isServer:Vue.js 是一个用于构建客户端应用的框架,这个属性通常用于在服务端渲染(SSR)时区分客户端和服务端环境。我们并不使用SSR,所以可以视为false
可以看到这是一个立即执行函数,只要正常存在document.addEventListener,就返回一个方法,这个方法接受三个参数(节点对象,事件名,处理方法),可为对应元素使用addEventListener建立监听事件
关于document.addEventListener第三个参数
详情介绍:https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener
别人的实例:https://blog.csdn.net/SunDaDa9/article/details/103693062
那么不存在document.addEventListener时呢,也是返回一个作用和参数一样的方法,只不过为了兼容IE把addEventListener换成了attachEvent
补充:addEventListener为W3C标准,除IE浏览器使用,而attachEvent只能在IE浏览器上使用,语法:Element.attachEvent(Etype,EventName),事件名前加on,如onclick
总结:on函数就是用于注册事件,可以兼容IE,以后要使用就可以直接引入使用了,使用如下:
1
| on(document, 'mousedown', e => startClick = e)
|
createDocumentHandler 方法
源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function createDocumentHandler (el, binding, vnode) { return function (mouseup = {}, mousedown = {}) { if (!vnode || !vnode.context || !mouseup.target || !mousedown.target || el.contains(mouseup.target) || el.contains(mousedown.target) || el === mouseup.target || (vnode.context.popperElm && (vnode.context.popperElm.contains(mouseup.target) || vnode.context.popperElm.contains(mousedown.target)))) return;
if (binding.expression && el[ctx].methodName && vnode.context[el[ctx].methodName]) { vnode.context[el[ctx].methodName](); } else { el[ctx].bindingFn && el[ctx].bindingFn(); } }; }
|
使用如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| bind (el, binding, vnode) { nodeList.push(el); const id = seed++; el[ctx] = { id, documentHandler: createDocumentHandler(el, binding, vnode), methodName: binding.expression, bindingFn: binding.value }; }
!Vue.prototype.$isServer && on(document, 'mouseup', e => { nodeList.forEach(node => node[ctx].documentHandler(e, startClick)); });
|
接受三个参数,这三个参数都是vue指令里面的参数就不说了,重点是返回的方法
documentHandler可传入两个参数,看源码中第一个参数传入了鼠标松开时的dom节点,第二个参数为鼠标按下时的dom节点,内部重点如下:
1 2 3 4 5 6 7
| if (binding.expression && el[ctx].methodName && vnode.context[el[ctx].methodName]) { vnode.context[el[ctx].methodName](); } else { el[ctx].bindingFn && el[ctx].bindingFn(); }
|
binding.expression:指令绑定的字符串,如v-demo="demoName"中就是demoName,而不是它的值
el[ctx].methodName:同上,(不能理解为什么还要判断一次)
vnode.context[el[ctx].methodName]:当前vue实例中存在这个名字(当绑定名的值不是方法时后续就会报错)
所以解释如下:当这个指令有绑定名,并且在当前的实例中存在这个属性就会直接调用,反之,如果这个指令存在绑定名但是这个值并没有在当前实例中,就会直接调用绑定值
尝试了半天也没有想到为什么要区分开这两个情况不直接调用, 我还是太菜了,总结就是一个作用:调用指令绑定的方法
分析好重点的部分的作用就只是调用方法后,来看下哪些情况下会直接返空
1 2 3 4 5 6 7 8 9 10
| !vnode || !vnode.context || !mouseup.target || !mousedown.target || el.contains(mouseup.target) || el.contains(mousedown.target) || el === mouseup.target || (vnode.context.popperElm && (vnode.context.popperElm.contains(mouseup.target) || vnode.context.popperElm.contains(mousedown.target)))
|
以上判断中最后三行为针对popper.js,目前能力不足,但是其他都是很好分析的
总结下重点只有两个:
- 鼠标按下时的元素不能为指定元素本身或子元素
- 鼠标抬起的元素不能为指定元素本身或子元素
所以整个createDocumentHandler返回的方法分析总结如下:
传入鼠标抬起和按下两个dom节点,如果这两个节点中的元素不为当前元素本身或子元素就调用指令绑定的方法
整体的详细分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| import Vue from 'vue'; import { on } from 'element-ui/src/utils/dom';
const nodeList = []; const ctx = '@@clickoutsideContext';
let startClick; let seed = 0;
!Vue.prototype.$isServer && on(document, 'mouseddown', e => (startClick = e));
!Vue.prototype.$isServer && on(document, 'mouseup', e => { nodeList.forEach(node => node[ctx].documentHandler(e, startClick)); });
function createDocumentHandler (el, binding, vnode) { return function (mouseup = {}, mousedown = {}) { if ( !vnode || !vnode.context || !mouseup.target || !mousedown.target || el.contains(mouseup.target) || el.contains(mousedown.target) || el === mouseup.target || (vnode.context.popperElm && (vnode.context.popperElm.contains(mouseup.target) || vnode.context.popperElm.contains(mousedown.target))) ) return; if ( binding.expression && el[ctx].methodName && vnode.context[el[ctx].methodName] ) { vnode.context[el[ctx].methodName](); } else { el[ctx].bindingFn && el[ctx].bindingFn(); } }; }
export default { bind (el, binding, vnode) { nodeList.push(el); const id = seed++; el[ctx] = { id, documentHandler: createDocumentHandler(el, binding, vnode), methodName: binding.expression, bindingFn: binding.value }; },
update (el, binding, vnode) { el[ctx].documentHandler = createDocumentHandler(el, binding, vnode); el[ctx].methodName = binding.expression; el[ctx].bindingFn = binding.value; },
unbind (el) { let len = nodeList.length;
for (let i = 0; i < len; i++) { if (nodeList[i][ctx].id === el[ctx].id) { nodeList.splice(i, 1); break; } } delete el[ctx]; } };
|
源码总结
这个实现方法主要由鼠标监听事件实现,通过把元素加上自身属性后添加进nodeList,然后在鼠标抬起的时候判断鼠标是否在元素之外来触发绑定的方法,唯一让我不理解的就是源码中methodName的作用,自己也尝试了外部引入方法,data中定义方法,还是在全局的原型链上定义方法,还是无法理解,我太菜了