CD's blog CD's blog
首页
  • HTMLCSS
  • JavaScript
  • Vue
  • TypeScript
  • React
  • Node
  • Webpack
  • Git
  • Nestjs
  • 小程序
  • 浏览器网络
  • 学习笔记

    • 《TypeScript 从零实现 axios》
    • Webpack笔记
  • JS/TS教程

    • 《现代JavaScript》教程
🔧工具方法
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

CD_wOw

内卷的行情,到不了的梦
首页
  • HTMLCSS
  • JavaScript
  • Vue
  • TypeScript
  • React
  • Node
  • Webpack
  • Git
  • Nestjs
  • 小程序
  • 浏览器网络
  • 学习笔记

    • 《TypeScript 从零实现 axios》
    • Webpack笔记
  • JS/TS教程

    • 《现代JavaScript》教程
🔧工具方法
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Vue2基础及原理

    • Vue2的生命周期
    • Vue2实用方法及原理
      • 自定义指令篇
        • 点击绑定指令外的元素关闭该元素
        • 点击数据上报
        • v-loading
        • elementui 弹窗拖拽
      • 方法篇
        • Emitter 方法分发事件 mixins
        • 向上、向下寻找相应组件方法
        • Vue 注册组件、挂载原型对象
        • 使用 JS 挂载使用组件(如 EL-Message)
    • vue2 的 keepAlive 实现原理
    • Vue2的extend与手动挂载$mount
    • Vue2如何做同构SSR
    • Vue2.x之$set是怎么实现的
    • vue2在TypeScript中如何使用vuex
    • Vue2.x中一些技巧及痛点问题解决方法
    • Vue2实现原理
    • Vue2 题解QA
  • Vue3基础及原理

  • 周边工具

  • Vue笔记
  • Vue2基础及原理
CD
2020-07-18
目录

Vue2实用方法及原理

# 自定义指令篇

编写 vue 的自定义指令可以使得许多事情变得轻松起来,只需要使用 v-xxx=“”并传入想要传入的参数即可。

# 点击绑定指令外的元素关闭该元素

即 clickoutside(v-clickoutside) (opens new window)

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

# 点击数据上报

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

# 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

# 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

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

# 方法篇

# 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

# 向上、向下寻找相应组件方法

// 由一个组件,向上找到最近的指定组件
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

# 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

# 使用 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

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
编辑 (opens new window)
#Vue2
上次更新: 2023/05/03, 18:01:41
Vue2的生命周期
vue2 的 keepAlive 实现原理

← Vue2的生命周期 vue2 的 keepAlive 实现原理→

最近更新
01
gsap动画库学习笔记 - 持续~
06-05
02
远程组件加载方案笔记
05-03
03
小程序使用笔记
03-29
更多文章>
Theme by Vdoing | Copyright © 2020-2023 CD | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式