Open AnnVoV opened 6 years ago
当我们使用draggable组件时,我们发现我们只要向下面这样用就可以了, 我们只要把我们需要渲染的东西,作为draggable的子元素即可。那么思考一下,如何实现呢?
<!-- https://github.com/SortableJS/Vue.Draggable --> <draggable v-model="myArray" :options="{group:'people'}" @start="drag=true" @end="drag=false"> <ul> <li v-for="element in myArray" :key="element.id">{{element.name}}</li> </ul> </draggable>
假设我们要实现这个draggable组件, 其实渲染这步挺简单的就是用render函数去做
export default { name: 'draggable', render(h)=>{ // 这时候我们肯定没有模板了,只能自己来写 var slot = this.$slots.default; // 获取默认的子元素vdom this.element = slot.tag.toLowerCase(); // todo 不知道这里attribute具体怎么搞,暂时置为空对象 // 获取ul 上的一些属性 // render('标签', {attribute}, children) h(this.element, {}, slot[0].children) } }
我们知道vue.draggable这个组件其实是基于sortable.js 去做拖拽的。里面有个new Sortable(this.$el)的过程。那我先来大致分析下sortable的实现。
sortable.js 主要是利用drag的对应事件去做处理(dragstart, dragover, dragend)。
当你dragstart时,会获取dragTarget。比如你有javascript <li><div>主标题1</div><div>副标题2</div></li> ,你的dragTarget可能是<div>主标题1</div>或者```
javascript <li><div>主标题1</div><div>副标题2</div></li>
<div>主标题1</div>
/** * el 是否匹配selector 选择规则 * @param el * @param selector * @returns {*} * @private */ var _matches = function (el, selector) { if (el.matches(selector)) { return el } return null } /** * 查找离el的target是selector的最近的元素 * @param el * @param selector * @private */ var _closest = function (el, selector) { while (el.parentNode !== document) { if (_matches(el, selector)) { return el } el = el.parentNode } }
dragover 时通过e.target我们可以知道,当前手拖动到了哪个元素上,同样通过_closest方法我们可以找到toTarget; 那我们要么是需要将这个dragTarget移动到toTarget的后面,要么是要移动到toTarget的前面。判断前后的标准其实很简单(之前我自己想复杂了)var after = (toNode.nextElementSibling !== this.dragTarget);参考下下面的代码片段:
var after = (toNode.nextElementSibling !== this.dragTarget)
_dragOver (e) { var toNode = _closest(e.target, this.options.selector || '*') // 用于判断是插入前面还是插入在后面 var after = (toNode.nextElementSibling !== this.dragTarget) if (toNode !== this.dragTarget) { // 为什么不会重复进入这个方法 因为移动后dragOver的target变化了 toNode.parentNode.insertBefore(this.dragTarget, (after) ? toNode.nextElementSibling : toNode) if (typeof this.options.onMove === 'function') { e.endIndex = this.endIndex = _index(this.dragTarget, this.options.selector); e.startIndex = this.startIndex; // 提供的钩子 this.options.onMove.call(this, e) } } }
那明显在sortable.js里面提供了一些onStart, onMove, onAdd 等的钩子,通过new Sortable({onMove:…})我们可以设置回调,那我们怎么在我们draggable的组件里面,去将两者关联起来呢?
new Sortable({onMove:…})
我觉得draggable组件,设计的很巧妙也很简单, 在mounted的时候我们改写我们传入的options
var eventsListened = ['Start', 'Add', 'Remove', 'Update', 'End']; export default { props: { options: Object }, render() { ... }, mounted() { var optionsAdded = {}; eventsListened.forEach((elt) =>{ // 重点就在这个 delegateAndEmit optionsAdded['on' + elt] = this.delegateAndEmit(elt); }); var options = _extends(this.options, optionsAdded); this.st = new Sortable(this.$el, options); }, methods: { delegateAndEmit (evt) { return (e) => { // 就是在传入的回调里塞上this.$emit 这样我们的组件也能通过@事件名=xxx 来监听了 if (typeof this['onDrag' + evt] === 'function') this['onDrag' + evt](e) this.$emit(evt.toLowerCase(), e) } } } }
这个其实是在sortable.js里面处理的,作者很鸡贼,他就是先监听tap,然后把对应的target的属性置为draggable=true。(ps.我一开始还以为要遍历子元素,过滤selector, 统一设置为draggable=true)
这里其实蛮容易想到我们props父元素传入的list是需要更新的,那我们显然需要去使用v-model这个语法糖来最简便的实现数据的双向更新(参考阅读:Vue2 利用 v-model 实现组件props双向绑定的优美解决方案) ;可以发现vue.draggable也确实是这么处理的
// 思路,onMove时,我们通过e.startIndex, e.endIndex修改我们当前的newList, 得到更新后的newList后通过this.$emit('input', newList) 让我们绑定的数据更新 // 我们修改下我们delegateAndEmit方法,让他在$emit时,也能触发我们组建的cb methods: { _delegateAndEmit (evt) { return (e) => { // 内置这个组件内部的钩子 if (typeof this['onDrag' + evt] === 'function') this['onDrag' + evt](e) this.$emit(evt.toLowerCase(), e) } }, onDragMove (e) { var newArray = Object.assign([], this.value); var startIndex = e.startIndex; var endIndex = e.endIndex; var temp = newArray.splice(startIndex, 1); newArray.splice(endIndex, 0, temp[0]); // 所以这里会更新我们的数据,并利用v-model语法糖来双向绑定 this.$emit('input', newArray); } }
完整的两个文件会放到 /js/myDrag/ 文件夹下。
1.我们只提供容器,那组件是如何渲染的?
当我们使用draggable组件时,我们发现我们只要向下面这样用就可以了, 我们只要把我们需要渲染的东西,作为draggable的子元素即可。那么思考一下,如何实现呢?
假设我们要实现这个draggable组件, 其实渲染这步挺简单的就是用render函数去做
2.sortbale上的钩子是如何挂到vue组件上并$emit出来的?
我们知道vue.draggable这个组件其实是基于sortable.js 去做拖拽的。里面有个new Sortable(this.$el)的过程。那我先来大致分析下sortable的实现。
sortable.js 主要是利用drag的对应事件去做处理(dragstart, dragover, dragend)。
当你dragstart时,会获取dragTarget。比如你有
javascript <li><div>主标题1</div><div>副标题2</div></li>
,你的dragTarget可能是<div>主标题1</div>
或者```
dragover 时通过e.target我们可以知道,当前手拖动到了哪个元素上,同样通过_closest方法我们可以找到toTarget; 那我们要么是需要将这个dragTarget移动到toTarget的后面,要么是要移动到toTarget的前面。判断前后的标准其实很简单(之前我自己想复杂了)
var after = (toNode.nextElementSibling !== this.dragTarget)
;参考下下面的代码片段: 那明显在sortable.js里面提供了一些onStart, onMove, onAdd 等的钩子,通过
new Sortable({onMove:…})
我们可以设置回调,那我们怎么在我们draggable的组件里面,去将两者关联起来呢? 我觉得draggable组件,设计的很巧妙也很简单, 在mounted的时候我们改写我们传入的options
3.sortable 利用了draggable=true这个属性,如何自动为元素设置的?(这是sortable中处理的)
这个其实是在sortable.js里面处理的,作者很鸡贼,他就是先监听tap,然后把对应的target的属性置为draggable=true。(ps.我一开始还以为要遍历子元素,过滤selector, 统一设置为draggable=true)
4.sortable 主要处理了dom相关,dom的移动在sortable里面是通过调用insertBefore去处理的,那我们在组件里绑定的数据是如何进行更新?
这里其实蛮容易想到我们props父元素传入的list是需要更新的,那我们显然需要去使用v-model这个语法糖来最简便的实现数据的双向更新(参考阅读:Vue2 利用 v-model 实现组件props双向绑定的优美解决方案) ;可以发现vue.draggable也确实是这么处理的
完整的两个文件会放到 /js/myDrag/ 文件夹下。