如何给真实DOM设置样式?
Vue支持动态style和class,如:<div :class="{active: false}" :style="{color: "red"}"></div>
,如何将样式属性映射至真实DOM?
我们的编码目标是下面的demo能够成功渲染。
let v = new Vue({
el: '#app',
data () {
return {
color: "red"
}
},
render (h) {
return h('h1', {style: {color: this.color}}, 'hello world!')
}
})
setTimeout(() => {
v.color ='#000'
}, 2000)
样式如何映射至真实DOM
【Ts重构Vue】01-如何创建虚拟节点中分析了虚拟DOM映射过程,我们知道虚拟DOM转为真实DOM只有唯一的途径---patch(oldVnode, vnode)
,是否可以在这一步上进行一些操作,将样式更新到真实DOM上?
观察下图,完整patch过程主要涉及如下方法:
我们在方法内执行钩子函数,并将虚拟节点作为参数传入,那么就可以在节点的生命周期(创建、更新、销毁)进行各种操作,Vue支持如下钩子函数:create、destroy、insert、remove、update、prepatch、postpatch、init
。
回顾问题,我们需要设置和更新真实DOM的样式,所以需要用到create
和update
这两个钩子函数:
- 在
createElm
中创建真实DOM,创建完成后调用create-hook
为节点设置样式属性。同时进行赋值操作vnode.elm = 真实节点
,方便后续管理。 - 在
patchVnode
中对相同虚拟节点进行比较,执行update-hook
对节点的样式进行更新。 - 在节点销毁时,样式属性自然不会展示,此时不需要进行任何操作。
钩子函数是如何执行的?
钩子函数是如何存储和执行的呢?
首先声明变量cbs = {}
用于存储全局的钩子函数。
let cbs = {} as ModuleHooks
export function createPatcher(modules?: Array<Partial<Module>>) {
modules = isArray(modules) ? modules : []
for (let i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (let j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]]
if (isDef(hook)) {
;(cbs[hooks[i]] as Array<any>).push(hook)
}
}
}
return patch
}
接着调用createPatcher
方法,并传入styleModule
,返回patch方法用作节点映射。
import styleModule from './style'
const patch = createPatcher(styleModule)
// styleModule的形式如下。
styleModule = {
create: updateStyle,
update: updateStyle
}
上面模块的全局变量存储了钩子函数,接着在具体方法中执行。
在createElm
函数中,当创建完成真实DOM后,执行invokeCreateHook(vnode)
钩子函数:
function createElm(vnode: VNode): Node {
if (!isVNode(vnode)) return null
if (createComponent(vnode)) {
return vnode.elm
}
if (vnode.tag === '!') {
vnode.elm = webMethods.createComment(vnode.text!)
} else if (!vnode.tag) {
vnode.elm = webMethods.createText(vnode.text!)
} else {
vnode.elm = webMethods.createElement(vnode.tag!)
//hook-create
invokeCreateHook(vnode)
}
return vnode.elm
}
function invokeCreateHook(vnode: VNode) {
invokeCbHooks('create')(emptyNode, vnode)
let i: any = vnode.data.hook
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
在patchVnode
方法中,当新旧虚拟节点不同时,执行invokeCbHooks('update')(oldVnode, vnode)
钩子函数。
function patchNode(oldVnode: VNode, vnode: VNode) {
let i: any
const data = vnode.data,
oldCh = oldVnode.children,
ch = vnode.children,
elm = (vnode.elm = oldVnode.elm!)
vnode.componentInstance = oldVnode.componentInstance
if (oldVnode === vnode) return
invokeVnodeHooks(oldVnode, vnode, 'prepatch')
invokeCbHooks('update')(oldVnode, vnode)
if (oldCh) {
...
} else {
...
}
invokeVnodeHooks(oldVnode, vnode, 'postpatch')
}
function invokeCbHooks(hook: keyof Module) {
let hookHandler = cbs[hook]
return function(...args) {
for (let i = 0; i < hookHandler.length; ++i) {
hookHandler[i](...args)
}
}
}
在钩子函数中设置样式属性
在虚拟节点映射为真实DOM过程中,执行了create
和update
钩子函数,其本质上市执行了updateStyle
函数,
在updateStyle函数中,从vnode.elm属性上获取真实DOM对象,遍历新旧虚拟节点的样式属性,对真实DOM进行设置更新操作。
function updateStyle(oldVnode: VNode, vnode: VNode): void {
let cur: any,
name: string,
elm = vnode.elm,
oldStyle = oldVnode.data!.style,
style = vnode.data!.style
if (!oldStyle && !style) return
if (oldStyle === style) return
oldStyle = oldStyle || ({} as VNodeStyle)
style = style || ({} as VNodeStyle)
for (name in oldStyle) {
if (!style[name]) {
;(elm as any).style[name] = ''
}
}
for (name in style) {
;(elm as any).style[name] = style[name]
}
}
export default {
create: updateStyle,
update: updateStyle
}
Vue样式处理流程
在Vue实例化后,生成如下虚拟DOM,并渲染至页面:
{
tag: 'h1',
ele: 真实DOM节点,
data: {
style: {
color: 'red'
}
},
children: [
{
tag: '',
text: 'hello world'
}
]
}
当用户更新this.color = '#000'
后,触发watch.update
函数,从而执行this._update(this._render())
方法,生成新的虚拟DOM:
{
tag: 'h1',
ele: 真实DOM节点,
data: {
style: {
color: '#000'
}
},
children: [
{
tag: '',
text: 'hello world'
}
]
}
在虚拟DOM进行patch过程中,通过钩子函数调用了updateStyle方法,函数执行时更新了真实DOM的样式属性。
总结
核心模块仅关心自身逻辑,其他需求借助钩子函数实现。既方便不同平台柴艺华,又方便实现拓展。Vue的style/class等功能都是基于vnode-hook实现的。
杠精一下
vue/react都有生命周期,vnode映射过程有钩子函数,如何开发可拓展的框架?