【Ts重构Vue】03-如何给真实DOM设置样式

1,821 阅读4分钟

如何给真实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的样式,所以需要用到createupdate这两个钩子函数:

  1. createElm中创建真实DOM,创建完成后调用create-hook为节点设置样式属性。同时进行赋值操作vnode.elm = 真实节点,方便后续管理。
  2. patchVnode中对相同虚拟节点进行比较,执行update-hook对节点的样式进行更新。
  3. 在节点销毁时,样式属性自然不会展示,此时不需要进行任何操作。

钩子函数是如何执行的?

钩子函数是如何存储和执行的呢?

首先声明变量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过程中,执行了createupdate钩子函数,其本质上市执行了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映射过程有钩子函数,如何开发可拓展的框架?

系列文章

【Ts重构Vue】00-Ts重构Vue前言

【Ts重构Vue】01-如何创建虚拟节点

【Ts重构Vue】02-数据如何驱动视图变化

【Ts重构Vue】03-如何给真实DOM设置样式

【Ts重构Vue】04-异步渲染

【Ts重构Vue】05-实现computed和watch功能