动画一直是前端比较纠结的点,容易被忽视却又是那么重要,能写出让人感到愉悦自然的交互体验确实能为项目增色不少,毕竟这是上手就能感受到的,所以很有必要对vue的transition组件实现原理一探究竟。transition组件的动画实现分为两种,使用css类名和JavaScript钩子,接下来依次介绍。
这是一个抽象组件,也就是说在组件渲染完成后,不会以任何dom的形式表现出现,只是以插槽的形式对内部的子节点进行控制。它的作用是在合适的时机进行Css类名的添加/删除或执行JavaScript钩子来达到动画执行的目的。
既然是组件,那么在生成为真实Dom的时候,首先需要转为VNode,然后才是拿着这个VNode去转为真实的Dom。所以我们首先来看下transition组件会变成一个什么样的VNode。
export const transitionProps = { // transition组件接受的props属性
appear: Boolean, // 是否首次渲染
css: Boolean, // 是否取消css动画
mode: String, // in-out或out-in二选一
type: String, // 显示声明监听animation或transition
name: String, // 默认v
enterClass: String, // 默认`${name}-enter`
leaveClass: String, // 默认`${name}-leave`
enterToClass: String, // 默认`${name}-enter-to`
leaveToClass: String, // 默认`${name}-leave-to`
enterActiveClass: String, // 默认`${name}-enter-active`
leaveActiveClass: String, // 默认`${name}-leave-active`
appearClass: String, // 首次渲染时进入
appearActiveClass: String, // 首次渲染时持续
appearToClass: String, // 首次渲染时离开
duration: [Number, String, Object] // 动画时长
}
export default {
name: 'transition',
props: transitionProps,
abstract: true, // 标记为抽象组件,在vue内部不会参与父子组件的构建关系
render(h) { // 采用render函数编写,终于知道为啥叫h了
let children = this.$slots.default // 获取默认插槽内节点
if (!children) {
return
}
if (!children.length) {
return
}
if (children.length > 1) {
...插槽内只能有一个子节点
}
const mode = this.mode
if (mode && mode !== 'in-out' && mode !== 'out-in') {
...mode只能是in-out或out-in
}
const child = children[0] // 子节点对应VNode
const id = `__transition-${this._uid}-`
child.key = child.key == null // 为子节点的VNode添加key属性
? child.isComment // 注释节点
? id + 'comment'
: id + child.tag
: isPrimitive(child.key) // 原始值
? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key)
: child.key
(child.data || (child.data = {})).transition = extractTransitionData(this)
// 核心!将props和钩子函数赋给子节点的transition属性,表示是一个经过transition组件渲染的VNode
return child
}
}
export function extractTransitionData(comp) { // 赋值函数
const data = {}
const options = comp.$options
for (const key in options.propsData) { // transition组件接收到的props
data[key] = comp[key]
}
const listeners = options._parentListeners // 注册在transition组件上的钩子方法
for (const key in listeners) {
data[key] = listeners[key]
}
return data
}
通过以上代码我们知道了,transition组件主要是做两件事情,首先为渲染子节点的VNode添加key属性,然后是在它的data属性下添加一个transition属性,表示这是一个经过transition组件渲染的VNode,在之后path创建真实Dom的过程中再另外处理。
我们首先重点来看Css类名实现方式的原理,现在已经拿到对应的VNode,现在就需要创建成真实的Dom,在path的过程中,Dom上的style、css、attr等属性都是分成的模块进行创建,这些模块都有各自的钩子函数,例如有created、update、insert函数,部分模块各有不同,表示在某个时间段做某件事。transition也不例外,首先会执行created钩子。我们知道,transition组件是分为enter和leave状态的,先看下enter状态:
export function enter (vnode) { // 参数为组件插槽内的VNode
const el = vnode.elm // 对应真实节点
const data = resolveTransition(vnode.data.transition) // 扩展属性
// data包含了传入的props以及扩展的6个class属性
if (isUndef(data)) { // 如果不是transition渲染的vnode,再见
return
}
...
}
export function resolveTransition (def) { // 扩展属性
const res = {}
extend(res, autoCssTransition(def.name || 'v')) // class对象扩展到空对象res上
extend(res, def) // 将def上的属性扩展到res对象上
return res
}
const autoCssTransition (name) { // 生成包含6个需要使用到的class对象
return {
enterClass: `${name}-enter`,
enterToClass: `${name}-enter-to`,
enterActiveClass: `${name}-enter-active`,
leaveClass: `${name}-leave`,
leaveToClass: `${name}-leave-to`,
leaveActiveClass: `${name}-leave-active`
}
})
执行enter,首先继续往transition属性上扩展6个之后会使用的class名,我们接着往下看:
export function enter (vnode) { // 参数为组件插槽的内的VNode
...
const { // 解构出需要的参数
enterClass,
enterToClass,
enterActiveClass,
appearClass,
appearActiveClass,
appearToClass,
css,
type
// ...省略其他参数
} = data
const isAppear = !context._isMounted || !vnode.isRootInsert
// _isMounted表示组件是否mounted
// isRootInsert表示是否根节点插入
if (isAppear && !appear && appear !== '') {
// 如果没有配置appear属性,也是第一次渲染的情况直接退出,没有动画效果
return
}
const startClass = isAppear && appearClass // 如果有定义appear且有对应的appearClass
? appearClass // 执行定义的appearClass
: enterClass // 否则还是执行enterClass
const activeClass = isAppear && appearActiveClass
? appearActiveClass
: enterActiveClass
const toClass = isAppear && appearToClass
? appearToClass
: enterToClass
...
}
接下来是取出props里以及扩展的class值,用于之后使用。然后是appear的实现原理,如果还没有mounted以及不是根节点插入,且有定义appear属性,则使用appearClass完整执行一次enter状态的函数,否则没有动画直接渲染。接下来是最核心的实现过程。
export function enter (vnode) {
...
const expectsCSS = css !== false && !isIE9 // 没有显性的指明不执行css动画
const cb = once(() => { // 定义只会执行一次的cb函数,只是定义并不执行
if (expectsCSS) {
removeTransitionClass(el, toClass) // 移除toClass
removeTransitionClass(el, activeClass) // 移除activeClass
}
})
if (expectsCSS) {
addTransitionClass(el, startClass) // 添加startClass
addTransitionClass(el, activeClass) // 添加activeClass
nextFrame(() => { // requestAnimationFrame的封装,下一帧浏览器渲染回调时执行
removeTransitionClass(el, startClass) // 移除startClass
addTransitionClass(el, toClass) // 添加toClass
whenTransitionEnds(el, type, cb)
// 浏览器过渡结束事件transitionend或animationend之后执行cb,移除toClass和activeClass
})
}
}
首先定义一个cb函数,这个函数被once函数包裹,它的作用是只让里面的函数执行一次,当然这个cb只是定义了,并不会执行。接下来同步的为当前真实节点添加startClass和activeClass,也就是我们熟悉的v-enter和v-enter-active;之后在requestAnimationFrame也就是浏览器渲染的下一帧移除startClass并添加toClass,也就是v-enter-to;最后执行whenTransitionEnds方法,这个方法是监听浏览器的动画结束事件,也就是transitionend或animationend事件,表示v-enter-active内定义的动画或过渡结束了,结束后执行上面定义cb,在这个函数里面移除toClass和activeClass。
不难发现其实enter状态的这个函数它主要做的事情就是管理v-enter/v-enter-active/v-enter-to这三个class的添加和删除,具体的动画是用户定义的。
很自然的我们能想到,leave状态的函数就是管理的另外三个class的添加和删除,接下来只展示leave的核心代码:
export function leave (vnode) {
const cb = once(() => {
removeTransitionClass(el, leaveToClass) // 移除v-leave-to
removeTransitionClass(el, leaveActiveClass) // 移除v-leave-active
})
addTransitionClass(el, leaveClass) // 添加v-leave
addTransitionClass(el, leaveActiveClass) // 添加v-leave-active
nextFrame(() => { // 浏览器下一帧执行
removeTransitionClass(el, leaveClass) // 移除v-leave
addTransitionClass(el, leaveToClass) // 添加v-leave-to
whenTransitionEnds(el, type, cb) // 在动画结束的事件之后执行cb函数
})
}
源码里还有很多边界的情况的处理,如transition包裹又是抽象组件、执行enter时leave还没执行、上一个enter没执行完又执行enter等。感兴趣大家可自行去看完整源码实现,这里只对核心实现原理进行了分析。接下来我们看JavaScript钩子是怎么实现的。
在知道了Css类名方式的实现原理后,再理解JavaScript钩子的实现其实就不难了。钩子的实现方式也是分为enter和leave两种状态的,而且代码也是在这两种函数里,只是前面介绍Css方式忽略掉了,现在我们从钩子的实现视角重新来看这两个状态函数。首先还是看enter:
export function enter(vnode) {
if (isDef(el._leaveCb)) { // 如果进入enter时,_leaveCb没执行,立刻执行
el._leaveCb.cancelled = true // 执行了_leaveCb的标记位
el._leaveCb() // cb._leaveCb执行后会变成null
}
// el._leaveCb是leave状态里定义的cb函数,表示的是leave状态的回调函数
// 看到下面的enter的cb定义就会知道怎么肥事
const {
beforeEnter,
enter,
afterEnter,
enterCancelled,
duration
... 其他参数
} = data
const userWantsControl = getHookArgumentsLength(enter) // 传入enter钩子
// 如果钩子里enter函数的参数大于1,说明有传入done函数,表示用户想要自己控制
// 这也是为什么enter里动画结束后需要调用done函数
const cb = el._enterCb = once(() => { // 这里定义了el._enterCb函数,对应leave里就是el._leaveCb
if (cb.cancelled) { // 如果在leave的状态里,enter状态的cb函数没执行,则执行enterCancelled钩子
enterCancelled && enterCancelled(el)
} else {
afterEnter && afterEnter(el) // 否则正常的执行afterEnter钩子
}
el._enterCb = null // 执行后el._enterCb就是null了
... 省略css逻辑相关
})
mergeVNodeHook(vnode, 'insert', () => { // 将函数体插入到insert钩子内,在path中模块的created之后执行的钩子
...
enter && enter(el, cb) // 执行enter钩子,传入cb,这里的cb也就是对应enter钩子里的done函数
})
beforeEnter && beforeEnter(el)
nextFrame(() => {
if (!userWantsControl) { // 如果用户不想控制
if (duration) { // 如果有指定合法的过渡时间参数
setTimeout(cb, duration) // setTimeout之后执行cb
} else {
whenTransitionEnds(el, type, cb) // 浏览器过渡结束之后的事件之后执行
}
}
})
}
以上代码就是JavaScript钩子实现的原理,这里一定要注意它们的执行顺序:
leave状态还是只贴出核心代码,供大家和enter比对,它们的区别不是很大:
export function leave(vnode) {
const {
beforeLeave,
leave,
afterLeave,
duration
... 省略其他参数
} = data
const cb = once(() => {
afterLeave && afterLeave(el)
...
})
beforeLeave && beforeLeave(el)
nextFrame(() => {
if (!userWantsControl) { // 用户不想控制
if (isValidDuration(duration)) {
setTimeout(cb, duration)
} else {
whenTransitionEnds(el, type, cb)
}
}
})
leave && leave(el, cb) // 用户想控制这里执行done
}
leave状态里钩子的执行顺序就是beforeLeave、leave、afterLeave。
至此,transition内置组件的两种实现原理就全部解析完了。源码里考虑的边界情况会多很多,需要更加全面的了解,则需要看源码了。
笔者看完这个transition原理后有点小失望,原来并不能让我成为动画高手,最重要的还是Css的那些动画知识,可见地基打牢的重要性!
最后还是以一道面试官可能会问到的题目作为结束,因为我真的被问到过。
面试官微笑而又不失礼貌的问道:
怼回去:
BFC已经是一个耳听熟闻的词语了,网上有许多关于 BFC 的文章,介绍了如何触发 BFC 以及 BFC 的一些用处(如清浮动,防止 margin 重叠等)。BFC直译为\"块级格式化上下文\"。它是一个独立的渲染区域,只有Block-level box参与
作为前端,我们每天都在与CSS打交道,那么CSS的原理是什么呢?开篇,我们还是不厌其烦的回顾一下浏览器的渲染过程,学会使用永远都是最基本的标准,但是懂得原理,你才能触类旁通,超越自我。
做了一些研究,我发现了函数式编程概念,如不变性和纯函数。 这些概念使你能够构建无副作用的功能,而函数式编程的一些优点,也使得系统变得更加容易维护。我将通过 JavaScript 中的大量代码示例向您详细介绍函数式编程和一些重要概念。
如果你阅读过关于Angular 2变化检测的资料,那么你很可能听说过zone。Zone是一个从Dart中引入的特性并被Angular 2内部用来判断是否应该触发变化检测
updateComponent在更新渲染组件时,会访问1或多个数据模版插值,当访问数据时,将通过getter拦截器把componentUpdateWatcher作为订阅者添加到多个依赖中,每当其中一个数据有更新,将执行setter函数
一个继承自 Foo.prototype 的新对象被创建;使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数时,Foo 不带任何参数调用的情况
Http 缓存机制作为 web 性能优化的重要手段,对于从事 Web 开发的同学们来说,应该是知识体系库中的一个基础环节,同时对于有志成为前端架构师的同学来说是必备的知识技能。
HTTPS = HTTP + TLS/SSL,简单理解 HTTPS 其实就是在 HTTP 上面加多了一层安全层。HTTP 可以是 Http2.0 也可以是 Http1.1,不过现在 Http2.0 是强制要求使用 Https 的。使用非对称密钥(即公钥私钥))和对称密钥)(即共享密钥)相结合
HTTP是无状态协议。例:打开一个域名的首页,进而打开该域名的其他页面,服务器无法识别访问者。即同一浏览器访问同一网站,每次访问都没有任何关系。Cookie的原理是
Promise 必须为以下三种状态之一:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。一旦Promise 被 resolve 或 reject,不能再迁移至其他任何状态(即状态 immutable)。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!