VirtualDOM与diff(Vue实现)

写在前面

因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出。
文章的原地址:https://github.com/answershuto/learnVue。
在学习过程中,为Vue加上了中文的注释https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以对其他想学习Vue源码的小伙伴有所帮助。
可能会有理解存在偏差的地方,欢迎提issue指出,共同学习,共同进步。

VNode

在刀耕火种的年代,我们需要在各个事件方法中直接操作DOM来达到修改视图的目的。但是当应用一大就会变得难以维护。

那我们是不是可以把真实DOM树抽象成一棵以JavaScript对象构成的抽象树,在修改抽象树数据后将抽象树转化成真实DOM重绘到页面上呢?于是虚拟DOM出现了,它是真实DOM的一层抽象,用属性描述真实DOM的各个特性。当它发生变化的时候,就会去修改视图。

但是这样的JavaScript操作DOM进行重绘整个视图层是相当消耗性能的,我们是不是可以每次只更新它的修改呢?所以Vue.js将DOM抽象成一个以JavaScript对象为节点的虚拟DOM树,以VNode节点模拟真实DOM,可以对这颗抽象树进行创建节点、删除节点以及修改节点等操作,在这过程中都不需要操作真实DOM,只需要操作JavaScript对象,大大提升了性能。修改以后经过diff算法得出一些需要修改的最小单位,再将这些小单位的视图进行更新。这样做减少了很多不需要的DOM操作,大大提高了性能。

Vue就使用了这样的抽象节点VNode,它是对真实Dom的一层抽象,而不依赖某个平台,它可以是浏览器平台,也可以是weex,甚至是node平台也可以对这样一棵抽象Dom树进行创建删除修改等操作,这也为前后端同构提供了可能。

具体VNode的细节可以看VNode节点。

修改视图

周所周知,Vue通过数据绑定来修改视图,当某个数据被修改的时候,set方法会让闭包中的Dep调用notify通知所有订阅者Watcher,Watcher通过get方法执行vm._update(vm._render(), hydrating)。

