模板tamplate
经过parse
,optimize
,generate
等一些列操作之后,把AST
转为render function code
进而生成虚拟VNode
,模板编译阶段基本已经完成了,那么这一章,我们来探讨一下Vue
中的一个算法策略–dom diff
首先来介绍下什么叫dom diff
我们经过前面的章节学习已经知道,要知道渲染真实DOM
的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染
到真实dom上会引起整个dom树
的重绘
和重排
,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?
为了解决这个问题,我们的解决方案是–根据真实DOM
生成一颗virtual DOM
,当virtual DOM
某个节点的数据改变后会生成一个新的Vnode
,然后Vnode和oldVnode
作对比,发现有不一样的地方就直接修改在真实的DOM
上,然后使oldVnode
的值为Vnode
。这也就是我们所说的一个虚拟dom diff
的过程
传统的Diff算法所耗费的时间复杂度为O(n^3)
,那么这个O(n^3)
是怎么算出来的?
O(n)
O(n^2)
O(n^3)
到这里那么n个节点与n个节点暴力对比就对比完了,那么就开启第三轮可编辑树节点遍历,更改之后的树由vdom(old)
到vdom(new)
故而传统diff算法O(n^3)
是这么算出来的,但是这不是我们今天研究的重点。
现代diff算法
策略说的是,同层级比较,广度优先
那么这里的话我们要深入源码了,在深入源码之前我们在心中应该形成这样一个概念,整个diff的流程是什么?我们再对比着源码解读
我们在Vue初始化的时候调用lifecycleMixin
函数的时候,会给Vue
的原型上挂载_update
方法
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {const vm: Component = thisif (vm._isMounted) {//会调用声明周期中的beforeUpdate回调函数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.//若组件本身的vnode未生成,直接用传入的vnode生成domif (!prevVnode) {// initial rendervm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */,vm.$options._parentElm,vm.$options._refElm)// no need for the ref nodes after initial patch// this prevents keeping a detached DOM tree in memory (#5851)vm.$options._parentElm = vm.$options._refElm = null} else {//对新旧vnode进行diff// updatesvm.$el = vm.__patch__(prevVnode, vnode)}activeInstance = prevActiveInstance// update __vue__ referenceif (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}
我们在这里可以看到vm.$el = vm.__patch__
方法,追根溯源_patch_
的定义:
Vue.prototype.__patch__ = inBrowser ? patch : noop
可见这里是一个浏览器环境的鉴别,如果在浏览器环境中,我们会执行patch,不在的话会执行noop,这是一个util里面的一个方法,用来跨平台的,我们这里暂时不考虑,接着我们去看patch的具体实现./patch
文件,
import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
const modules = platformModules.concat(baseModules)export const patch: Function = createPatchFunction({ nodeOps, modules })
参考 Vue面试题详细解答
/** * 创建patch方法 */
export function createPatchFunction (backend) {let i, jconst cbs = {}const { modules, nodeOps } = backendfor (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]])}}}function emptyNodeAt (elm) {return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)}/** * 创建一个回调方法, 用于删除节点 * * */function createRmCb (childElm, listeners) {function remove () {if (--remove.listeners === 0) {removeNode(childElm)}}remove.listeners = listenersreturn remove}function removeNode (el) {const parent = nodeOps.parentNode(el)// element may have already been removed due to v-html / v-textif (isDef(parent)) {nodeOps.removeChild(parent, el)}}/** * 通过vnode的tag判断是否是原生dom标签或者组件标签 * 用于创建真实DOM节点时, 预先判断tag的合法性 */function isUnknownElement (vnode, inVPre) {return (!inVPre &&!vnode.ns &&!(config.ignoredElements.length &&config.ignoredElements.some(ignore => {return isRegExp(ignore)? ignore.test(vnode.tag): ignore === vnode.tag})) &&config.isUnknownElement(vnode.tag))}let creatingElmInVPre = 0// 创建一个节点function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) {// 节点已经被渲染, 需要使用一个克隆节点if (isDef(vnode.elm) && isDef(ownerArray)) {// This vnode was used in a previous render!// now it's used as a new node, overwriting its elm would cause// potential patch errors down the road when it's used as an insertion// reference node. Instead, we clone the node on-demand before creating// associated DOM element for it.vnode = ownerArray[index] = cloneVNode(vnode)}// 创建组件节点 详见本文件中的createComponent方法vnode.isRootInsert = !nested // for transition enter checkif (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {return}const data = vnode.dataconst children = vnode.childrenconst tag = vnode.tag/** * 如果要创建的节点有tag属性, 这里做一下校验 * 如果该节点上面有v-pre指令, 直接给flag加1 * 如果没有v-pre需要调用isUnknownElement判断标签是否合法, 然后给出警告 */if (isDef(tag)) {if (process.env.NODE_ENV !== 'production') {if (data && data.pre) {creatingElmInVPre++}if (isUnknownElement(vnode, creatingElmInVPre)) {warn('Unknown custom element: <' + tag + '> - did you ' +'register the component correctly? For recursive components, ' +'make sure to provide the "name" option.',vnode.context)}}vnode.elm = vnode.ns? nodeOps.createElementNS(vnode.ns, tag): nodeOps.createElement(tag, vnode)setScope(vnode)/* istanbul ignore if */if (__WEEX__) {// in Weex, the default insertion order is parent-first.// List items can be optimized to use children-first insertion// with append="tree".const appendAsTree = isDef(data) && isTrue(data.appendAsTree)if (!appendAsTree) {if (isDef(data)) {invokeCreateHooks(vnode, insertedVnodeQueue)}insert(parentElm, vnode.elm, refElm)}createChildren(vnode, children, insertedVnodeQueue)if (appendAsTree) {if (isDef(data)) {invokeCreateHooks(vnode, insertedVnodeQueue)}insert(parentElm, vnode.elm, refElm)}} else {createChildren(vnode, children, insertedVnodeQueue)if (isDef(data)) {invokeCreateHooks(vnode, insertedVnodeQueue)}insert(parentElm, vnode.elm, refElm)}if (process.env.NODE_ENV !== 'production' && data && data.pre) {creatingElmInVPre--}} else if (isTrue(vnode.isComment)) {vnode.elm = nodeOps.createComment(vnode.text)insert(parentElm, vnode.elm, refElm)} else {vnode.elm = nodeOps.createTextNode(vnode.text)insert(parentElm, vnode.elm, refElm)}}/** * 创建组件 * 如果组件实例已经存在, 只需要初始化组件并重新激活组件即可 */function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {let i = vnode.dataif (isDef(i)) {const isReactivated = isDef(vnode.componentInstance) && i.keepAliveif (isDef(i = i.hook) && isDef(i = i.init)) {i(vnode, false /* hydrating */, parentElm, refElm)}// after calling the init hook, if the vnode is a child component// it should've created a child instance and mounted it. the child// component also has set the placeholder vnode's elm.// in that case we can just return the element and be done.if (isDef(vnode.componentInstance)) {initComponent(vnode, insertedVnodeQueue)if (isTrue(isReactivated)) {reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)}return true}}}/** * 初始化组件 * 主要的操作是已插入的vnode队列, 触发create钩子, 设置style的scope, 注册ref */function initComponent (vnode, insertedVnodeQueue) {if (isDef(vnode.data.pendingInsert)) {insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)vnode.data.pendingInsert = null}vnode.elm = vnode.componentInstance.$elif (isPatchable(vnode)) {invokeCreateHooks(vnode, insertedVnodeQueue)setScope(vnode)} else {// empty component root.// skip all element-related modules except for ref (#3455)registerRef(vnode)// make sure to invoke the insert hookinsertedVnodeQueue.push(vnode)}}/** * 激活组件 */function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {let i// hack for #4339: a reactivated component with inner transition// does not trigger because the inner node's created hooks are not called// again. It's not ideal to involve module-specific logic in here but// there doesn't seem to be a better way to do it.let innerNode = vnodewhile (innerNode.componentInstance) {innerNode = innerNode.componentInstance._vnodeif (isDef(i = innerNode.data) && isDef(i = i.transition)) {for (i = 0; i < cbs.activate.length; ++i) {cbs.activate[i](emptyNode, innerNode)}insertedVnodeQueue.push(innerNode)break}}// unlike a newly created component,// a reactivated keep-alive component doesn't insert itselfinsert(parentElm, vnode.elm, refElm)}/** * 插入节点, 有父节点的插入到前面, 没有的插入到后面 */function insert (parent, elm, ref) {if (isDef(parent)) {if (isDef(ref)) {if (ref.parentNode === parent) {nodeOps.insertBefore(parent, elm, ref)}} else {nodeOps.appendChild(parent, elm)}}}function createChildren (vnode, children, insertedVnodeQueue) {if (Array.isArray(children)) {if (process.env.NODE_ENV !== 'production') {checkDuplicateKeys(children)}for (let i = 0; i < children.length; ++i) {createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)}} else if (isPrimitive(vnode.text)) {nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))}}function isPatchable (vnode) {while (vnode.componentInstance) {vnode = vnode.componentInstance._vnode}return isDef(vnode.tag)}function invokeCreateHooks (vnode, insertedVnodeQueue) {for (let i = 0; i < cbs.create.length; ++i) {cbs.create[i](emptyNode, vnode)}i = vnode.data.hook // Reuse variableif (isDef(i)) {if (isDef(i.create)) i.create(emptyNode, vnode)if (isDef(i.insert)) insertedVnodeQueue.push(vnode)}}// set scope id attribute for scoped CSS.// this is implemented as a special case to avoid the overhead// of going through the normal attribute patching process.function setScope (vnode) {let iif (isDef(i = vnode.fnScopeId)) {nodeOps.setStyleScope(vnode.elm, i)} else {let ancestor = vnodewhile (ancestor) {if (isDef(i = ancestor.context) && isDef(i = i.$options._scopeId)) {nodeOps.setStyleScope(vnode.elm, i)}ancestor = ancestor.parent}}// for slot content they should also get the scopeId from the host instance.if (isDef(i = activeInstance) &&i !== vnode.context &&i !== vnode.fnContext &&isDef(i = i.$options._scopeId)) {nodeOps.setStyleScope(vnode.elm, i)}}function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {for (; startIdx <= endIdx; ++startIdx) {createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)}}// 递归调用销毁钩子function invokeDestroyHook (vnode) {let i, jconst data = vnode.dataif (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)}if (isDef(i = vnode.children)) {for (j = 0; j < vnode.children.length; ++j) {invokeDestroyHook(vnode.children[j])}}}/** * 删除多个节点 * 文本节点可以直接删除, 其他节点需要触发两个钩子 */function removeVnodes (parentElm, vnodes, startIdx, endIdx) {for (; startIdx <= endIdx; ++startIdx) {const ch = vnodes[startIdx]if (isDef(ch)) {if (isDef(ch.tag)) {removeAndInvokeRemoveHook(ch)invokeDestroyHook(ch)} else { // Text noderemoveNode(ch.elm)}}}}function removeAndInvokeRemoveHook (vnode, rm) {if (isDef(rm) || isDef(vnode.data)) {let iconst listeners = cbs.remove.length + 1if (isDef(rm)) {// we have a recursively passed down rm callback// increase the listeners countrm.listeners += listeners} else {// directly removingrm = createRmCb(vnode.elm, listeners)}// recursively invoke hooks on child component root nodeif (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) {removeAndInvokeRemoveHook(i, rm)}for (i = 0; i < cbs.remove.length; ++i) {cbs.remove[i](vnode, rm)}if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) {i(vnode, rm)} else {rm()}} else {removeNode(vnode.elm)}}// diff操作核心算法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, vnodeToMove, refElm// removeOnly is a special flag used only by // to ensure removed elements stay in correct relative positions// during leaving transitions// 在transition中 不能移动节点const canMove = !removeOnly// 检查是否有重复的keyif (process.env.NODE_ENV !== 'production') {checkDuplicateKeys(newCh)}// 一共分四种情况讨论, 旧列表第一个与新列表第一个对比, 旧列表最后一个与新列表最后一个对比// 然后新列表第一个和旧列表最后一个对比, 新列表最后一个和旧列表第一个对比// 之所以要交叉头尾对比, 是为了防止最差的情况出现while (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)) {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 {// 以上四种情况都不满足时, 使用新列表第一个vdom的key去旧列表查找// 如果可以找到key相同的元素, 直接进行patch然后进入下一次循环// 找不到则插入一个新节点if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)idxInOld = isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key]: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)if (isUndef(idxInOld)) { // New elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)} else {vnodeToMove = oldCh[idxInOld]if (sameVnode(vnodeToMove, newStartVnode)) {patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)oldCh[idxInOld] = undefinedcanMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)} else {// same key but different element. treat as new elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)}}newStartVnode = newCh[++newStartIdx]}}// 新旧列表其中之一全部循环完成后, 开始清理剩余的节点// 如果旧列表全部遍历完成, 新列表还有剩余, 直接创建这些新节点// 反之, 如果新列表全部遍历, 旧列表还有剩余, 直接删除这些旧节点if (oldStartIdx > oldEndIdx) {refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elmaddVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)} else if (newStartIdx > newEndIdx) {removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)}}/** * 检查是否有重复的key * 一个很简单的遍历查找重复值的操作 * 其实这个seenKeys我觉得改成数组会更好, 写成object又给每个key的value置为true蛮奇怪的 */function checkDuplicateKeys (children) {const seenKeys = {}for (let i = 0; i < children.length; i++) {const vnode = children[i]const key = vnode.keyif (isDef(key)) {if (seenKeys[key]) {warn(`Duplicate keys detected: '${key}'. This may cause an update error.`,vnode.context)} else {seenKeys[key] = true}}}}/** * 在旧的子节点列表寻找相似节点(只查找第一个) */function findIdxInOld (node, oldCh, start, end) {for (let i = start; i < end; i++) {const c = oldCh[i]if (isDef(c) && sameVnode(node, c)) return i}}function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {// 如果oldVnode跟vnode完全一致,那么不需要做任何事情if (oldVnode === vnode) {return}const elm = vnode.elm = oldVnode.elmif (isTrue(oldVnode.isAsyncPlaceholder)) {if (isDef(vnode.asyncFactory.resolved)) {hydrate(oldVnode.elm, vnode, insertedVnodeQueue)} else {vnode.isAsyncPlaceholder = true}return}// 如果oldVnode跟vnode都是静态节点,且具有相同的key// 当vnode是克隆节点或是v-once指令控制的节点时// 只需要把oldVnode.elm和oldVnode.child都复制到vnode上,也不用再有其他操作// 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.if (isTrue(vnode.isStatic) &&isTrue(oldVnode.isStatic) &&vnode.key === oldVnode.key &&(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {vnode.componentInstance = oldVnode.componentInstancereturn}let iconst data = vnode.dataif (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)}const oldCh = oldVnode.childrenconst ch = vnode.childrenif (isDef(data) && isPatchable(vnode)) {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不是文本节点或注释节点if (isUndef(vnode.text)) {// 如果oldVnode和vnode都有子节点,且2方的子节点不完全一致,就执行updateChildrenif (isDef(oldCh) && isDef(ch)) {if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)} else if (isDef(ch)) {// 如果只有vnode有子节点,那就创建这些子节点if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)// 如果只有oldVnode有子节点,那就把这些节点都删除} else if (isDef(oldCh)) {removeVnodes(elm, oldCh, 0, oldCh.length - 1)// 如果oldVnode和vnode都没有子节点,但是oldVnode是文本节点或注释节点,就把vnode.elm的文本设置为空字符串} else if (isDef(oldVnode.text)) {nodeOps.setTextContent(elm, '')}// 如果vnode是文本节点或注释节点,但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容即可} else if (oldVnode.text !== vnode.text) {nodeOps.setTextContent(elm, vnode.text)}if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)}}function invokeInsertHook (vnode, queue, initial) {// delay insert hooks for component root nodes, invoke them after the// element is really insertedif (isTrue(initial) && isDef(vnode.parent)) {vnode.parent.data.pendingInsert = queue} else {for (let i = 0; i < queue.length; ++i) {queue[i].data.hook.insert(queue[i])}}}let hydrationBailed = false// list of modules that can skip create hook during hydration because they// are already rendered on the client or has no need for initialization// Note: style is excluded because it relies on initial clone for future// deep updates (#7063).const isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key')// Note: this is a browser-only function so we can assume elms are DOM nodes.function hydrate (elm, vnode, insertedVnodeQueue, inVPre) {let iconst { tag, data, children } = vnodeinVPre = inVPre || (data && data.pre)vnode.elm = elmif (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) {vnode.isAsyncPlaceholder = truereturn true}// assert node matchif (process.env.NODE_ENV !== 'production') {if (!assertNodeMatch(elm, vnode, inVPre)) {return false}}if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode, true /* hydrating */)if (isDef(i = vnode.componentInstance)) {// child component. it should have hydrated its own tree.initComponent(vnode, insertedVnodeQueue)return true}}if (isDef(tag)) {if (isDef(children)) {// empty element, allow client to pick up and populate childrenif (!elm.hasChildNodes()) {createChildren(vnode, children, insertedVnodeQueue)} else {// v-html and domProps: innerHTMLif (isDef(i = data) && isDef(i = i.domProps) && isDef(i = i.innerHTML)) {if (i !== elm.innerHTML) {/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' &&typeof console !== 'undefined' &&!hydrationBailed) {hydrationBailed = trueconsole.warn('Parent: ', elm)console.warn('server innerHTML: ', i)console.warn('client innerHTML: ', elm.innerHTML)}return false}} else {// iterate and compare children listslet childrenMatch = truelet childNode = elm.firstChildfor (let i = 0; i < children.length; i++) {if (!childNode || !hydrate(childNode, children[i], insertedVnodeQueue, inVPre)) {childrenMatch = falsebreak}childNode = childNode.nextSibling}// if childNode is not null, it means the actual childNodes list is// longer than the virtual children list.if (!childrenMatch || childNode) {/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' &&typeof console !== 'undefined' &&!hydrationBailed) {hydrationBailed = trueconsole.warn('Parent: ', elm)console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children)}return false}}}}if (isDef(data)) {let fullInvoke = falsefor (const key in data) {if (!isRenderedModule(key)) {fullInvoke = trueinvokeCreateHooks(vnode, insertedVnodeQueue)break}}if (!fullInvoke && data['class']) {// ensure collecting deps for deep class bindings for future updatestraverse(data['class'])}}} else if (elm.data !== vnode.text) {elm.data = vnode.text}return true}function assertNodeMatch (node, vnode, inVPre) {if (isDef(vnode.tag)) {return vnode.tag.indexOf('vue-component') === 0 || (!isUnknownElement(vnode, inVPre) &&vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase()))} else {return node.nodeType === (vnode.isComment ? 8 : 3)}}/** * 这里返回一个patch函数供后续对vnode进行patch操作 * 这里的patch操作是指, 将oldVnode对应的真实DOM更改为vnode对应的真实DOM, 所需要的最低性能开销的操作(或者说是较低) * 参数中的oldVnode是更新前的旧节点, vnode是将要更新的新节点, hydrating是一个flag标识是否与原生DOM混合, removeOnly是在过渡动画中使用 */return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {// 这里很简单, 如果新节点不存在, 旧节点也不存在, 无需任何操作, 如果新节点不存在,但旧节点存在, 说明需要删除旧节点, 调用一个销毁钩子if (isUndef(vnode)) {if (isDef(oldVnode)) invokeDestroyHook(oldVnode)return}// 用于标识是否初始化这个节点let isInitialPatch = falseconst insertedVnodeQueue = []// 旧节点不存在 说明需要创建一个新节点if (isUndef(oldVnode)) {// empty mount (likely as component), create new root elementisInitialPatch = truecreateElm(vnode, insertedVnodeQueue, parentElm, refElm)} else {// 走到这里 说明新旧节点都存在, 这时比较复杂, 分几种情况处理 // 先通过nodeType判断是否是真正的节点, 真正的节点nodeType取值范围是1~12// vue里常用的基本只有三种 1代表是dom元素节点 3是文本节点 8是注释节点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)) {oldVnode.removeAttribute(SSR_ATTR)hydrating = true}// 需要用hydrate函数将虚拟DOM和真实DOM进行映射if (isTrue(hydrating)) {if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {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 ' +', or missing
. Bailing hydration and performing ' +'full client-side render.')}}// either not server-rendered, or hydration failed.// create an empty node and replace itoldVnode = emptyNodeAt(oldVnode)}// replacing existing elementconst oldElm = oldVnode.elmconst parentElm = nodeOps.parentNode(oldElm)// create new nodecreateElm(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))// update parent placeholder node element, recursivelyif (isDef(vnode.parent)) {let ancestor = vnode.parentconst patchable = isPatchable(vnode)while (ancestor) {for (let i = 0; i < cbs.destroy.length; ++i) {cbs.destroy[i](ancestor)}ancestor.elm = vnode.elmif (patchable) {for (let i = 0; i < cbs.create.length; ++i) {cbs.create[i](emptyNode, ancestor)}// #6513// invoke insert hooks that may have been merged by create hooks.// e.g. for directives that uses the "inserted" hook.const insert = ancestor.data.hook.insertif (insert.merged) {// start at index 1 to avoid re-invoking component mounted hookfor (let i = 1; i < insert.fns.length; i++) {insert.fns[i]()}}} else {registerRef(ancestor)}ancestor = ancestor.parent}}// destroy old nodeif (isDef(parentElm)) {removeVnodes(parentElm, [oldVnode], 0, 0)} else if (isDef(oldVnode.tag)) {invokeDestroyHook(oldVnode)}}}invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)return vnode.elm}
}
可以看到patch
接收的参数
oldVnode
:旧的虚拟节点vnode
:新的虚拟节点hydrating
:是否映射removeOnly
:标识parentElm
:父节点refElm
:被插入之后的占位符
那么核心diff
代码在于 sameVnode
、createElm
、patchVNode
我们依次展开来说
sameVnode
顾名思义可以看判断两个节点是不是同一个节点
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)) || (isTrue(a.isAsyncPlaceholder) &&a.asyncFactory === b.asyncFactory &&isUndef(b.asyncFactory.error))))
}
/** * 节点 key 必须相同 * tag、注释、data是否存在、input类型是否相同 * 如果isAsyncPlaceholder是true,则需要asyncFactory属性相同 */
createElm
// 创建一个节点function createElm (vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index) {// 节点已经被渲染, 需要使用一个克隆节点if (isDef(vnode.elm) && isDef(ownerArray)) {// This vnode was used in a previous render!// now it's used as a new node, overwriting its elm would cause// potential patch errors down the road when it's used as an insertion// reference node. Instead, we clone the node on-demand before creating// associated DOM element for it.vnode = ownerArray[index] = cloneVNode(vnode)}// 创建组件节点 详见本文件中的createComponent方法vnode.isRootInsert = !nested // for transition enter checkif (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {return}const data = vnode.dataconst children = vnode.childrenconst tag = vnode.tag/** * 如果要创建的节点有tag属性, 这里做一下校验 * 如果该节点上面有v-pre指令, 直接给flag加1 * 如果没有v-pre需要调用isUnknownElement判断标签是否合法, 然后给出警告 */if (isDef(tag)) {if (process.env.NODE_ENV !== 'production') {if (data && data.pre) {creatingElmInVPre++}if (isUnknownElement(vnode, creatingElmInVPre)) {warn('Unknown custom element: <' + tag + '> - did you ' +'register the component correctly? For recursive components, ' +'make sure to provide the "name" option.',vnode.context)}}vnode.elm = vnode.ns? nodeOps.createElementNS(vnode.ns, tag): nodeOps.createElement(tag, vnode)setScope(vnode)/* istanbul ignore if */if (__WEEX__) {// in Weex, the default insertion order is parent-first.// List items can be optimized to use children-first insertion// with append="tree".const appendAsTree = isDef(data) && isTrue(data.appendAsTree)if (!appendAsTree) {if (isDef(data)) {invokeCreateHooks(vnode, insertedVnodeQueue)}insert(parentElm, vnode.elm, refElm)}createChildren(vnode, children, insertedVnodeQueue)if (appendAsTree) {if (isDef(data)) {invokeCreateHooks(vnode, insertedVnodeQueue)}insert(parentElm, vnode.elm, refElm)}} else {createChildren(vnode, children, insertedVnodeQueue)if (isDef(data)) {invokeCreateHooks(vnode, insertedVnodeQueue)}insert(parentElm, vnode.elm, refElm)}if (process.env.NODE_ENV !== 'production' && data && data.pre) {creatingElmInVPre--}} else if (isTrue(vnode.isComment)) {vnode.elm = nodeOps.createComment(vnode.text)insert(parentElm, vnode.elm, refElm)} else {vnode.elm = nodeOps.createTextNode(vnode.text)insert(parentElm, vnode.elm, refElm)}}
此段代码就是创建真实dom
的目的,下一章会谈到。
patchVnode
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {// 如果oldVnode跟vnode完全一致,那么不需要做任何事情if (oldVnode === vnode) {return}const elm = vnode.elm = oldVnode.elmif (isTrue(oldVnode.isAsyncPlaceholder)) {if (isDef(vnode.asyncFactory.resolved)) {hydrate(oldVnode.elm, vnode, insertedVnodeQueue)} else {vnode.isAsyncPlaceholder = true}return}// 如果oldVnode跟vnode都是静态节点,且具有相同的key// 当vnode是克隆节点或是v-once指令控制的节点时// 只需要把oldVnode.elm和oldVnode.child都复制到vnode上,也不用再有其他操作// 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.if (isTrue(vnode.isStatic) &&isTrue(oldVnode.isStatic) &&vnode.key === oldVnode.key &&(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {vnode.componentInstance = oldVnode.componentInstancereturn}let iconst data = vnode.dataif (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)}const oldCh = oldVnode.childrenconst ch = vnode.childrenif (isDef(data) && isPatchable(vnode)) {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不是文本节点或注释节点if (isUndef(vnode.text)) {// 如果oldVnode和vnode都有子节点,且2方的子节点不完全一致,就执行updateChildrenif (isDef(oldCh) && isDef(ch)) {if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)} else if (isDef(ch)) {// 如果只有vnode有子节点,那就创建这些子节点if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)// 如果只有oldVnode有子节点,那就把这些节点都删除} else if (isDef(oldCh)) {removeVnodes(elm, oldCh, 0, oldCh.length - 1)// 如果oldVnode和vnode都没有子节点,但是oldVnode是文本节点或注释节点,就把vnode.elm的文本设置为空字符串} else if (isDef(oldVnode.text)) {nodeOps.setTextContent(elm, '')}// 如果vnode是文本节点或注释节点,但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容即可} else if (oldVnode.text !== vnode.text) {nodeOps.setTextContent(elm, vnode.text)}if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)}}
具体代码功能已经解释的很清楚了,这里的addVnodes
和removeVnodes
就是新增与移除虚拟节点,核心代码我们主要关注一个updateChildren
updateChildren
// diff操作核心算法function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {// 记录新旧节点列表的首尾元素 用于比较let oldStartIdx = 0 // 旧列表起点位置let newStartIdx = 0 // 新列表起点位置let oldEndIdx = oldCh.length - 1 // 旧列表终点位置let oldStartVnode = oldCh[0] // 旧列表起点值let oldEndVnode = oldCh[oldEndIdx] // 旧列表终点值let newEndIdx = newCh.length - 1 // 新列表终点位置let newStartVnode = newCh[0] // 新列表起点值let newEndVnode = newCh[newEndIdx] // 新列表终点值let oldKeyToIdx, idxInOld, vnodeToMove, refElm// removeOnly is a special flag used only by // to ensure removed elements stay in correct relative positions// during leaving transitions// 在transition中 不能移动节点const canMove = !removeOnly// 检查是否有重复的keyif (process.env.NODE_ENV !== 'production') {checkDuplicateKeys(newCh)}// 一共分四种情况讨论, 旧列表第一个与新列表第一个对比, 旧列表最后一个与新列表最后一个对比// 然后新列表第一个和旧列表最后一个对比, 新列表最后一个和旧列表第一个对比// 之所以要交叉头尾对比, 是为了防止最差的情况出现while (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)) {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 {// 以上四种情况都不满足时, 使用新列表第一个vdom的key去旧列表查找// 如果可以找到key相同的元素, 直接进行patch然后进入下一次循环// 找不到则插入一个新节点if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)idxInOld = isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key]: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)if (isUndef(idxInOld)) { // New elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)} else {vnodeToMove = oldCh[idxInOld]if (sameVnode(vnodeToMove, newStartVnode)) {patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)oldCh[idxInOld] = undefinedcanMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)} else {// same key but different element. treat as new elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)}}newStartVnode = newCh[++newStartIdx]}}// 新旧列表其中之一全部循环完成后, 开始清理剩余的节点// 如果旧列表全部遍历完成, 新列表还有剩余, 直接创建这些新节点// 反之, 如果新列表全部遍历, 旧列表还有剩余, 直接删除这些旧节点if (oldStartIdx > oldEndIdx) {refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elmaddVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)} else if (newStartIdx > newEndIdx) {removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)}}
这里利用了while
循环与双指针对比
新旧虚拟dom
定义指针变量
let oldStartIdx = 0 // 旧列表起点位置let newStartIdx = 0 // 新列表起点位置let oldEndIdx = oldCh.length - 1 // 旧列表终点位置let oldStartVnode = oldCh[0] // 旧列表起点值let oldEndVnode = oldCh[oldEndIdx] // 旧列表终点值let newEndIdx = newCh.length - 1 // 新列表终点位置let newStartVnode = newCh[0] // 新列表起点值let newEndVnode = newCh[newEndIdx] // 新列表终点值
定义循环
while (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)) {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 {// 以上四种情况都不满足时, 使用新列表第一个vdom的key去旧列表查找// 如果可以找到key相同的元素, 直接进行patch然后进入下一次循环// 找不到则插入一个新节点if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)idxInOld = isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key]: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)if (isUndef(idxInOld)) { // New elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)} else {vnodeToMove = oldCh[idxInOld]if (sameVnode(vnodeToMove, newStartVnode)) {patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)oldCh[idxInOld] = undefinedcanMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)} else {// same key but different element. treat as new elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)}}newStartVnode = newCh[++newStartIdx]}}
检测oldStartVnode、oldEndVnode
if (isUndef(oldStartVnode)) {oldStartVnode = oldCh[++oldStartIdx]
} else if (isUndef(oldEndVnode)) {oldEndVnode = oldCh[--oldEndIdx]
}
如果oldStartVnode
不存在,oldCh
起始点向后移动。如果oldEndVnode
不存在,oldCh
终止点向前移动。
oldStartVnode 和 newStartVnode 是相同节点
else if (sameVnode(oldStartVnode, newStartVnode)) {patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)oldStartVnode = oldCh[++oldStartIdx]newStartVnode = newCh[++newStartIdx]
}
如果oldStartVnode
和 newStartVnode
是相同节点,则patchVnode
,同时彼此向后移动一位
oldEndVnode 和 newEndVnode 是相同节点
else if (sameVnode(oldEndVnode, newEndVnode)) {patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)oldEndVnode = oldCh[--oldEndIdx]newEndVnode = newCh[--newEndIdx]
}
如果oldEndVnode
和 newEndVnode
是相同节点,则patchVnode
,同时彼此向前移动一位
oldStartVnode 和 newEndVnode 是相同节点
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]
}
如果oldStartVnode
和 newEndVnode
是相同节点,则先 patchVnode
,然后把oldStartVnode
移到oldCh
最后的位置即可,然后oldStartIdx
向后移动一位,newEndIdx
向前移动一位
oldEndVnode 和 newStartVnode 是相同节点
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]
}
如果oldEndVnode
和 newStartVnode
是相同节点,则先 patchVnode
,然后把oldEndVnode
移到oldCh
最前的位置即可,然后newStartIdx
向后移动一位,oldEndIdx
向前移动一位
key不相同执行createElm方法
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key]: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
如果以上条件都不匹配,则查找oldVnode
中与vnode
具有相同key
的节点,并将查找的结果赋值给elmToMove
。如果找不到相同key
的节点,则表示是新创建的节点
key相同,就认为是同一节点
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)oldCh[idxInOld] = undefinedcanMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {// same key but different element. treat as new elementcreateElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
newStartVnode = newCh[++newStartIdx]
若为同一类型就调用patchVnode
,就将对应下标处的oldVnode
设置为undefined
,把vnodeToMove
插入到oldCh
之前,newStartIdx
继续向后移动。如果两个 vnode
不相同,视为新元素,执行 createElm
创建。
如果老dom的开始索引大于结束索引,新dom数组大于老dom数组,表示新增会调用addVnodes方法
if (oldStartIdx > oldEndIdx) {refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elmaddVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
}
如果老dom的开始索引小于结束索引,新dom数组小于老dom数组,表示新增会调用removeVnodes方法
else if (newStartIdx > newEndIdx) {removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
总结

因为现代diff算法策略
是同层级比较,广度优先
,故而现代算法复杂度为O(n)
这一章我们讲述了传统diff算法复杂度
,O(n^3)
到现代的O(n)
的实现的一个思路,下一章就开始讲解对比过后的vdom
如何映射
成真实dom
上一篇:Sunday 算法详解
相关内容
热门资讯
AWSECS:访问外部网络时出...
如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
银河麒麟V10SP1高级服务器...
银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
【NI Multisim 14...
目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
不能访问光猫的的管理页面
光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSElasticBeans...
在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
月入8000+的steam搬砖...
大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...
ToDesk 远程工具安装及...
目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
北信源内网安全管理卸载
北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWS管理控制台菜单和权限
要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
AWR报告解读
WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...