Vue2实用方法及原理
# 自定义指令篇
编写 vue 的自定义指令可以使得许多事情变得轻松起来,只需要使用 v-xxx=“”并传入想要传入的参数即可。
# 点击绑定指令外的元素关闭该元素
let nodeList = []; // 元素搜集器,会将页面中所有绑定了clickoutside指令的dom元素存储起来,方便控制管理
let seed = 0;
let nowClickDom;
const rtx = "@clickOutSide";
const addEvent = (function () {
if (document.addEventListener) {
return function (element, event, func) {
if (element && event && func) {
element.addEventListener(event, func, false);
}
};
} else {
return function (element, event, func) {
if (element && event && func) {
element.attachEvent("on" + event, func);
}
};
}
})();
addEvent(document, "mousedown", (e) => (nowClickDom = e));
addEvent(document, "mouseup", (e) => {
nodeList.forEach((dom) => {
dom[rtx].documentHandler(e, nowClickDom);
});
});
function createDocumentHandler(el, binding, vnode) {
return function (mouseup = {}, mousedown = {}) {
if (
!vnode ||
!vnode.context || //vnode.context 是否存在,不存在退出
!mouseup.target || // mouseup.target 是否存在,不存在退出
!mousedown.target || //mousedown.target 是否存在,不存在退出
el.contains(mousedown.target) || //el是否包含mouseup.target/mousedown.target子节点,如果包含说明点击的是绑定元素的内部,则不执行clickoutside指令内容
el.contains(mouseup.target) ||
el === mouseup.target || //绑定对象el是否等于mouseup.target,等于说明点击的就是绑定元素自身,也不执行clickoutside指令内容
(vnode.context.popperElm &&
(vnode.context.popperElm.contains(mouseup.target) ||
vnode.context.popperElm.contains(mousedown.target)))
) {
return;
}
el[rtx].bindingFn && el[rtx].bindingFn();
};
}
export default {
bind(el, binding, vnode) {
nodeList.push(el);
const id = seed++;
el[rtx] = {
id, // 生成唯一id
documentHandler: createDocumentHandler(el, binding, vnode),
methods: binding.expression,
bindingFn: binding.value,
};
},
update(el, binding, vnode) {
el[rtx].documentHandler = createDocumentHandler(el, binding, vnode);
el[rtx].methods = binding.expression;
el[rtx].bindingFn = binding.value;
},
unbind(el) {
for (let index = 0; index < nodeList.length; index++) {
let node = nodeList[index];
if (node === el) {
nodeList.splice(index, 1);
break;
}
}
delete el[rtx];
},
};
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
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
# 点击数据上报
function reportClick(el, binding) {
ajax....
}
export default = (Vue) => {
Vue.directive('report', {
bind(el, binding, vnode) {
el.addEventListener('click', reportClick(el, binding))
},
unbind(el, binding, vnode) {
el.removeEventListener('click', reportClick)
}
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# v-loading
import Vue from "vue";
import LoadingComponent from "./loading";
// 使用 Vue.extend构造组件子类
const LoadingContructor = Vue.extend(LoadingComponent);
// 定义一个名为loading的指令
Vue.directive("loading", {
/**
* 只调用一次,在指令第一次绑定到元素时调用,可以在这里做一些初始化的设置
* @param {*} el 指令要绑定的元素
* @param {*} binding 指令传入的信息,包括 {name:'指令名称', value: '指令绑定的值',arg: '指令参数 v-bind:text 对应 text'}
*/
bind(el, binding) {
const instance = new LoadingContructor({
el: document.createElement("div"),
data: {},
});
el.appendChild(instance.$el);
el.instance = instance;
Vue.nextTick(() => {
el.instance.visible = binding.value;
});
},
/**
* 所在组件的 VNode 更新时调用
* @param {*} el
* @param {*} binding
*/
update(el, binding) {
// 通过对比值的变化判断loading是否显示
if (binding.oldValue !== binding.value) {
el.instance.visible = binding.value;
}
},
/**
* 只调用一次,在 指令与元素解绑时调用
* @param {*} el
*/
unbind(el) {
const mask = el.instance.$el;
if (mask.parentNode) {
mask.parentNode.removeChild(mask);
}
el.instance.$destroy();
el.instance = undefined;
},
});
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
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
# elementui 弹窗拖拽
index.js
import drag from './drag'
const install = function(Vue) {
Vue.directive('el-dialog-drag', drag)
}
if (window.Vue) {
window['el-dialog-drag'] = drag
Vue.use(install) // eslint-disable-line
}
drag.install = install
export default drag
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
drag.js
export default {
bind(el, binding, vnode) {
const dialogHeaderEl = el.querySelector('.el-dialog__header')
const dragDom = el.querySelector('.el-dialog')
dialogHeaderEl.style.cssText += ';cursor:move;'
dragDom.style.cssText += ';top:0px;'
const getStyle = (function() {
if (window.document.currentStyle) {
// ie
return (dom, attr) => dom.currentStyle[attr]
} else {
// google, firefox
return (dom, attr) => getComputedStyle(dom, false)[attr]
}
})()
dialogHeaderEl.onmousedown = e => {
// 元素距离可视区域
const disX = e.clientX - dialogHeaderEl.offsetLeft
const disY = e.clientY - dialogHeaderEl.offsetTop
const dragDomWidth = dragDom.offsetWidth
const dragDomHeight = dragDom.offsetHeight
const screenWidth = document.body.clientWidth
const screenHeight = document.body.clientHeight
const minDragDomLeft = dragDom.offsetLeft
const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth
const minDragDomTop = dragDom.offsetTop
const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight
// 获取到的值带px 正则匹配替换
let styL = getStyle(dragDom, 'left')
let styT = getStyle(dragDom, 'top')
if (styL.includes('%')) {
styL = +document.body.clientWidth * (+styL.replace(/\%/g, '') / 100)
styT = +document.body.clientHeight * (+styT.replace(/\%/g, '') / 100)
} else {
styL = +styL.replace(/\px/g, '')
styT = +styT.replace(/\px/g, '')
}
document.onmousemove = function(e) {
// 通过事件委托,计算移动的距离
let left = e.clientX - disX
let top = e.clientY - disY
// 边界处理
if (-(left) > minDragDomLeft) {
left = -minDragDomLeft
} else if (left > maxDragDomLeft) {
left = maxDragDomLeft
}
if (-(top) > minDragDomTop) {
top = -minDragDomTop
} else if (top > maxDragDomTop) {
top = maxDragDomTop
}
// 移动当前元素
dragDom.style.cssText += `;left:${left + styL}px;top:${top + styT}px;`
vnode.child && vnode.child.$emit('dragDialog')
}
document.onmouseup = function(e) {
document.onmousemove = null
document.onmouseup = null
}
}
}
}
}
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
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
# 方法篇
# Emitter 方法分发事件 mixins
function broadcast(componentName, eventName, params) {
this.$children.forEach((child) => {
const name = child.$options.name;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.call(child, componentName, eventName, params);
}
});
}
export default {
methods: {
/**
* @desc 向上遍历找寻对应组件名称的父组件分发事件
* @param {*} componentName
* @param {*} eventName
* @param {*} params
*/
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
// 当有父级且父级没有命名或是名字不等于传入的名字时
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.name;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
/**
* @desc 向下遍历找寻对应组件名称的子组件分发事件
* @param {*} componentName
* @param {*} eventName
* @param {*} params
*/
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
},
},
};
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
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
# 向上、向下寻找相应组件方法
// 由一个组件,向上找到最近的指定组件
export function findComponentUpward(context, componentName) {
let parent = context.$parent;
let name = parent.$options.name;
while (parent && (!name || [componentName].indexOf(name) < 0)) {
parent = parent.$parent;
if (parent) name = parent.$options.name;
}
return parent;
}
// 由一个组件,向上找到所有的指定组件
export function findComponentsUpward(context, componentName) {
let parents = [];
const parent = context.$parent;
if (parent) {
if (parent.$options.name === componentName) parents.push(parent);
return parents.concat(findComponentsUpward(parent, componentName));
} else {
return [];
}
}
// 由一个组件,向下找到最近的指定组件
export function findComponentDownward(context, componentName) {
const childrens = context.$children;
let children = null;
if (childrens.length) {
for (const child of childrens) {
const name = child.$options.name;
if (name === componentName) {
children = child;
break;
} else {
children = findComponentDownward(child, componentName);
if (children) break;
}
}
}
return children;
}
// 由一个组件,向下找到所有指定的组件
export function findComponentsDownward(context, componentName) {
return context.$children.reduce((components, child) => {
if (child.$options.name === componentName) components.push(child);
const foundChilds = findComponentsDownward(child, componentName);
return components.concat(foundChilds);
}, []);
}
// 由一个组件,找到指定组件的兄弟组件
export function findBrothersComponents(
context,
componentName,
exceptMe = true
) {
let res = context.$parent.$children.filter((item) => {
return item.$options.name === componentName;
});
let index = res.findIndex((item) => item._uid === context._uid);
if (exceptMe) res.splice(index, 1);
return res;
}
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
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
# Vue 注册组件、挂载原型对象
const install = function (Vue) {
// 判断是否安装
if (install.installed) return;
Vue.component(component.name, component);
Vue.prototype.$cdmessage = Message;
};
// 使用
Vue.use(cdui);
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 使用 JS 挂载使用组件(如 EL-Message)
extend 配合$mount 挂载
这里使用 Message 组件源码来理解
import Vue from "vue";
import Main from "./main.vue";
// 创建一个构造器,此时未挂载
let MessageConstructor = Vue.extend(Main);
let seed = 1;
let instance;
let instances = [];
const Message = function (options) {
if (Vue.prototype.$isServer) return;
options = options || {};
// 若是传入非object对象类型,做兼容
if (typeof options === "string") {
options = {
message: options,
};
}
let id = "cdmessage_" + seed++;
let onClose = options.onClose;
options.onClose = function () {
return Message.close(id, onClose);
};
// 传递data参数(调用$mount前,此时还未完成渲染),new之后的instance已经是一个标准的vue组件实例了
instance = new MessageConstructor({
data: options,
});
instance.id = id;
// 在mount挂载之后添加的属性是不会被拦截监听的,如果想要监听需要事先在vue文件内定义该属性走render
instance.$mount();
document.body.appendChild(instance.$el);
// 距离顶口的偏移量,当出现多个提示时候,要根据次序排列距离顶部的高度,避免覆盖。
if (options.offset && typeof options.offset !== "number") {
window ? window.console.warn("offet value must be number!") : "";
}
let verticalOffset = options.offset || 20;
instances.forEach((item) => {
verticalOffset += item.$el.offsetHeight + 16;
});
instance.verticalOffset = verticalOffset;
instance.visible = true;
instances.push(instance);
return instance;
};
const msgType = ["success", "info", "warning", "error", "loading"];
msgType.map((item) => {
Message[item] = (options) => {
options = options || {};
if (typeof options === "string") {
options = {
message: options,
};
} else if (!options.type) {
options.type = item;
}
return Message(options);
};
});
Message.close = function (id, fn) {
let len = instances.length;
let index = -1;
let removedHeight;
for (let i = 0; i < len; i++) {
if (instances[i] && instances[i].id === id) {
index = i;
removedHeight = instances[i].$el.offsetHeight;
if (typeof fn === "function") {
fn(instances[i]);
}
instances.splice(i, 1);
}
}
/**
* 经典判断:是否需要因为message的减少而进行top的位移
* 1.若是不超过1个,则不发生位移
* 2.未找到需要关闭的message
* 3.当关闭的message是最后一个时
*/
if (len <= 1 || index === -1 || index > instances.length - 1) return;
for (let i = 0; i < instances.length; i++) {
const element = instances[i];
element.$el.style["top"] =
parseInt(element.$el.style["top"], 10) - removedHeight - 16 + "px";
}
};
// 全局销毁
Message.closeAll = function () {
//for (let i = 0; i < instances.length; i++)!!这样的for循环是错的,因为会随着close减少instances的数量
for (let i = instances.length - 1; i > -1; i--) {
instances[i].close();
}
};
export default Message;
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
newVue 实例挂载
import Vue from "vue";
import xxx from "xxx.vue";
export function init() {
const container = document.createELement("div");
document.body.appendChild(container);
new Vue({
render: (h) => h(xxx),
}).$mount(container);
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
编辑 (opens new window)
上次更新: 2023/05/03, 18:01:41