这里看一下_update方法

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {const vm: Component = this/*如果已经该组件已经挂载过了则代表进入这个步骤是个更新的过程,触发beforeUpdate钩子*/if (vm._isMounted) {callHook(vm, 'beforeUpdate')}const prevEl = vm.$elconst prevVnode = vm._vnodeconst prevActiveInstance = activeInstanceactiveInstance = vmvm._vnode = vnode// Vue.prototype.__patch__ is injected in entry points// based on the rendering backend used./*基于后端渲染Vue.prototype.__patch__被用来作为一个入口*/if (!prevVnode) {// initial rendervm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */,vm.$options._parentElm,vm.$options._refElm)} else {// updatesvm.$el = vm.__patch__(prevVnode, vnode)}activeInstance = prevActiveInstance// update __vue__ reference/*更新新的实例对象的__vue__*/if (prevEl) {prevEl.__vue__ = null}if (vm.$el) {vm.$el.__vue__ = vm}// if parent is an HOC, update its $el as wellif (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {vm.$parent.$el = vm.$el}// updated hook is called by the scheduler to ensure that children are// updated in a parent's updated hook.}

_update方法的第一个参数是一个VNode对象,在内部会将该VNode对象与之前旧的VNode对象进行patch

什么是patch呢?

patch

patch将新老VNode节点进行比对,然后将根据两者的比较结果进行最小单位地修改视图,而不是将整个视图根据新的VNode重绘。patch的核心在于diff算法,这套算法可以高效地比较viturl dom的变更,得出变化以修改视图。

那么patch如何工作的呢?

首先说一下patch的核心diff算法,diff算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当高效的算法。

img
img

img
img

着两张图代表旧的VNode与新VNode进行patch的过程,他们只是在同层级的VNode之间进行比较得到变化(第二张图中相同颜色的方块代表互相进行比较的VNode节点),然后修改变化的视图,所以十分高效。

让我们看一下patch的代码。

  /*createPatchFunction的返回值,一个patch函数*/return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {/*vnode不存在则直接调用销毁钩子*/if (isUndef(vnode)) {if (isDef(oldVnode)) invokeDestroyHook(oldVnode)return}let isInitialPatch = falseconst insertedVnodeQueue = []if (isUndef(oldVnode)) {// empty mount (likely as component), create new root element/*oldVnode未定义的时候,其实也就是root节点,创建一个新的节点*/isInitialPatch = truecreateElm(vnode, insertedVnodeQueue, parentElm, refElm)} else {/*标记旧的VNode是否有nodeType*//*Github:https://github.com/answershuto*/const isRealElement = isDef(oldVnode.nodeType)if (!isRealElement && sameVnode(oldVnode, vnode)) {// patch existing root node/*是同一个节点的时候直接修改现有的节点*/patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)} else {if (isRealElement) {// mounting to a real element// check if this is server-rendered content and if we can perform// a successful hydration.if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {/*当旧的VNode是服务端渲染的元素,hydrating记为true*/oldVnode.removeAttribute(SSR_ATTR)hydrating = true}if (isTrue(hydrating)) {/*需要合并到真实DOM上*/if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {/*调用insert钩子*/invokeInsertHook(vnode, insertedVnodeQueue, true)return oldVnode} else if (process.env.NODE_ENV !== 'production') {warn('The client-side rendered virtual DOM tree is not matching '  'server-rendered content. This is likely caused by incorrect '  'HTML markup, for example nesting block-level elements inside '  '<p>, or missing <tbody>. Bailing hydration and performing '  'full client-side render.')}}// either not server-rendered, or hydration failed.// create an empty node and replace it/*如果不是服务端渲染或者合并到真实DOM失败,则创建一个空的VNode节点替换它*/oldVnode = emptyNodeAt(oldVnode)}// replacing existing element/*取代现有元素*/const oldElm = oldVnode.elmconst parentElm = nodeOps.parentNode(oldElm)createElm(vnode,insertedVnodeQueue,// extremely rare edge case: do not insert if old element is in a// leaving transition. Only happens when combining transition  // keep-alive   HOCs. (#4590)oldElm._leaveCb ? null : parentElm,nodeOps.nextSibling(oldElm))if (isDef(vnode.parent)) {// component root element replaced.// update parent placeholder node element, recursively/*组件根节点被替换,遍历更新父节点element*/let ancestor = vnode.parentwhile (ancestor) {ancestor.elm = vnode.elmancestor = ancestor.parent}if (isPatchable(vnode)) {/*调用create回调*/for (let i = 0; i < cbs.create.length;   i) {cbs.create[i](emptyNode, vnode.parent)}}}if (isDef(parentElm)) {/*移除老节点*/removeVnodes(parentElm, [oldVnode], 0, 0)} else if (isDef(oldVnode.tag)) {/*Github:https://github.com/answershuto*//*调用destroy钩子*/invokeDestroyHook(oldVnode)}}}/*调用insert钩子*/invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)return vnode.elm}

从代码中不难发现,当oldVnode与vnode在sameVnode的时候才会进行patchVnode,也就是新旧VNode节点判定为同一节点的时候才会进行patchVnode这个过程,否则就是创建新的DOM,移除旧的DOM。

怎么样的节点算sameVnode呢?

sameVnode

我们来看一下sameVnode的实现。

/*判断两个VNode节点是否是同一个节点,需要满足以下条件key相同tag(当前节点的标签名)相同isComment(是否为注释节点)相同是否data(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息)都有定义当标签是<input>的时候,type必须相同
*/
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))
}// Some browsers do not support dynamically changing type for <input>
// so they need to be treated as different nodes
/*判断当标签是<input>的时候,type是否相同某些浏览器不支持动态修改<input>类型,所以他们被视为不同类型
*/
function sameInputType (a, b) {if (a.tag !== 'input') return truelet iconst typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.typeconst typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.typereturn typeA === typeB
}

当两个VNode的tag、key、isComment都相同,并且同时定义或未定义data的时候,且如果标签为input则type必须相同。这时候这两个VNode则算sameVnode,可以直接进行patchVnode操作。

patchVnode

