function sameVnode (a, b) { return ( a.key === b.key && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) || ( isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error) ) ) ) }
首先会判断key是否一致,然后是tag类型还有input类型等等。
所以下标作为key的时候,很明显key会很容易就会判断为一致了,其次就是要看tag类型等等。
继续如果从map里面找到了对应的旧Vnode,又会继续把这个Vnode对应的Dom节点移动到旧的oldStartElem前面。
综上,Diff算法的移动都是在旧Vnode上进行的,而新Vnode仅仅只是更新了elm这个属性。
在个Diff算法的最后,可以想象一种情况,元素都会往头尾两边移动,剩下都是待会要剔除的元素了,需要执行离开动画,但是这个效果肯定很糟糕,因为这个时候的列表是打乱了的,我们所期望的动画明显是元素从原有的位置执行离开动画了,那么也就是removeOnly存在的意义了。
transition-group的魔法
transition-group是如何利用removeOnly的尼;直接跳到transition-group的源码上,直接就是一段注释:
// Provides transition support for list items. // supports move transitions using the FLIP technique. // Because the vdom's children update algorithm is "unstable" - i.e. // it doesn't guarantee the relative positioning of removed elements, // we force transition-group to update its children into two passes: // in the first pass, we remove all nodes that need to be removed, // triggering their leaving transition; in the second pass, we insert/move // into the final desired state. This way in the second pass removed // nodes will remain where they should be.
大意就是:
这个组件是为了给列表提供动画支持的,而组件提供的动画运用了FLIP技术;
因为Diff算法是不能保证移除元素的相对位置的(正如我们上面总结的),我们让transition-group的更新必须经过了两个阶段,第一个阶段:我们先把所有要移除的元素移除以便触发它们的离开动画;在第二个阶段:我们才把元素移动到正确的位置上。
知道了大致的逻辑了,那么transition-group具体是怎么实现的尼?
首先transition-group继承了transiton组件相关的props,所以它们两个真是铁打的亲兄弟。
const props = extend({ tag: String, moveClass: String }, transitionProps)
然后第一个重点来了beforeMount方法
beforeMount () { const update = this._update this._update = (vnode, hydrating) => { const restoreActiveInstance = setActiveInstance(this) // force removing pass this.__patch__( this._vnode, this.kept, false, // hydrating true // removeOnly (!important, avoids unnecessary moves) ) this._vnode = this.kept restoreActiveInstance() update.call(this, vnode, hydrating) } }
transition-group对_update方法做了特殊处理,先强行进行一次patch,然后才执行原本的update方法,这里也就是刚才注释说的两个阶段的处理;
接着看this.kept,transition-group是在什么时候对VNode tree做的缓存的尼,再跟踪代码发现render方法也做了特殊处理:
render (h: Function) { const tag: string = this.tag || this.$vnode.data.tag || 'span' const map: Object = Object.create(null) const prevChildren: Array<VNode> = this.prevChildren = this.children const rawChildren: Array<VNode> = this.$slots.default || [] const children: Array<VNode> = this.children = [] const transitionData: Object = extractTransitionData(this) for (let i = 0; i < rawChildren.length; i++) { const c: VNode = rawChildren[i] if (c.tag) { if (c.key != null && String(c.key).indexOf('__vlist') !== 0) { children.push(c) map[c.key] = c ;(c.data || (c.data = {})).transition = transitionData } else if (process.env.NODE_ENV !== 'production') { const opts: ?VNodeComponentOptions = c.componentOptions const name: string = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag warn(`<transition-group> children must be keyed: <${name}>`) } } } if (prevChildren) { const kept: Array<VNode> = [] const removed: Array<VNode> = [] for (let i = 0; i < prevChildren.length; i++) { const c: VNode = prevChildren[i] c.data.transition = transitionData c.data.pos = c.elm.getBoundingClientRect() if (map[c.key]) { kept.push(c) } else { removed.push(c) } } this.kept = h(tag, null, kept) this.removed = removed } return h(tag, null, children) },
这里的处理是首先用遍历transition-group包含的VNode列表,把VNode都收集到children数组还有map上面去,并且把transition相关的属性注入到VNode上,以便VNode移除的时候触发对应的动画。