由于平台字数限制放不下了,接上文。
## DOM 操作
由于 Vue 使用了虚拟 DOM,所以虚拟 DOM 可以在任何支持 JavaScript 语言的平台上操作,譬如说目前 Vue 支持的浏览器平台或是 weex,在虚拟 DOM 的实现上是一致的。那么最后虚拟 DOM 如何映射到真实的 DOM 节点上呢?
Vue 为平台做了一层适配层,浏览器平台见[/platforms/web/runtime/node-ops.js](
https://github.com/answershuto/learnVue/blob/master/vue-src/platforms/web/runtime/node-ops.js)以及 weex 平台见[/platforms/weex/runtime/node-ops.js](
https://github.com/answershuto/learnVue/blob/master/vue-src/platforms/weex/runtime/node-ops.js)。不同平台之间通过适配层对外提供相同的接口,虚拟 DOM 进行操作真实 DOM 节点的时候,只需要调用这些适配层的接口即可,而内部实现则不需要关心,它会根据平台的改变而改变。
现在又出现了一个问题,我们只是将虚拟 DOM 映射成了真实的 DOM。那如何给这些 DOM 加入 attr、class、style 等 DOM 属性呢?
这要依赖于虚拟 DOM 的生命钩子。虚拟 DOM 提供了如下的钩子函数,分别在不同的时期会进行调用。
```JavaScript
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
/*构建 cbs 回调函数,web 平台上见 /platforms/web/runtime/modules*/
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
```
同理,也会根据不同平台有自己不同的实现,我们这里以 Web 平台为例。Web 平台的钩子函数见[/platforms/web/runtime/modules](
https://github.com/answershuto/learnVue/tree/master/vue-src/platforms/web/runtime/modules)。里面有对 attr、class、props、events、style 以及 transition (过渡状态)的 DOM 属性进行操作。
以 attr 为例,代码很简单。
```JavaScript
/* @
flow */
import { isIE9 } from 'core/util/env'
import {
extend,
isDef,
isUndef
} from 'shared/util'
import {
isXlink,
xlinkNS,
getXlinkProp,
isBooleanAttr,
isEnumeratedAttr,
isFalsyAttrValue
} from 'web/util/index'
/*更新 attr*/
function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) {
/*如果旧的以及新的 VNode 节点均没有 attr 属性,则直接返回*/
if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) {
return
}
let key, cur, old
/*VNode 节点对应的 Dom 实例*/
const elm = vnode.elm
/*旧 VNode 节点的 attr*/
const oldAttrs = oldVnode.data.attrs || {}
/*新 VNode 节点的 attr*/
let attrs: any = vnode.data.attrs || {}
// clone observed objects, as the user probably wants to mutate it
/*如果新的 VNode 的 attr 已经有__ob__(代表已经被 Observe 处理过了), 进行深拷贝*/
if (isDef(attrs.__ob__)) {
attrs = vnode.data.attrs = extend({}, attrs)
}
/*遍历 attr,不一致则替换*/
for (key in attrs) {
cur = attrs[key]
old = oldAttrs[key]
if (old !== cur) {
setAttr(elm, key, cur)
}
}
// #4391: in IE9, setting type can reset value for input[type=radio]
/* istanbul ignore if */
if (isIE9 && attrs.value !== oldAttrs.value) {
setAttr(elm, 'value', attrs.value)
}
for (key in oldAttrs) {
if (isUndef(attrs[key])) {
if (isXlink(key)) {
elm.removeAttributeNS(xlinkNS, getXlinkProp(key))
} else if (!isEnumeratedAttr(key)) {
elm.removeAttribute(key)
}
}
}
}
/*设置 attr*/
function setAttr (el: Element, key: string, value: any) {
if (isBooleanAttr(key)) {
// set attribute for blank value
// e.g. <option disabled>Select one</option>
if (isFalsyAttrValue(value)) {
el.removeAttribute(key)
} else {
el.setAttribute(key, key)
}
} else if (isEnumeratedAttr(key)) {
el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true')
} else if (isXlink(key)) {
if (isFalsyAttrValue(value)) {
el.removeAttributeNS(xlinkNS, getXlinkProp(key))
} else {
el.setAttributeNS(xlinkNS, key, value)
}
} else {
if (isFalsyAttrValue(value)) {
el.removeAttribute(key)
} else {
el.setAttribute(key, value)
}
}
}
export default {
create: updateAttrs,
update: updateAttrs
}
```
attr 只需要在 create 以及 update 钩子被调用时更新 DOM 的 attr 属性即可。