还是先来看一下patchVnode的代码。

  /*patch VNode节点*/function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {/*两个VNode节点相同则直接返回*/if (oldVnode === vnode) {return}// reuse element for static trees.// note we only do this if the vnode is cloned -// if the new node is not cloned it means the render functions have been// reset by the hot-reload-api and we need to do a proper re-render./*如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换elm以及componentInstance即可。*/if (isTrue(vnode.isStatic) &&isTrue(oldVnode.isStatic) &&vnode.key === oldVnode.key &&(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {vnode.elm = oldVnode.elmvnode.componentInstance = oldVnode.componentInstancereturn}let iconst data = vnode.dataif (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {/*i = data.hook.prepatch,如果存在的话,见"./create-component componentVNodeHooks"。*/i(oldVnode, vnode)}const elm = vnode.elm = oldVnode.elmconst oldCh = oldVnode.childrenconst ch = vnode.childrenif (isDef(data) && isPatchable(vnode)) {/*调用update回调以及update钩子*/for (i = 0; i < cbs.update.length;   i) cbs.update[i](oldVnode, vnode)if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)}/*如果这个VNode节点没有text文本时*/if (isUndef(vnode.text)) {if (isDef(oldCh) && isDef(ch)) {/*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)} else if (isDef(ch)) {/*如果老节点没有子节点而新节点存在子节点,先清空elm的文本内容,然后为当前节点加入子节点*/if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)} else if (isDef(oldCh)) {/*当新节点没有子节点而老节点有子节点的时候,则移除所有ele的子节点*/removeVnodes(elm, oldCh, 0, oldCh.length - 1)} else if (isDef(oldVnode.text)) {/*当新老节点都无子节点的时候,只是文本的替换,因为这个逻辑中新节点text不存在,所以直接去除ele的文本*/nodeOps.setTextContent(elm, '')}} else if (oldVnode.text !== vnode.text) {/*当新老节点text不一样时,直接替换这段文本*/nodeOps.setTextContent(elm, vnode.text)}/*调用postpatch钩子*/if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)}}

patchVnode的规则是这样的:

1.如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换elm以及componentInstance即可。

2.新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren,这个updateChildren也是diff的核心。

3.如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。

4.当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。

5.当新老节点都无子节点的时候,只是文本的替换。

updateChildren

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {let oldStartIdx = 0let newStartIdx = 0let oldEndIdx = oldCh.length - 1let oldStartVnode = oldCh[0]let oldEndVnode = oldCh[oldEndIdx]let newEndIdx = newCh.length - 1let newStartVnode = newCh[0]let newEndVnode = newCh[newEndIdx]let oldKeyToIdx, idxInOld, elmToMove, refElm// removeOnly is a special flag used only by <transition-group>// to ensure removed elements stay in correct relative positions// during leaving transitionsconst canMove = !removeOnlywhile (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {if (isUndef(oldStartVnode)) {oldStartVnode = oldCh[  oldStartIdx] // Vnode has been moved left} else if (isUndef(oldEndVnode)) {oldEndVnode = oldCh[--oldEndIdx]} else if (sameVnode(oldStartVnode, newStartVnode)) {/*前四种情况其实是指定key的时候,判定为同一个VNode,则直接patchVnode即可,分别比较oldCh以及newCh的两头节点2*2=4种情况*/patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)oldStartVnode = oldCh[  oldStartIdx]newStartVnode = newCh[  newStartIdx]} else if (sameVnode(oldEndVnode, newEndVnode)) {patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)oldEndVnode = oldCh[--oldEndIdx]newEndVnode = newCh[--newEndIdx]} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved rightpatchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))oldStartVnode = oldCh[  oldStartIdx]newEndVnode = newCh[--newEndIdx]} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved leftpatchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)oldEndVnode = oldCh[--oldEndIdx]newStartVnode = newCh[  newStartIdx]} else {/*生成一个key与旧VNode的key对应的哈希表(只有第一次进来undefined的时候会生成,也为后面检测重复的key值做铺垫)比如childre是这样的 [{xx: xx, key: 'key0'}, {xx: xx, key: 'key1'}, {xx: xx, key: 'key2'}]  beginIdx = 0   endIdx = 2  结果生成{key0: 0, key1: 1, key2: 2}*/if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)/*如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到则返回这个节点的idxInOld(即第几个节点,下标)*/idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : nullif (isUndef(idxInOld)) { // New element/*newStartVnode没有key或者是该key没有在老节点中找到则创建一个新的节点*/createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)newStartVnode = newCh[  newStartIdx]} else {/*获取同key的老节点*/elmToMove = oldCh[idxInOld]/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && !elmToMove) {/*如果elmToMove不存在说明之前已经有新节点放入过这个key的DOM中,提示可能存在重复的key,确保v-for的时候item有唯一的key值*/warn('It seems there are duplicate keys that is causing an update error. '  'Make sure each v-for item has a unique key.')}if (sameVnode(elmToMove, newStartVnode)) {/*Github:https://github.com/answershuto*//*如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode*/patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)/*因为已经patchVnode进去了,所以将这个老节点赋值undefined,之后如果还有新节点与该节点key相同可以检测出来提示已有重复的key*/oldCh[idxInOld] = undefined/*当有标识位canMove实可以直接插入oldStartVnode对应的真实DOM节点前面*/canMove && nodeOps.insertBefore(parentElm, newStartVnode.elm, oldStartVnode.elm)newStartVnode = newCh[  newStartIdx]} else {// same key but different element. treat as new element/*当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点*/createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)newStartVnode = newCh[  newStartIdx]}}}}if (oldStartIdx > oldEndIdx) {/*全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多,所以这时候多出来的新节点需要一个一个创建出来加入到真实DOM中*/refElm = isUndef(newCh[newEndIdx   1]) ? null : newCh[newEndIdx   1].elmaddVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)} else if (newStartIdx > newEndIdx) {/*如果全部比较完成以后发现newStartIdx > newEndIdx,则说明新节点已经遍历完了,老节点多余新节点,这个时候需要将多余的老节点从真实DOM中移除*/removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)}}

直接看源码可能比较难以滤清其中的关系,我们通过图来看一下。

img
img

首先,在新老两个VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢。当oldStartIdx <= oldEndIdx或者newStartIdx <= newEndIdx时结束循环。

索引与VNode节点的对应关系:
oldStartIdx => oldStartVnode
oldEndIdx => oldEndVnode
newStartIdx => newStartVnode
newEndIdx => newEndVnode

在遍历中,如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点。

首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两比较一共有2*2=4种比较方法。

当新老VNode节点的start或者end满足sameVnode时,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接将该VNode节点进行patchVnode即可。

img
img

如果oldStartVnode与newEndVnode满足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。

这时候说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。

img
img

如果oldEndVnode与newStartVnode满足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。

这说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面。

img
img

如果以上情况均不符合,则通过createKeyToOldIdx会得到一个oldKeyToIdx,里面存放了一个key为旧的VNode,value为对应index序列的哈希表。从这个哈希表中可以找到是否有与newStartVnode一致key的旧的VNode节点,如果同时满足sameVnode,patchVnode的同时会将这个真实DOM(elmToMove)移动到oldStartVnode对应的真实DOM的前面。

img
img

当然也有可能newStartVnode在旧的VNode节点找不到一致的key,或者是即便key相同却不是sameVnode,这个时候会调用createElm创建一个新的DOM节点。

img
img

到这里循环已经结束了,那么剩下我们还需要处理多余或者不够的真实DOM节点。

1.当结束时oldStartIdx > oldEndIdx,这个时候老的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,也就是比真实DOM多,需要将剩下的(也就是新增的)VNode节点插入到真实DOM节点中去,此时调用addVnodes(批量调用createElm的接口将这些节点加入到真实DOM中去)。

img
img

2。同理,当newStartIdx > newEndIdx时,新的VNode节点已经遍历完了,但是老的节点还有剩余,说明真实DOM节点多余了,需要从文档中删除,这时候调用removeVnodes将这些多余的真实DOM删除。

img
img

DOM操作

由于Vue使用了虚拟DOM,所以虚拟DOM可以在任何支持JavaScript语言的平台上操作,譬如说目前Vue支持的浏览器平台或是weex,在虚拟DOM的实现上是一致的。那么最后虚拟DOM如何映射到真实的DOM节点上呢?

Vue为平台做了一层适配层,浏览器平台见/platforms/web/runtime/node-ops.js以及weex平台见/platforms/weex/runtime/node-ops.js。不同平台之间通过适配层对外提供相同的接口,虚拟DOM进行操作真实DOM节点的时候,只需要调用这些适配层的接口即可,而内部实现则不需要关心,它会根据平台的改变而改变。

现在又出现了一个问题,我们只是将虚拟DOM映射成了真实的DOM。那如何给这些DOM加入attr、class、style等DOM属性呢?

这要依赖于虚拟DOM的生命钩子。虚拟DOM提供了如下的钩子函数,分别在不同的时期会进行调用。

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。里面有对attr、class、props、events、style以及transition(过渡状态)的DOM属性进行操作。

以attr为例,代码很简单。

/* @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属性即可。

关于

作者:染陌

Email:answershuto@gmail.com or answershuto@126.com

Github: https://github.com/answershuto

Blog:http://answershuto.github.io/

知乎专栏:https://zhuanlan.zhihu.com/ranmo

掘金: https://juejin.im/user/58f87ae844d9040069ca7507

osChina:https://my.oschina.net/u/3161824/blog

转载请注明出处,谢谢。

欢迎关注我的公众号


更多专业前端知识,请上 【猿2048】www.mk2048.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/366713.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用Java的RESTful Web服务

REST代表“代表性状态转移”&#xff0c;由Roy Fielding于2000年在其论文“建筑风格和基于网络的软件体系结构设计”中首次提出。 REST是一种建筑风格。 HTTP是一种协议&#xff0c;其中包含一组REST体系结构约束。 REST基础 REST中的所有内容都被视为资源。 每个资源都由UR…

windows npm安装webpack

Webpack: Webpack 是一个前端资源加载/打包工具。 它将根据模块的依赖关系进行静态分析&#xff0c;然后将这些模块按照指定的规则生成对应的静态资源。 参考下图&#xff1a; 安装Webpack: 1.首先需要安装node.js&#xff08;npm&#xff09; 下载地址&#xff1a;node.js dow…

JavaFX中的塔防

我想长时间使用我的游戏引擎来编写《塔防》游戏&#xff0c;但是由于另一个小组努力创建JavaFX《塔防》游戏&#xff0c;所以我认为我宁愿创建另一款游戏。 从邮件列表中&#xff0c;我了解到不再开发其他游戏。 因此&#xff0c;我决定尝试一下。 塔防是一款非常适合基于图块…

CSS pointer-events属性的使用

楔子 在前端的开发中&#xff0c;我们都是直接与用户接触&#xff0c;应该尽量让用户感到操作畅快愉悦&#xff0c;获得类似native的感觉。其中动画是最常用的方法。 这里的需求是&#xff0c;弹层的设计&#xff0c;这个弹层希望可以像 native 上的弹层一样&#xff0c;点击…

深入理解JavaScript之Event Loop

前言 最近阅读《高性能JavaScript》时&#xff0c;第六章谈到“通过定时器将JavaScript执行代码的控制权先让给浏览器用于更新UI状态&#xff0c;然后再将控制权交回给JavaScript代码&#xff0c;这样就可以使得页面更为流畅”&#xff0c;就联想到了之前理解的事件循环。 这…

使用EasyPoi导出Excel

Excel模板来自自己写死的一个excel模板&#xff0c;相当于是用户查询数据&#xff0c;数据填充到一个模板的Excel里&#xff0c;再导出Excel /*创建模板*/String a request.getSession().getServletContext().getRealPath("/resource/河南能源化工集团安全监控系统联网系统…

Hazelcast入门

7月&#xff0c;我写了一个博客向Java开发人员介绍erlang&#xff0c;重点介绍了这两种语言之间的一些异同。 erlang虚拟机具有许多令人印象深刻的内置功能&#xff0c;其中之一是它们独立于位置并且可以互相通信。 这意味着可以通过编写很少的代码行在VM之间同步数据。 如果您…

android手机最低内存,原神手机端需要哪些配置 手机端最低配置要求介绍

原神是一款由米哈游自主研发的全新开放世界冒险游戏&#xff0c;游戏最近迎来了pc端的首次测试&#xff0c;而且在不久之后就会开启原神手机端的公测版本&#xff0c;那么手机端需要什么配置呢&#xff1f;小编带来了详细的介绍。移动端预下载&#xff1a;9月25日下午16&#x…

AnswerOpenCV一周佳作欣赏(0615-0622)

一、How to make auto-adjustments(brightness and contrast) for image Android Opencv Image Correctionim using OpenCV for Android. I would like to know,how to make image correction(auto adjustments of brightness/contrast) for image(bitmap) in android via Open…

所有其他指标均无用

对于队列&#xff0c;无论是实现为JMS &#xff0c;数据库表&#xff08;即Ruby的Delayed :: Job用于队列的什么&#xff09;&#xff0c;甚至是Amazon的SQS &#xff0c;用于评估队列状态的最常见指标是其长度。 从本质上讲&#xff0c;可以基于在任何给定时间队列中驻留多少消…

类似苹果数据线的android,除了常见的安卓、苹果、Type-c,还有哪些你不知道的手机数据线?...

随着智能手机日益发展&#xff0c;辅助智能手机的数据线配件也越来越多样。现在我们最常见的无非就是标准Micro usb口、正反随便插的Type-c接口、还有苹果Lightning数据线&#xff0c;那么除了这些类型数据线&#xff0c;你知道如今市面上还有哪些更方便好用的手机数据线吗&…

canvas入门实战--邀请卡生成与下载

1.前言 写了很多的javascript和css3的文章&#xff0c;是时候写一篇canvas的了。canvas是html5提供的一个新的功能&#xff01;至于作用&#xff0c;就是一个画布。然后画笔就是javascript。canvas的用途非常的广&#xff0c;特别是html5游戏以及数据可视化这两个方面。现在can…

Apache ActiveMQ 5.9发布

Apache ActiveMQ团队刚刚发布了新的ActiveMQ 5.9版本 。 Apache ActiveMQ 5.9发布 自从先前的5.8版本以来&#xff0c;此版本是8个月的辛苦工作。 在此发行版中&#xff0c;我们将像往常一样对代理进行增强&#xff0c;并使用最新的协议&#xff08;例如AMQP和MQTT&#xff…

android 美颜录像,Android 关于美颜/滤镜 利用PBO从OpenGL录制视频

前言上次我写了一遍文章《Android 关于美颜/滤镜 从OpenGl录制视频的一种方案》&#xff0c;里面利用ImageReader来从获取Surface上获取数据&#xff0c;但是经过熊皮皮的提醒&#xff0c;我发现多PBO的确可以实现跟ImageReader一样的效果&#xff0c;并且版本要求仅为Android4…

Java对象到对象映射器

我在该项目上使用了Dozer一段时间。 但是&#xff0c;最近我遇到了一个非常有趣的错误&#xff0c;它促使我环顾四周&#xff0c;并尝试使用其他“对象到对象”映射器。 这是我找到的工具列表&#xff1a; 推土机&#xff1a;推土机是Java Bean到Java Bean的映射器&#xff…

android媒体播放框架,Android 使用超简单的多媒体播放器JiaoZiVideoPlayer

在之前的项目中用到了视频播放的功能&#xff0c;在网上看了看使用了大家用的比较多的一个开源项目JiaoZiVideo可以迅速的实现视频播放的相关功能。JiaoZiVideo的简单使用集成了JiaoZiVideo后仅需这几行代码就可以实现播放视频JZVideoPlayerStandard jzVideoPlayerStandard (J…

送福利:ROKID 语音开发板免费送,开启你的物联网之旅

都让一让&#xff0c;我说个事情&#xff1a;掘金联合 Rokid 开发者社区给大家发福利啦&#xff01; 掘金联合 Rokid 开发者社区为大家准备了一些福利&#xff0c;只要秀出你的 skill 和技术栈&#xff0c;就有可能获得 Rokid 全栈语音智能开发套件。 ? Rokid开箱试用活动 活…

如何使用JavaScript控制台改进工作流程

作为Web开发人员&#xff0c;很有必要了解如何调试代码。后台开发我们经常使用外部库来记录日志&#xff0c;并在某些情况下格式化显示日志&#xff0c;前端我们会使用断点和控制台&#xff0c;但是我们浏览器的控制台比我们想象的要强大得多。 当我们考虑控制台时&#xff0c…

select、poll、epoll之间的区别总结[整理]

原文:https://www.cnblogs.com/Anker/p/3265058.html 好文章收藏下&#xff0c;慢慢品味 select&#xff0c;poll&#xff0c;epoll都是IO多路复用的机制。I/O多路复用就通过一种机制&#xff0c;可以监视多个描述符&#xff0c;一旦某个描述符就绪&#xff08;一般是读就绪或者…

JPA(七):映射关联关系------映射双向多对一的关联关系

映射双向多对一的关联关系 修改Customer.java package com.dx.jpa.singlemanytoone;import java.util.Date; import java.util.HashSet; import java.util.Set;import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; impo…