学习目标

分析Vue中虚拟DOM的实现

  • 虚拟DOM创建的过程
  • 与虚拟DOM相关的一些函数,如
    • h 函数
    • patch 函数
    • patchVnode 函数
  • v-for 中使用 key 的好处

虚拟DOM相关回顾

什么是虚拟DOM

  • 虚拟DOM(Virtual DOM) 是使用 JavaScript 对象描述真实 DOM
    • 虚拟DOM的本质就是JavaScript对象
    • 程序的各种变化首先作用于虚拟DOM,最终映射到真实DOM
  • Vue.js 中的虚拟DOM借鉴了 Snabbdom,并添加了 Vue.js 的特性
    • 借鉴如
      • 模块机制、钩子函数、diff 算法
    • 特性如
      • 指令和组件机制

为什么要使用虚拟DOM

  • 避免用户直接操作真实DOM,提高开发效率
    • 开发过程只需关注业务代码的实现,不需要关注如何操作DOM
    • 不需要关注DOM的浏览器兼容性问题
  • 作为一个中间层可以跨平台
    • 服务端渲染
    • weex框架
  • 虚拟DOM不一定可以提高性能
    • 首次渲染的时候会增加开销,不如直接操作DOM性能好
      • 因为要额外的维护一层虚拟DOM,也就是要创建一些额外的JavaScript对象,增加了开销
      • 复杂视图情况下提升渲染性能
        • 如果有频繁DOM操作的化,虚拟DOM在更新真实DOM之前,首先通过Diff算法,对比新旧两个DOM树的差异,最终把差异更新到真实DOM,而不会每次都直接操作真实DOM。
        • 另外通过给节点设置key属性,可以另节点重用,避免大量的重绘

代码演示

演示Vue中的虚拟DOM,明确后面要研究的内容。

<div id="app"></div>
<script src="../../dist/vue.js"></script>
<script>const vm = new Vue({el: "#app",render(h) {// h语法:h(tag, data, children)// return h('h1', this.msg)// return h('h1', { domProps: { innerHTML: this.msg }})// return h('h1', { attrs: {id: 'title'}}, this.msg)const vnode = h('h1',{attrs: { id: 'title' }},this.msg)console.log(vnode)return vnode},data: {msg: 'Hello Vue'}});
</script>

h函数

官方说明

vm.$createElement(tag, data, children, normalizeChildren)

  • tag 标签名称或者组件对象
  • data 描述tag,可以设置 DOM 的属性或者标签的属性
  • children tag中的文本内容或者子节点

vnode核心属性

  • children 存放vnode的子节点

    • 当前示例是一个文本节点
  • data 调用h函数的时候传入的data选项

  • elm vnode转换的真实DOM

    • 当前示例是 h1
  • key 用于复用虚拟DOM

  • tag 调用h函数时传入的第一个参数

  • text

整体过程分析

虚拟DOM创建的整体过程:

  1. 首次渲染过程
    1. vm._init()
    2. vm.$mount()
    3. mountComponent
    4. 创建 Watcher 对象
    5. updateComponent()
      1. 调用了 vm._update(vm._render(), hydrating)
  2. 和虚拟DOM相关的过程(本次学习内容)
    1. _render 创建虚拟DOM并返回,最终传入 _update
      1. vnode = render.call(vm._renderProxy, vm.$createElement)
        1. 调用了用户传入的render或编译生成的render函数
      2. vm.$createElement
        1. 就是render中调用的 h 函数
        2. 内部调用了vm._createElement
      3. vm._createElement
        1. new VNode创建虚拟节点并返回
    2. _update 调用__patch__,负责把虚拟DOM,渲染成真实DOM
      1. 首次执行
        1. vm.__patch__(vm.$el, vnode, hydrating, false)
      2. 数据更新
        1. vm.__patch__(preVnode, vnode)
    3. vm.__patch__
    4. patchVnode
    5. updateChildren

VNode的创建过程 createElement

定义位置

找到调用 updateComponent 的位置(src\core\instance\lifecycle.js),函数内调用了_render函数。

_render是在初始化实例方法时(renderMixin)定义的(src\core\instance\render.js)。

它的核心是调用render:

// 调用render函数
// vm.$createElement -> h函数:生成虚拟DOM
// initProxy 中定义了_renderProxy,它是vm或者vm的代理对象(Proxy)
vnode = render.call(vm._renderProxy, vm.$createElement)

render是从vm.$options中获取:

  • 用户传入的render
  • 或者 编译生成的render

vm.$createElement就是传给用户的h函数:

render(h) {return h(/*...*/)
}

$createElement是在当前文件中定义的:

// 对编译生成的 render 进行渲染的方法
// _c是在 template 选项转换成的 render 函数中调用
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)// 对手写 render 函数进行渲染的方法
// $createElement 是在用户手写的render选项中使用的 h 函数
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

_c 和 $createElement 都是调用的 createElement。

区别是

  • 参数:最后一个参数不同(这个在之后看源码的时候解释)。
  • 调用时机
    • 当render是由用户传入的时候,内部调用 $createElement
    • 当render是由模板编译生成的时候,这个函数内部调用的就是 _c

createElement

它最终返回Vnode,但是Vnode不是它创建的,而是通过 _createElement创建的。

createElement内部主要是处理参数:

  • 判断h函数传递参数的方式:2个参数或3个参数
export function createElement (context: Component,tag: any,data: any,children: any,normalizationType: any,alwaysNormalize: boolean
): VNode | Array<VNode> {// 判断data是数组(子节点)或原始值(标签的内容)的时候// 其实就是传递的children,省略了dataif (Array.isArray(data) || isPrimitive(data)) {normalizationType = children// 把data赋值给children,并且把data设置为undefinedchildren = datadata = undefined}// 判断使用的是_c(false)还是$createElement(true)if (isTrue(alwaysNormalize)) {// ALWAYS_NORMALIZE = 2// normalizationType 将来用来处理childrennormalizationType = ALWAYS_NORMALIZE}return _createElement(context, tag, data, children, normalizationType)
}

_createElement

创建Vnode并返回。

export function _createElement (context: Component,tag?: string | Class<Component> | Function | Object,data?: VNodeData,children?: any,normalizationType?: number
): VNode | Array<VNode> {// 首先判断data是否为空,并且包含 Observer 对象// 说明data是响应式的数据if (isDef(data) && isDef((data: any).__ob__)) {// 发出警告:data应该避免使用响应式数据process.env.NODE_ENV !== 'production' && warn(`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +'Always create fresh vnode data objects in each render!',context)// 返回一个空的vnodereturn createEmptyVNode()}// object syntax in v-bind// 判断data是否为空,并且包含is属性// <component v-bind:is="currentTaComponent"></component>// is 最终会把 currentTaComponent 渲染到 component的位置// vnode选项中的is 和 Vue的is效果一样if (isDef(data) && isDef(data.is)) {// 替换tagtag = data.is}// 如果tag为false,也就是is为false,返回空的vnodeif (!tag) {// in case of component :is set to falsy valuereturn createEmptyVNode()}// warn against non-primitive key// 判断如果data有key,并且key不是一个原始值,发出警告if (process.env.NODE_ENV !== 'production' &&isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {if (!__WEEX__ || !('@binding' in data.key)) {// 警告:key应该避免使用非原始值,应该使用string或number类型的值warn('Avoid using non-primitive value as key, ' +'use string/number value instead.',context)}}// support single function children as default scoped slot// 这段代码用于处理作用域插槽(暂时跳过)if (Array.isArray(children) &&typeof children[0] === 'function') {data = data || {}data.scopedSlots = { default: children[0] }children.length = 0}// ALWAYS_NORMALIZE = 2// SIMPLE_NORMALIZE = 1// 处理children:主要就是把数组拍平,转换成一维数组// 这是_createElement第一个核心的作用if (normalizationType === ALWAYS_NORMALIZE) {// 如果是用于传入的render函数,调用normalizeChildren处理childrenchildren = normalizeChildren(children)} else if (normalizationType === SIMPLE_NORMALIZE) {// 把二维数组,转换成一维数组children = simpleNormalizeChildren(children)}// 下面是第二个核心的内容:创建vnode对象let vnode, nsif (typeof tag === 'string') {// 如果tag是字符串let Ctorns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)// 判断html中的保留标签:HTML和SVG下不能被定义为组件名的标签if (config.isReservedTag(tag)) {// platform built-in elementsif (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {warn(`The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,context)}// new VNode创建 Vnode// context 是 Vue 实例vnode = new VNode(config.parsePlatformTagName(tag), data, children,undefined, undefined, context)} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {// 如果是自定义组件// resolveAsset 从vm.$options.components中获取组件的构造函数// createComponent创建组件对应的Vnode// componentvnode = createComponent(Ctor, data, context, children, tag)} else {// 如果是自定义标签,new VNode创建 Vnode// unknown or unlisted namespaced elements// check at runtime because it may get assigned a namespace when its// parent normalizes childrenvnode = new VNode(tag, data, children,undefined, undefined, context)}} else {// 如果tag不是字符串,那它应该是一个组件// createComponent创建组件对应的Vnode// direct component options / constructorvnode = createComponent(tag, data, context, children)}if (Array.isArray(vnode)) {// 如果vnode是数组,直接返回return vnode} else if (isDef(vnode)) {// 如果vnode不为空,对vnode做一些处理if (isDef(ns)) applyNS(vnode, ns)if (isDef(data)) registerDeepBindings(data)return vnode} else {// 否则返回空的vnodereturn createEmptyVNode()}
}

createEmptyVNode

// src\core\vdom\vnode.js
// 创建一个空的vnode(注释节点)
export const createEmptyVNode = (text: string = '') => {const node = new VNode()node.text = text// 标识当前vnode是一个注释节点node.isComment = truereturn node
}

VNode 类

export default class VNode {// 声明vnode的属性tag: string | void;data: VNodeData | void;children: ?Array<VNode>;text: string | void;elm: Node | void;ns: string | void;context: Component | void; // rendered in this component's scopekey: string | number | void;// ... 其他属性constructor (tag?: string,data?: VNodeData,children?: ?Array<VNode>,text?: string,elm?: Node,context?: Component,componentOptions?: VNodeComponentOptions,asyncFactory?: Function) {// 只是初始化了vnode的属性this.tag = tagthis.data = datathis.children = childrenthis.text = textthis.elm = elm// ...其他属性}// DEPRECATED: alias for componentInstance for backwards compat./* istanbul ignore next */get child (): Component | void {return this.componentInstance}
}

normalizeChildren

不管children是什么值,都返回一个一维数组,方便后续处理

// src\core\vdom\helpers\normalize-children.js
// normalizeChildren的核心作用:不管children是什么值,都返回一个一维数组,方便后续处理
// <template>, <slot>, v-for情况下children可能是数组,并且可能嵌套了多层
export function normalizeChildren (children: any): ?Array<VNode> {// 如果调用的是用户传入的render// children可能是字符串或数组// 如果children是原始值,把children转换成文本节点,并包装成数组返回// 否则(children是数组)调用normalizeArrayChildren把children拍平//   normalizeArrayChildren 把一个多为的数组转化成一维数组return isPrimitive(children)? [createTextVNode(children)]: Array.isArray(children)? normalizeArrayChildren(children): undefined
}

simpleNormalizeChildren

把二维数组转化成一维数组

// src\core\vdom\helpers\normalize-children.js
// 如果children中包含组件,并且这个组件是函数式组件的话,就会调用这个方法进行处理
// 因为函数式组件已经进行了一维数组的转化,此时children顶多是一个二维数组
// 把二维数组转化成一维数组
export function simpleNormalizeChildren (children: any) {for (let i = 0; i < children.length; i++) {if (Array.isArray(children[i])) {// concat用于拼接数组,还能把后面的二维数组展开去处理return Array.prototype.concat.apply([], children)}}return children
}

isReservedTag

判断是否是HTML保留标签。

规定一些 html 和 svg 下面的不能定义为组件名的标签。

// src\platforms\web\util\element.js
export const isReservedTag = (tag: string): ?boolean => {return isHTMLTag(tag) || isSVG(tag)
}

VNode 的处理过程 update

lifecycle.js中定义了updateComponent方法,内部调用了_update和_render。

_render 中 最终调用了_createElement,创建了VNode。

然后传递给 _update 处理创建的VNode。

_update也是在当前文件中定义,核心就是调用了 vm.__patch__。

_update的工作就是:

  • 判断是否有 preVnode(旧vnode)
    • 如果没有,就是首次渲染,调用vm.__patch__传入$el
    • 如果有,就是数据变更渲染,调用vm.__patch__传入旧vnode
// _update 方法的作用是把 VNode 渲染成真实的 DOM
// 首次渲染会调用,数据更新会调用
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {const vm: Component = thisconst prevEl = vm.$el// 获取之前处理的 vnode 对象const prevVnode = vm._vnodeconst restoreActiveInstance = setActiveInstance(vm)// 更新_vnode为新vnodevm._vnode = vnode// Vue.prototype.__patch__ is injected in entry points// Vue原型上的__patch__方法是在入口中被注入进来的// based on the rendering backend used.、// 核心部分:调用__patch__,把虚拟DOM转换成真实DOM,最终挂载到 $elif (!prevVnode) {// 如果不存在处理过的 vnode,说明是首次渲染// initial rendervm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)} else {// 数据变更渲染// updatesvm.$el = vm.__patch__(prevVnode, vnode)}restoreActiveInstance()// 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}// updated hook is called by the scheduler to ensure that children are// updated in a parent's updated hook.
}

patch 函数的初始化

从_update源码中看出,真正处理vnode的地方是在 Vue 实例的 __patch__方法中。

回顾 Snabbdom 中的 vnode 和 patch

vnode 创建一个对象,包含了几个参数(比Vue少很多)

export function vnode (sel: string | undefined,data: any | undefined,children: Array<VNode | string> | undefined,text: string | undefined,elm: Element | Text | undefined): VNode {const key = data === undefined ? undefined : data.keyreturn { sel, data, children, text, elm, key }
}

patch函数通过init函数返回(高阶函数函数)。

init中传入了modules模块(插件) 和 domApi(操作DOM的方法)。

它先用必报的方式对这两个参数继续缓存,然后返回一个patch函数。

这样将来在调用patch时,就不需要关心modules 和 domApi的引入。

Vue也是类似的机制。

export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {}
}

Vue.js中 patch 的初始化

Vue中的patch在 src\platforms\web\runtime 目录下定义,它是和平台相关的。

// src\platforms\web\runtime\index.js
import { patch } from './patch'// install platform patch function
// 在 Vue 的原型上注册 __patch__ 函数
// 类似 Snabbdom 的 patch 函数:把虚拟DOM转化成真实DOM
// inBrowser:判断是否是浏览器环境;noop:空函数
Vue.prototype.__patch__ = inBrowser ? patch : noop

patch函数是通过 createPatchFunction 函数返回的,所以它也是一个高阶函数。

// src\platforms\web\runtime\patch.js
import { createPatchFunction } from 'core/vdom/patch'export const patch: Function = createPatchFunction({ nodeOps, modules })

它接收两个参数:

  • nodeOps 一些DOM的操作,类似Snabbdom 的 domApi
    • 特殊的地方:对select做了特殊处理(multiple 属性)
  • modules 模块,类似Snabbdom的modules。
    • 它拼接了 platformModules 和 baseModules
      • platformModules 和平台相关的模块 src\platforms\web\runtime\modules\index.js
        • 它和 Snabbdom 中的模块基本一致,都是导出生命周期的钩子函数
        • 多了一个transition,处理过渡动画
      • baseModules 和平台无关的模块 src\core\vdom\modules\index.js
        • 它用来处理 指令 和 ref

createPatchFunction

createPatchFunction 相当于 Snabbdom 的 init。

函数最后返回了 patch 函数。

// src\core\vdom\patch.js
// 类似 Snabbdom 的 init,创建并返回patch函数
export function createPatchFunction (backend) {let i, j// callbacks 与Snabbdom类似,存储模块中定义的钩子函数const cbs = {}// modules 模块:用于操作节点的属性/事件/样式// nodeOps DOM操作的方法const { modules, nodeOps } = backend// 遍历hooks,生命周期钩子函数的名称for (i = 0; i < hooks.length; ++i) {// 把名称作为cbs的属性,初始化为一个数组,收集模块的钩子函数// cbs['update'] = []cbs[hooks[i]] = []for (j = 0; j < modules.length; ++j) {if (isDef(modules[j][hooks[i]])) {// cbs['update'] = [updateAttrs, updateClass, update..]cbs[hooks[i]].push(modules[j][hooks[i]])}}}// ...// 定义并返回patchreturn function patch (oldVnode, vnode, hydrating, removeOnly) {// ...}
}

patch 函数的执行过程

patch 中代码比较多,本节只关注它的核心逻辑。

核心:

  • createElm 将vnode转化为真实DOM,并渲染到视图
    • 处理新增节点
  • patchVnode diff算法对比新旧vnode差异,将差异更新到视图
    • 处理相同节点
/*** 定义并返回patch* @param {*} oldVnode 旧的vnode* @param {*} vnode  新的vnode*/
return function patch (oldVnode, vnode, hydrating, removeOnly) {// 如果新的vnode不存在,判断旧的vnodeif (isUndef(vnode)) {// 如果旧的vnode存在,执行 destroy 钩子函数if (isDef(oldVnode)) invokeDestroyHook(oldVnode)return}let isInitialPatch = false// 存储新插入的vnode节点的队列// 目的是将新插入的vnode挂载到DOM上后,触发 insert钩子函数(invokeInsertHook)const insertedVnodeQueue = []if (isUndef(oldVnode)) {// 如果旧的vnode不存在// 当调用$mount方法,但是没有传参的时候,旧的vnode不存在// 此时表示当前只是把组件创建出来,并不挂载到视图// isInitialPatch为true,记录vnode创建完成,对应的DOM元素也创建完了。// 但是仅仅存储在内存中,没有挂载到DOM树上isInitialPatch = true// createElm 把vnode转化成真实DOM// createElm 的第三个参数是parentElm,当为空的时候,不会把真实DOM挂载到DOM树createElm(vnode, insertedVnodeQueue)} else {// 如果旧vnode存在// nodeType是DOM对象的属性,根据它判断vnode是一个真实DOM// 如果它是真实DOM,说明是首次渲染const isRealElement = isDef(oldVnode.nodeType)if (!isRealElement && sameVnode(oldVnode, vnode)) {// 如果oldVnode不是真实DOM,并且新旧vnode是相同节点// 调用patchVnode:使用 diff 算法对比新旧vnode差异,更新到视图patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)} else {// 如果新旧vnode不是相同节点if (isRealElement) {// 如果oldVnode是真实DOM,说明是首次渲染// 和服务端渲染相关的(暂不关心)if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {//...}// 和服务端渲染相关的(暂不关心)if (isTrue(hydrating)) {//...}// 当前判断的核心目的:把oldVnode(当前是真实DOM)转换成vnodeoldVnode = emptyNodeAt(oldVnode)}// 获取oldVnode中的DOM元素const oldElm = oldVnode.elm// 获取oldVnode对应的DOM元素的父元素,将来用于把vnode转化的真实DOM挂载到父元素const parentElm = nodeOps.parentNode(oldElm)// 把vnode转化成真实DOM// createElm://   1. 创建真实DOM//   2. 把vnode记录到insertedVnodeQueue队列//   3. 把真实DOM插入到 参数4 对应的DOM节点前createElm(vnode,insertedVnodeQueue,// 如果旧元素正处于离开转换 leave transition(从界面消失的过渡动画)// 就不把新创建的DOM元素挂载到DOM树上oldElm._leaveCb ? null : parentElm,nodeOps.nextSibling(oldElm))// 处理父节点的占位符问题(与核心无关,暂不关心)if (isDef(vnode.parent)) {// ...}// destroy old node// 判断parentElm是否存在if (isDef(parentElm)) {// 如果存在,就把oldVnode 从界面移除,并且触发相关钩子函数removeVnodes([oldVnode], 0, 0)} else if (isDef(oldVnode.tag)) {// 如果不存在 parentElm,说明DOM树上没有这个oldVnode// 此时判断如果它有 tag 属性(是一个标签)// 触发destroy钩子函数invokeDestroyHook(oldVnode)}}}// 触发 新插入vnode的insert钩子函数// isInitialPatch为true,表示当前vnode创建的DOM元素没有挂载到DOM树上// 如果DOM元素没有挂载到DOM树,就不执行insert钩子函数invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)// 最后返回新vnode对应的真实DOM元素return vnode.elm
}

emptyNodeAt

把真实DOM转化成一个空的vnode

// 把真实DOM转化成一个空的vnode
function emptyNodeAt (elm) {// nodeOps.tagName获取DOM元素的tagnamereturn new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}

removeVnodes

function removeVnodes (vnodes, startIdx, endIdx) {// 遍历所有节点for (; startIdx <= endIdx; ++startIdx) {// 获取节点const ch = vnodes[startIdx]// 判断节点是否存在if (isDef(ch)) {// 判断是否有tag属性if (isDef(ch.tag)) {// 如果有,说明是一个tag标签// removeAndInvokeRemoveHook:移除这个标签,并触发remove钩子函数// invokeDestroyHook:触发destroy钩子函数removeAndInvokeRemoveHook(ch)invokeDestroyHook(ch)} else { // Text node// 如果没有,说明是一个文本节点// 直接删除节点removeNode(ch.elm)}}}
}

invokeInsertHook

// 触发vnode队列的insert钩子函数
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)) {// 如果DOM元素没有挂载到DOM树,并且它有父节点// 不调用insert钩子函数// 标记当前插入是一个延缓插入的操作(pendingInsert)// 把队列(queue)记录到pendingInsert中// 将来这些元素真正的插入到DOM上之后,才会触发这个队列中每一个vnode的insert钩子函数vnode.parent.data.pendingInsert = queue} else {// 遍历队列中的每一个vnode,触发insert钩子函数for (let i = 0; i < queue.length; ++i) {queue[i].data.hook.insert(queue[i])}}
}

createElm

把vnode转化成真实DOM,然后挂载到DOM树上。

/*** 把vnode转化成真实DOM挂载到DOM树,并触发一些相应的钩子函数* @param {*} vnode 要转化的vnode* @param {*} insertedVnodeQueue 新增vnode的队列,用于触发insert钩子函数* @param {*} parentElm vnode的父元素,会将vnode转化的真实DOM,插入到父元素中* @param {*} refElm 下一个兄弟节点:将vnode转化的真实DOM,插入到这个元素之前*/
function createElm (
vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index
) {// vnode.elm存在表示vnode已经被渲染过// ownerArray表示vnode是否有子节点// 这个判断是为了避免一些潜在的错误(暂不关心)if (isDef(vnode.elm) && isDef(ownerArray)) {// 克隆vnode及它的子节点vnode = ownerArray[index] = cloneVNode(vnode)}vnode.isRootInsert = !nested // for transition enter check// 调用 createComponent 处理组件的情况if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {return}// 获取data children tagconst data = vnode.dataconst children = vnode.childrenconst tag = vnode.tag// 判断3种情况:// 1. vnode是标签:tag存在,表示是标签,因为如果是组件,上面已经处理过了// 2. vnode是注释节点// 3. vnode是文本节点if (isDef(tag)) {if (process.env.NODE_ENV !== 'production') {// 开发环境if (data && data.pre) {creatingElmInVPre++}// 判断tag是否是未知的标签,也就是HTML中不存在的标签if (isUnknownElement(vnode, creatingElmInVPre)) {// 这是个常见的警告:tag是一个自定义标签,你是否正确的注册了对应的组件warn('Unknown custom element: <' + tag + '> - did you ' +'register the component correctly? For recursive components, ' +'make sure to provide the "name" option.',vnode.context)}}// 创建DOM元素,存储到vnode的elm属性中// 判断ns:// 有:创建带命名空间(SVG)的DOM元素// 没有:创建对应的DOM元素vnode.elm = vnode.ns? nodeOps.createElementNS(vnode.ns, tag): nodeOps.createElement(tag, vnode)// setScope会为vnode所对应的DOM元素设置样式的作用域setScope(vnode)/* istanbul ignore if */// 判断是否是weex环境(暂不关心)if (__WEEX__) {// ...} else {// createChildren 把 vnode 中所有的子元素转化成 vnodecreateChildren(vnode, children, insertedVnodeQueue)if (isDef(data)) {// 当前vnode已经创建好了对应的DOM对象// 如果data有值,触发create钩子函数invokeCreateHooks(vnode, insertedVnodeQueue)}// 把创建的DOM对象插入到parentElm中// insert会判断parent为空,不做处理insert(parentElm, vnode.elm, refElm)}if (process.env.NODE_ENV !== 'production' && data && data.pre) {creatingElmInVPre--}} else if (isTrue(vnode.isComment)) {// 如果vnode是注释节点// 创建并赋值为一个注释的dom元素vnode.elm = nodeOps.createComment(vnode.text)// 调用insert 插入到DOM树insert(parentElm, vnode.elm, refElm)} else {// 如果vnode是文本节点// 创建并赋值为一个文本的dom元素vnode.elm = nodeOps.createTextNode(vnode.text)// 调用insert 插入到DOM树insert(parentElm, vnode.elm, refElm)}
}

createChildren

// src\core\vdom\patch.js
function createChildren (vnode, children, insertedVnodeQueue) {if (Array.isArray(children)) {// 如果是数组if (process.env.NODE_ENV !== 'production') {// 开发环境,判断子元素中是否有相同的keycheckDuplicateKeys(children)}// 遍历childrenfor (let i = 0; i < children.length; ++i) {// 把子节点vnode转换成真实DOM,并挂载到DOM树上createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)}} else if (isPrimitive(vnode.text)) {// 如果是原始值// 将值转化成字符串,通过createTextNode创建一个文本DOM元素// 把文本DOM元素插入到vnode.elm中nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))}
}

checkDuplicateKeys

// src\core\vdom\patch.js
function checkDuplicateKeys (children) {// seenKeys存储子元素的keyconst seenKeys = {}// 遍历子元素for (let i = 0; i < children.length; i++) {const vnode = children[i]const key = vnode.keyif (isDef(key)) {if (seenKeys[key]) {// 如果定义了key属性,并且子节点中有重复的key// 发出警告warn(`Duplicate keys detected: '${key}'. This may cause an update error.`,vnode.context)} else {// 记录keyseenKeys[key] = true}}}
}

invokeCreateHooks

// src\core\vdom\patch.js
function invokeCreateHooks (vnode, insertedVnodeQueue) {// 遍历cbs中所有的create钩子函数并调用// cbs存储的是modules模块的钩子函数for (let i = 0; i < cbs.create.length; ++i) {cbs.create[i](emptyNode, vnode)}// 获取用户定义的钩子函数i = vnode.data.hook // Reuse variableif (isDef(i)) {// 如果有create钩子函数,触发if (isDef(i.create)) i.create(emptyNode, vnode)// 如果有insert钩子函数,添加到队列if (isDef(i.insert)) insertedVnodeQueue.push(vnode)}
}

insert

// src\core\vdom\patch.js
// 向元素中插入dom元素
function insert (parent, elm, ref) {// 如果parent未传入,则不作处理if (isDef(parent)) {if (isDef(ref)) {// 如果ref存在,要判断ref的父节点和parent是否相同if (nodeOps.parentNode(ref) === parent) {// 如果相同,就把elm插入到 ref 对应的dom元素之前nodeOps.insertBefore(parent, elm, ref)}} else {// 如果没有ref,就把elm插入到 parentnodeOps.appendChild(parent, elm)}}
}

patchVnode

使用 diff 算法对比新旧节点,将差异更新到视图

// src\core\vdom\patch.js
function patchVnode (oldVnode,vnode,insertedVnodeQueue,ownerArray,index,removeOnly
) {// ... 一些判断(暂不关心)let i// 获取vnode中的dataconst data = vnode.data// 判断如果用户定义了prepatch钩子函数,执行这个钩子函数if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {i(oldVnode, vnode)}// 获取新旧节点的子节点const oldCh = oldVnode.childrenconst ch = vnode.children// 触发update钩子函数if (isDef(data) && isPatchable(vnode)) {// 调用cbs存储的模块的update钩子函数:操作节点的属性/样式/事件等for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)// 调用用户定义的update钩子函数if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)}// patchVnode 的核心:对比新旧vnode的差异// 判断:// 1. 如果新vnode中没有text,就会对比新旧vnode的子节点// 2. 如果新vnode中有text,并且不同于旧vnode的text,直接修改dom的文本内容if (isUndef(vnode.text)) {if (isDef(oldCh) && isDef(ch)) {// 如果新旧vnode都存在子节点,并且不相等// 调用updateChildren对比新旧节点的差异,将差异更新到DOMif (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)} else if (isDef(ch)) {// 如果新vnode有子节点,旧vnode没有// 先检查新vnode的子节点中是否有重复的key,开发环境会发出警告if (process.env.NODE_ENV !== 'production') {checkDuplicateKeys(ch)}// 如果旧vnode有文本内容,先清空文本if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')// 为当前DOM节点加入子节点// addVnodes:把新vnode下的子节点转化成DOM元素,添加到DOM树addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)} else if (isDef(oldCh)) {// 如果旧vnode中有子节点,新vnode没有// 删除旧vnode中的子节点,并触发remove和destroy钩子函数removeVnodes(oldCh, 0, oldCh.length - 1)} else if (isDef(oldVnode.text)) {// 如果新旧vnode都没有子节点// 并且旧vnode有文本内容,新vnode没有文本内容// 清空旧vnode的文本内容nodeOps.setTextContent(elm, '')}} else if (oldVnode.text !== vnode.text) {// 新vnode有text,并且与旧的不同,修改文本nodeOps.setTextContent(elm, vnode.text)}// patch过程执行完毕,触发postPatch钩子函数if (isDef(data)) {if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)}
}

addVnodes

// src\core\vdom\patch.js
function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {// 遍历指定范围的子节点,转化成真实DOM,挂载到DOM树for (; startIdx <= endIdx; ++startIdx) {createElm(vnodes[startIdx], insertedVnodeQueue, parentElm, refElm, false, vnodes, startIdx)}}

updateChildren

对比相同节点的子节点,找到它们的差异,更新到DOM树。

如果节点没有发生变化,会重用该节点(key的使用)。

查看源码发现 updateChildren 的执行过程和Snabbdom中是一样的。

// diff 算法
// 更新新旧vnode的子节点
// 子节点都是以数组形式传入
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {// 初始化:// 1. 新旧子节点的结束索引// 2. 新旧子节点的开始索引// 3. 新旧开始子节点// 4. 新旧结束子节点// 5. 其他辅助变量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 <transition-group>// to ensure removed elements stay in correct relative positions// during leaving transitionsconst canMove = !removeOnly// 检查新节点的子节点中是否有重复的key,发出警告if (process.env.NODE_ENV !== 'production') {checkDuplicateKeys(newCh)}// diff算法// 循环中止条件:新节点或旧节点遍历完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对比差异,并更新到视图patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)// 更新索引和开始节点oldStartVnode = oldCh[++oldStartIdx]newStartVnode = newCh[++newStartIdx]} else if (sameVnode(oldEndVnode, newEndVnode)) {// 判断 新旧结束节点 相同// 调用patchVnode对比差异,并更新到视图patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)// 更新索引和结束节点oldEndVnode = oldCh[--oldEndIdx]newEndVnode = newCh[--newEndIdx]} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right// 判断 旧开始节点和新结束节点 相同// 调用patchVnode对比差异,并更新到视图patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)// 移动旧开始节点的位置canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))// 更新索引和两个节点oldStartVnode = oldCh[++oldStartIdx]newEndVnode = newCh[--newEndIdx]} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left// 判断 旧结束节点和新开始节点 相同// 调用patchVnode对比差异,并更新到视图patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)// 移动旧结束节点的位置canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)oldEndVnode = oldCh[--oldEndIdx]newStartVnode = newCh[++newStartIdx]// 如果对比顶点节点都不相同// 就去旧节点中寻找与新开始节点的key相同的节点} else {if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)// 如果新开始节点有key,则用key去寻找// 如果新开始节点没有key,则遍历旧节点,调用 sameVnode 寻找与新开始节点相同的节点idxInOld = isDef(newStartVnode.key)? oldKeyToIdx[newStartVnode.key]: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)if (isUndef(idxInOld)) { // New element// 如果没有找到相同节点// 调用 createElm 把新开始节点转换成真实DOM,更新到旧开始节点对应的dom元素前createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)} else {// 如果找到key相同的节点,还要调用 sameVnode 再确认一下是否是相同节点// 先拷贝一下旧节点(将要移动的节点),稍后要置空这个oldCh中的这个节点vnodeToMove = oldCh[idxInOld]if (sameVnode(vnodeToMove, newStartVnode)) {// 如果是相同节点,调用patchVnodepatchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)// 把旧节点设置为 undefined并移动到旧开始节点前oldCh[idxInOld] = undefinedcanMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)} else {// 如果不是相同节点(key相同,但是是不同的元素),表示是新元素,调用createElm// 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(oldCh, oldStartIdx, oldEndIdx)}
}

key 的作用

Vue 官方文档(维护状态)中讲到,在v-for中为每个节点添加一个key属性,可以跟踪每个节点的身份,从而重用和重新排序现有元素。

在使用 Vue CLI创建的项目中,如果没有给v-for的节点设置key,还会发出警告。

通过调试代码查看没有设置key和设置key的区别。

<div id="app"><button @click="handler">按钮</button><ul><!-- 没有设置key --><li v-for="value in arr">{{value}}</li><!-- 设置key --><!-- <li v-for="value in arr" :key="value">{{value}}</li> --></ul>
</div>
<script src="../../dist/vue.js"></script>
<script>const vm = new Vue({el: "#app",data: {arr: ['a', 'b', 'c', 'd']},methods: {handler() {this.arr.splice(1, 0, 'x')// this.arr = ['a', 'x', 'b', 'c', 'd']}}});
</script>

断点位置:

  1. src/core/vdom/patch.js :updateChildren定义的位置。
  2. while 循环:对比新旧节点差异的位置

调试结果:

  • 没有设置key的情况下
    • 修改了3次DOM,插入了1次DOM
      • 由于新旧节点的key都相同(undefined),以及其他条件判断得到新旧节点是相同节点,所以对它们都进行了文本修改
        • b -> x
        • c -> b
        • d -> c
      • 最后旧节点遍历结束,调用addVnodes,插入文本为 d 的节点
  • 设置key的情况下
    • 插入了1次DOM,没有修改任何DOM
      • 用于新增节点影响了后面元素的索引
      • 先对比开始节点为不同节点,然后对比结束节点是相同节点
      • 由于内容相同,不做处理
      • 最后旧节点遍历结束,调用addVnodes,插入文本为 x 的节点

设置key的DOM操作要比没有设置key的操作少很多。

总结 - 和虚拟DOM相关的过程

  1. _render 创建虚拟DOM并返回,最终传入 _update
    1. vnode = render.call(vm._renderProxy, vm.$createElement)
      1. 调用了用户传入的render或编译生成的render函数
    2. 如果是用户传入的render就调用 vm.$createElement
    3. 如果是template编译的render,内部编译成调用 vm._c 的函数
    4. $createElement 和 _c 都是调用 vm._createElement
    5. vm._createElement
      1. new VNode创建虚拟节点并返回
    6. _render结束,返回vnode,交给_update去处理
  2. _update 调用__patch__,负责把虚拟DOM,渲染成真实DOM
    1. 首次执行
      1. vm.__patch__(vm.$el, vnode, hydrating, false)
      2. 第一个参数是真实DOM
    2. 数据更新
      1. vm.__patch__(preVnode, vnode)
      2. 第一个参数是上一次渲染时保存的虚拟DOM
  3. vm.__patch__
    1. src\platforms\web\runtime\index.js 中初始化
      1. 挂载到Vue.prototype.__patch__
      2. 它其实是 src\core\vdom\patch.js中createPatchFunction 导出的 patch 函数
    2. createPatchFunction
      1. 设置并缓存了
        1. modules 和平台相关以及和平台无关的模块
        2. nodeOps 定义了操作DOM的方法
      2. 初始化了 cbs
        1. 存储了所有模块中定义的钩子函数
        2. 这些钩子函数的作用是用来处理节点的属性/事件/样式等
      3. 返回patch函数
  4. patch
    1. 判断第一个参数是否是真实DOM
      1. 如果是真实DOM,那就是首次渲染
        1. 将新节点转化成真实DOM,更新到视图
          1. 调用 createElm
        2. 将旧的DOM元素转换成一个空的vnode,直接从界面移除,触发相关钩子函数
      2. 如果不是真实DOM,并且新旧节点是相同节点,说明是数据更新
        1. 调用patchVnode对比差异,将差异更新到视图
    2. 最后触发insert钩子函数
  5. createElm 把虚拟节点及子节点,转换成真实DOM,并插入到DOM树,还会触发相应的钩子函数
  6. patchVnode 对比新旧vnode,以及子节点的差异,并更新到视图
    1. 如果新旧vnode都有子节点,并且子节点不同的话就调用 updateChildren 对比子节点差异
  7. updateChildren diff算法对比新旧子节点的差异,并更新到视图
    1. 循环遍历判断
      1. 循环遍历判断1:从头和尾开始依次找到相同的子节点进行比较 patchVnode,总共有四种比较方式
        1. 头头对比
        2. 尾尾对比
        3. 头尾对比
        4. 尾头对比
      2. 循环遍历判断2:在老节点的子节点中查找与 newStartVnode 相同key的节点,辅以 sameVnode判断是否是相同节点,并进行处理
    2. 循环结束后处理
      1. 批量添加新增节点
      2. 批量删除旧节点中多余的节点
查看全文
如若内容造成侵权/违法违规/事实不符,请联系编程学习网邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!

相关文章

  1. dijkstra算法求最短路

    #include<iostream> #include<algorithm> #include<cstring> using namespace std;const int N=510;int g[N][N]; //为稠密阵所以用邻接矩阵存储 int dist[N]; //用于记录每一个点距离第一个点的距离 bool st[N]; //用于记录该点的最短距离是否已经…...

    2024/3/29 4:36:27
  2. Apache Pivot教程 -- ListButton

    Apache Pivot教程 ListButton,即下拉列表,在HTML里面是<select>标签 ListButton也是Button的成员,可以使用Button类的方法 ListButton有几个独有的方法 void clearSelection();//清除选择项 void setListData(List<>);//设置列表内容 void setListData(String);…...

    2024/4/12 12:43:00
  3. C++ bitset 用法

    1.构造函数bitset 有3种常用的构造函数bitset<4> bitset1; // 无参构造,长度为4,默认为0 bitset<8> bitset2(12); // 长度为8,以二进制的形式将12进行保存,前面用0补充string s = "100101"; bitset<10> bitset3(s); // 长度为10前面用0补充,用…...

    2024/3/29 4:36:25
  4. 北斗/GPS模块输出的NMEA 0183语句详解

    NMEA - 0183 是美国国家海洋电子协会(National Marine Electronics Association)为海用电子设备制定的标准格式。目前业已成了 GPS/北斗导航设备统一的 RTCM(Radio Technical Commission for Maritime services)标准协议。 NMEA-0183 协议采用 ASCII 码来传递 GPS 定位信息…...

    2024/4/17 23:38:51
  5. 【剑指offer】面试题18:删除链表的节点

    方法一:书上图3.3的(b) /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode(int x) : val(x), next(NULL) {}* };*/ class Solution { public:ListNode* deleteNode(ListNode* head, int val) {ListNode *pList…...

    2024/3/29 4:36:23
  6. 1016 部分A+B (15分)(C语言)

    1016 部分A+B (15分) 正整数 A 的“D ​A ​​ (为 1 位整数)部分”定义为由 A 中所有 D ​A ​​ 组成的新整数 P ​A ​​ 。例如:给定 A=3862767,D ​A ​​ =6,则 A 的“6 部分”P ​A ​​ 是 66,因为 A 中有 2 个 6。 现给定 A、D ​A ​​ 、B、D ​B ​​ ,请…...

    2024/4/8 6:07:29
  7. centos7不能使用yum命令,离线安装yum(-bash: yum: command not found )

    1、删除现有Python rpm -qa|grep python|xargs rpm -ev --allmatches --nodeps ##强制删除已安装程序及其关联 whereis python |xargs rm -frv -rf ##删除所有残余文件 ##xargs,允许你对输出执行其他某些命令whereis python ##验证删除,返回无结果``## 2、删除现有的yum````j…...

    2024/3/29 4:36:21
  8. Qt - 电子时钟QTime、QTimer、QLCDNumber

    电子时钟QTime、QTimer、QLCDNumber 新建桌面应用程序,项目名LCDClock,类名Clock,基类QDialog,取消产生界面文件 当前项目添加C++类DigitalClock,基类QLCDNumber 编辑digitalclock.h文件Digitalclock.cpp编辑构造函数及添加事件定义编辑main.cpp文件具体操作结语: 时间: 2020…...

    2024/3/29 4:36:20
  9. GitHub注册安装与使用

    1.注册与登录 1.1点击超链接进入注册页面,如下图 注册,https://github.com/join?source=login登录,https://github.com/login 2.设置SSH 步骤一:设置git全局账号 git config --global user.name “用户名” git config --global user.email “yonhuming@itcast.cn”步骤二:删…...

    2024/3/29 4:36:19
  10. vsftpd服务配置

    vsftpd服务配置ftp简介vsftpd服务程序FTP配置实践安装FTP服务程序防火墙设置切换至internal区域,并开启ftp服务保存原始ftp配置文件匿名开放模式坑一坑二坑三结果显示匿名模式总结本地用户模式配置vsftpd.conf文件查看结果虚拟用户模式创建FTP认证的用户数据库文件hash加密数据…...

    2024/3/29 4:36:18
  11. Redis常见面试问题汇总及解析

    面试后端开发的职位,相信大家经常被问到有关redis问题。Redis作为缓存系统的代表很有必要弄熟搞懂,无论是在工作当中还是求职面试过程中都是大有裨益的,本文将详细介绍一些redis的一些典型问题,并给出了一些参考解答。由于作者水平有限,可能会有存在一些问题,欢迎大家不吝…...

    2024/4/13 7:27:26
  12. spring的 DispatcherServlet 中 /和/*的区别

    其中/和/*的区别: < url-pattern > / </ url-pattern > 不会匹配到*.jsp,即:*.jsp不会进入spring的 DispatcherServlet类 。 < url-pattern > /* </ url-pattern > 会匹配*.jsp,会出现返回jsp视图时再次进入spring的DispatcherServlet 类,导致找…...

    2024/3/29 4:36:16
  13. JAVA并发-AtomicLong

    AtomicLong 类提供了long类型的变量与AtomicInteger非常类似,变量可以原子写和读,同时还包括先进的原子操作例如 compareAndSet()。 AtomicLong 类位于java.util.concurrent.atomic 包中,全名java.util.concurrent.atomic.AtomicLong 。本文讲述JAVA8中的AtomicLong ,但是第…...

    2024/4/19 19:42:01
  14. RestTemplate整合Ribbon

    本次项目在父工程上搭建三个工程 父工程pom<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.1.0.RELEASE</version><relativePath/> <!-- lookup par…...

    2024/3/29 0:41:18
  15. 【TensorFlow】(四)tf.feature_column.embedding_column()函数

    1.作用将sparse/categrical特征转化为dense 向量。输入:categorical_column一个CategoricalColumn创建由categorical_column_with_*功能。此列产生稀疏的ID为输入给嵌入查找。dimension一个整数,指定嵌入的尺寸,必须> 0。combiner如何指定一个字符串,以减少是否有单排多…...

    2024/3/29 0:41:17
  16. 传说中四个月的java速成“大法”,我见识到了,不过就是没啥用

    踩过坑的过来人,当时招生的老师告诉我一般要半年到八个月,我TM居然信!! 明确回答 不能没有基础的人,系统学完java的时间几乎是固定的,比如说需要一年,如果要想半年教完怎么办呢,两种办法,只讲一半的内容,或者双倍进度,而双倍进度往往意味着如果你不是每天熬夜,零基…...

    2024/3/29 0:41:16
  17. 详解SSM环境的搭建(上)

    1.简介 SSM(Spring+SpringMVC+MyBatis)框架集由Spring、MyBatis两个开源框架整合而成(SpringMVC是Spring中的部分内容)。常作为数据源较简单的web项目的框架。Spring就像是整个项目中装配bean的大工厂,在配置文件中可以指定使用特定的参数去调用实体类的构造方法来实例化对…...

    2024/3/29 0:41:15
  18. 出现k次与出现1次

    解法1; HashMap public static int getOnce2(int[] nums) {Map<Integer, Integer> map = new HashMap<Integer, Integer>();for (int i = 0; i < nums.length; i++) {if(map.containsKey(nums[i])) {int num = map.get(nums[i]);num++;map.put(nums[i], num);}…...

    2024/3/29 0:41:14
  19. int和String相互转换

    String转换为int 1.int i = Integer.parseInt(String); 2.int i = Integer.valueOf(s).intValue(); int转换为String 1.String s = String.valueOf(i); 2.String s = Integer.toString(i); 3.String s = " " + i;...

    2024/3/31 3:55:58
  20. 使用pip安装python第三方库

    利用pip安装python第三方库的前提条件是你的pip达到最新版本 关于如何升级pip: https://blog.csdn.net/hack_C/article/details/107966957 利用pip install requests检查能否导入第三方库提示successfully则表明成功。 由于国内网络原因,在cmd中安装第三方库,网络不好经常导…...

    2024/3/29 0:41:12

最新文章

  1. 产品经理常用工具汇总

    英文名称中文名称描述Axure原型原型图&#xff0c;流程图&#xff0c;框架图&#xff0c;原型图&#xff1b;Axhub团队原型共享Axure原型团队共享&#xff0c;链接转发&#xff1b;iconfont阿里矢量图标图标下载&#xff0c;协助原型和方案&#xff1b;visio流程图 业务流程图&…...

    2024/4/20 11:48:38
  2. 梯度消失和梯度爆炸的一些处理方法

    在这里是记录一下梯度消失或梯度爆炸的一些处理技巧。全当学习总结了如有错误还请留言&#xff0c;在此感激不尽。 权重和梯度的更新公式如下&#xff1a; w w − η ⋅ ∇ w w w - \eta \cdot \nabla w ww−η⋅∇w 个人通俗的理解梯度消失就是网络模型在反向求导的时候出…...

    2024/3/20 10:50:27
  3. JS中空合并运算符 ?? 的使用

    什么是空合并运算符&#xff1f; 空合并运算符 ?? (Nullish coalescing operator) 是一个逻辑运算符&#xff0c;当其左侧操作数为 null 或 undefined 时&#xff0c;它返回其右侧操作数&#xff0c;否则返回其左侧操作数 const foo null ?? default string; console.lo…...

    2024/4/16 3:58:54
  4. 深入理解springboot

    第五章 访问数据库 1.配置数据源 在applicaiton.properties中 spring.datasource.urljdbc:mysql://localhost:3306/chapter5 第九章 springmvc 1.总体流程 http请求发送给控制器&#xff0c;控制器与业务层交互&#xff0c;业务层使用noSQL缓存&#xff0c;业务层与数据访问层…...

    2024/4/18 20:01:15
  5. 416. 分割等和子集问题(动态规划)

    题目 题解 class Solution:def canPartition(self, nums: List[int]) -> bool:# badcaseif not nums:return True# 不能被2整除if sum(nums) % 2 ! 0:return False# 状态定义&#xff1a;dp[i][j]表示当背包容量为j&#xff0c;用前i个物品是否正好可以将背包填满&#xff…...

    2024/4/19 19:02:10
  6. 【Java】ExcelWriter自适应宽度工具类(支持中文)

    工具类 import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet;/*** Excel工具类** author xiaoming* date 2023/11/17 10:40*/ public class ExcelUti…...

    2024/4/19 11:51:49
  7. Spring cloud负载均衡@LoadBalanced LoadBalancerClient

    LoadBalance vs Ribbon 由于Spring cloud2020之后移除了Ribbon&#xff0c;直接使用Spring Cloud LoadBalancer作为客户端负载均衡组件&#xff0c;我们讨论Spring负载均衡以Spring Cloud2020之后版本为主&#xff0c;学习Spring Cloud LoadBalance&#xff0c;暂不讨论Ribbon…...

    2024/4/19 11:33:34
  8. TSINGSEE青犀AI智能分析+视频监控工业园区周界安全防范方案

    一、背景需求分析 在工业产业园、化工园或生产制造园区中&#xff0c;周界防范意义重大&#xff0c;对园区的安全起到重要的作用。常规的安防方式是采用人员巡查&#xff0c;人力投入成本大而且效率低。周界一旦被破坏或入侵&#xff0c;会影响园区人员和资产安全&#xff0c;…...

    2024/4/19 11:52:08
  9. VB.net WebBrowser网页元素抓取分析方法

    在用WebBrowser编程实现网页操作自动化时&#xff0c;常要分析网页Html&#xff0c;例如网页在加载数据时&#xff0c;常会显示“系统处理中&#xff0c;请稍候..”&#xff0c;我们需要在数据加载完成后才能继续下一步操作&#xff0c;如何抓取这个信息的网页html元素变化&…...

    2024/4/20 9:42:32
  10. 【Objective-C】Objective-C汇总

    方法定义 参考&#xff1a;https://www.yiibai.com/objective_c/objective_c_functions.html Objective-C编程语言中方法定义的一般形式如下 - (return_type) method_name:( argumentType1 )argumentName1 joiningArgument2:( argumentType2 )argumentName2 ... joiningArgu…...

    2024/4/20 7:01:14
  11. 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】

    &#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5713-洛谷团队系统【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格…...

    2024/4/19 11:52:49
  12. 【ES6.0】- 扩展运算符(...)

    【ES6.0】- 扩展运算符... 文章目录 【ES6.0】- 扩展运算符...一、概述二、拷贝数组对象三、合并操作四、参数传递五、数组去重六、字符串转字符数组七、NodeList转数组八、解构变量九、打印日志十、总结 一、概述 **扩展运算符(...)**允许一个表达式在期望多个参数&#xff0…...

    2024/4/19 18:52:15
  13. 摩根看好的前智能硬件头部品牌双11交易数据极度异常!——是模式创新还是饮鸩止渴?

    文 | 螳螂观察 作者 | 李燃 双11狂欢已落下帷幕&#xff0c;各大品牌纷纷晒出优异的成绩单&#xff0c;摩根士丹利投资的智能硬件头部品牌凯迪仕也不例外。然而有爆料称&#xff0c;在自媒体平台发布霸榜各大榜单喜讯的凯迪仕智能锁&#xff0c;多个平台数据都表现出极度异常…...

    2024/4/19 23:08:02
  14. Go语言常用命令详解(二)

    文章目录 前言常用命令go bug示例参数说明 go doc示例参数说明 go env示例 go fix示例 go fmt示例 go generate示例 总结写在最后 前言 接着上一篇继续介绍Go语言的常用命令 常用命令 以下是一些常用的Go命令&#xff0c;这些命令可以帮助您在Go开发中进行编译、测试、运行和…...

    2024/4/20 0:22:23
  15. 用欧拉路径判断图同构推出reverse合法性:1116T4

    http://cplusoj.com/d/senior/p/SS231116D 假设我们要把 a a a 变成 b b b&#xff0c;我们在 a i a_i ai​ 和 a i 1 a_{i1} ai1​ 之间连边&#xff0c; b b b 同理&#xff0c;则 a a a 能变成 b b b 的充要条件是两图 A , B A,B A,B 同构。 必要性显然&#xff0…...

    2024/4/19 23:04:54
  16. 【NGINX--1】基础知识

    1、在 Debian/Ubuntu 上安装 NGINX 在 Debian 或 Ubuntu 机器上安装 NGINX 开源版。 更新已配置源的软件包信息&#xff0c;并安装一些有助于配置官方 NGINX 软件包仓库的软件包&#xff1a; apt-get update apt install -y curl gnupg2 ca-certificates lsb-release debian-…...

    2024/4/20 1:12:38
  17. Hive默认分割符、存储格式与数据压缩

    目录 1、Hive默认分割符2、Hive存储格式3、Hive数据压缩 1、Hive默认分割符 Hive创建表时指定的行受限&#xff08;ROW FORMAT&#xff09;配置标准HQL为&#xff1a; ... ROW FORMAT DELIMITED FIELDS TERMINATED BY \u0001 COLLECTION ITEMS TERMINATED BY , MAP KEYS TERMI…...

    2024/4/19 3:53:57
  18. 【论文阅读】MAG:一种用于航天器遥测数据中有效异常检测的新方法

    文章目录 摘要1 引言2 问题描述3 拟议框架4 所提出方法的细节A.数据预处理B.变量相关分析C.MAG模型D.异常分数 5 实验A.数据集和性能指标B.实验设置与平台C.结果和比较 6 结论 摘要 异常检测是保证航天器稳定性的关键。在航天器运行过程中&#xff0c;传感器和控制器产生大量周…...

    2024/4/19 19:50:16
  19. --max-old-space-size=8192报错

    vue项目运行时&#xff0c;如果经常运行慢&#xff0c;崩溃停止服务&#xff0c;报如下错误 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory 因为在 Node 中&#xff0c;通过JavaScript使用内存时只能使用部分内存&#xff08;64位系统&…...

    2024/4/20 1:43:00
  20. 基于深度学习的恶意软件检测

    恶意软件是指恶意软件犯罪者用来感染个人计算机或整个组织的网络的软件。 它利用目标系统漏洞&#xff0c;例如可以被劫持的合法软件&#xff08;例如浏览器或 Web 应用程序插件&#xff09;中的错误。 恶意软件渗透可能会造成灾难性的后果&#xff0c;包括数据被盗、勒索或网…...

    2024/4/19 11:54:11
  21. JS原型对象prototype

    让我简单的为大家介绍一下原型对象prototype吧&#xff01; 使用原型实现方法共享 1.构造函数通过原型分配的函数是所有对象所 共享的。 2.JavaScript 规定&#xff0c;每一个构造函数都有一个 prototype 属性&#xff0c;指向另一个对象&#xff0c;所以我们也称为原型对象…...

    2024/4/19 23:35:17
  22. C++中只能有一个实例的单例类

    C中只能有一个实例的单例类 前面讨论的 President 类很不错&#xff0c;但存在一个缺陷&#xff1a;无法禁止通过实例化多个对象来创建多名总统&#xff1a; President One, Two, Three; 由于复制构造函数是私有的&#xff0c;其中每个对象都是不可复制的&#xff0c;但您的目…...

    2024/4/19 10:00:05
  23. python django 小程序图书借阅源码

    开发工具&#xff1a; PyCharm&#xff0c;mysql5.7&#xff0c;微信开发者工具 技术说明&#xff1a; python django html 小程序 功能介绍&#xff1a; 用户端&#xff1a; 登录注册&#xff08;含授权登录&#xff09; 首页显示搜索图书&#xff0c;轮播图&#xff0…...

    2024/4/20 6:45:17
  24. 电子学会C/C++编程等级考试2022年03月(一级)真题解析

    C/C++等级考试(1~8级)全部真题・点这里 第1题:双精度浮点数的输入输出 输入一个双精度浮点数,保留8位小数,输出这个浮点数。 时间限制:1000 内存限制:65536输入 只有一行,一个双精度浮点数。输出 一行,保留8位小数的浮点数。样例输入 3.1415926535798932样例输出 3.1…...

    2024/4/20 3:28:04
  25. 配置失败还原请勿关闭计算机,电脑开机屏幕上面显示,配置失败还原更改 请勿关闭计算机 开不了机 这个问题怎么办...

    解析如下&#xff1a;1、长按电脑电源键直至关机&#xff0c;然后再按一次电源健重启电脑&#xff0c;按F8健进入安全模式2、安全模式下进入Windows系统桌面后&#xff0c;按住“winR”打开运行窗口&#xff0c;输入“services.msc”打开服务设置3、在服务界面&#xff0c;选中…...

    2022/11/19 21:17:18
  26. 错误使用 reshape要执行 RESHAPE,请勿更改元素数目。

    %读入6幅图像&#xff08;每一幅图像的大小是564*564&#xff09; f1 imread(WashingtonDC_Band1_564.tif); subplot(3,2,1),imshow(f1); f2 imread(WashingtonDC_Band2_564.tif); subplot(3,2,2),imshow(f2); f3 imread(WashingtonDC_Band3_564.tif); subplot(3,2,3),imsho…...

    2022/11/19 21:17:16
  27. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机...

    win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”问题的解决方法在win7系统关机时如果有升级系统的或者其他需要会直接进入一个 等待界面&#xff0c;在等待界面中我们需要等待操作结束才能关机&#xff0c;虽然这比较麻烦&#xff0c;但是对系统进行配置和升级…...

    2022/11/19 21:17:15
  28. 台式电脑显示配置100%请勿关闭计算机,“准备配置windows 请勿关闭计算机”的解决方法...

    有不少用户在重装Win7系统或更新系统后会遇到“准备配置windows&#xff0c;请勿关闭计算机”的提示&#xff0c;要过很久才能进入系统&#xff0c;有的用户甚至几个小时也无法进入&#xff0c;下面就教大家这个问题的解决方法。第一种方法&#xff1a;我们首先在左下角的“开始…...

    2022/11/19 21:17:14
  29. win7 正在配置 请勿关闭计算机,怎么办Win7开机显示正在配置Windows Update请勿关机...

    置信有很多用户都跟小编一样遇到过这样的问题&#xff0c;电脑时发现开机屏幕显现“正在配置Windows Update&#xff0c;请勿关机”(如下图所示)&#xff0c;而且还需求等大约5分钟才干进入系统。这是怎样回事呢&#xff1f;一切都是正常操作的&#xff0c;为什么开时机呈现“正…...

    2022/11/19 21:17:13
  30. 准备配置windows 请勿关闭计算机 蓝屏,Win7开机总是出现提示“配置Windows请勿关机”...

    Win7系统开机启动时总是出现“配置Windows请勿关机”的提示&#xff0c;没过几秒后电脑自动重启&#xff0c;每次开机都这样无法进入系统&#xff0c;此时碰到这种现象的用户就可以使用以下5种方法解决问题。方法一&#xff1a;开机按下F8&#xff0c;在出现的Windows高级启动选…...

    2022/11/19 21:17:12
  31. 准备windows请勿关闭计算机要多久,windows10系统提示正在准备windows请勿关闭计算机怎么办...

    有不少windows10系统用户反映说碰到这样一个情况&#xff0c;就是电脑提示正在准备windows请勿关闭计算机&#xff0c;碰到这样的问题该怎么解决呢&#xff0c;现在小编就给大家分享一下windows10系统提示正在准备windows请勿关闭计算机的具体第一种方法&#xff1a;1、2、依次…...

    2022/11/19 21:17:11
  32. 配置 已完成 请勿关闭计算机,win7系统关机提示“配置Windows Update已完成30%请勿关闭计算机”的解决方法...

    今天和大家分享一下win7系统重装了Win7旗舰版系统后&#xff0c;每次关机的时候桌面上都会显示一个“配置Windows Update的界面&#xff0c;提示请勿关闭计算机”&#xff0c;每次停留好几分钟才能正常关机&#xff0c;导致什么情况引起的呢&#xff1f;出现配置Windows Update…...

    2022/11/19 21:17:10
  33. 电脑桌面一直是清理请关闭计算机,windows7一直卡在清理 请勿关闭计算机-win7清理请勿关机,win7配置更新35%不动...

    只能是等着&#xff0c;别无他法。说是卡着如果你看硬盘灯应该在读写。如果从 Win 10 无法正常回滚&#xff0c;只能是考虑备份数据后重装系统了。解决来方案一&#xff1a;管理员运行cmd&#xff1a;net stop WuAuServcd %windir%ren SoftwareDistribution SDoldnet start WuA…...

    2022/11/19 21:17:09
  34. 计算机配置更新不起,电脑提示“配置Windows Update请勿关闭计算机”怎么办?

    原标题&#xff1a;电脑提示“配置Windows Update请勿关闭计算机”怎么办&#xff1f;win7系统中在开机与关闭的时候总是显示“配置windows update请勿关闭计算机”相信有不少朋友都曾遇到过一次两次还能忍但经常遇到就叫人感到心烦了遇到这种问题怎么办呢&#xff1f;一般的方…...

    2022/11/19 21:17:08
  35. 计算机正在配置无法关机,关机提示 windows7 正在配置windows 请勿关闭计算机 ,然后等了一晚上也没有关掉。现在电脑无法正常关机...

    关机提示 windows7 正在配置windows 请勿关闭计算机 &#xff0c;然后等了一晚上也没有关掉。现在电脑无法正常关机以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;关机提示 windows7 正在配…...

    2022/11/19 21:17:05
  36. 钉钉提示请勿通过开发者调试模式_钉钉请勿通过开发者调试模式是真的吗好不好用...

    钉钉请勿通过开发者调试模式是真的吗好不好用 更新时间:2020-04-20 22:24:19 浏览次数:729次 区域: 南阳 > 卧龙 列举网提醒您:为保障您的权益,请不要提前支付任何费用! 虚拟位置外设器!!轨迹模拟&虚拟位置外设神器 专业用于:钉钉,外勤365,红圈通,企业微信和…...

    2022/11/19 21:17:05
  37. 配置失败还原请勿关闭计算机怎么办,win7系统出现“配置windows update失败 还原更改 请勿关闭计算机”,长时间没反应,无法进入系统的解决方案...

    前几天班里有位学生电脑(windows 7系统)出问题了&#xff0c;具体表现是开机时一直停留在“配置windows update失败 还原更改 请勿关闭计算机”这个界面&#xff0c;长时间没反应&#xff0c;无法进入系统。这个问题原来帮其他同学也解决过&#xff0c;网上搜了不少资料&#x…...

    2022/11/19 21:17:04
  38. 一个电脑无法关闭计算机你应该怎么办,电脑显示“清理请勿关闭计算机”怎么办?...

    本文为你提供了3个有效解决电脑显示“清理请勿关闭计算机”问题的方法&#xff0c;并在最后教给你1种保护系统安全的好方法&#xff0c;一起来看看&#xff01;电脑出现“清理请勿关闭计算机”在Windows 7(SP1)和Windows Server 2008 R2 SP1中&#xff0c;添加了1个新功能在“磁…...

    2022/11/19 21:17:03
  39. 请勿关闭计算机还原更改要多久,电脑显示:配置windows更新失败,正在还原更改,请勿关闭计算机怎么办...

    许多用户在长期不使用电脑的时候&#xff0c;开启电脑发现电脑显示&#xff1a;配置windows更新失败&#xff0c;正在还原更改&#xff0c;请勿关闭计算机。。.这要怎么办呢&#xff1f;下面小编就带着大家一起看看吧&#xff01;如果能够正常进入系统&#xff0c;建议您暂时移…...

    2022/11/19 21:17:02
  40. 还原更改请勿关闭计算机 要多久,配置windows update失败 还原更改 请勿关闭计算机,电脑开机后一直显示以...

    配置windows update失败 还原更改 请勿关闭计算机&#xff0c;电脑开机后一直显示以以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容&#xff0c;让我们赶快一起来看一下吧&#xff01;配置windows update失败 还原更改 请勿关闭计算机&#x…...

    2022/11/19 21:17:01
  41. 电脑配置中请勿关闭计算机怎么办,准备配置windows请勿关闭计算机一直显示怎么办【图解】...

    不知道大家有没有遇到过这样的一个问题&#xff0c;就是我们的win7系统在关机的时候&#xff0c;总是喜欢显示“准备配置windows&#xff0c;请勿关机”这样的一个页面&#xff0c;没有什么大碍&#xff0c;但是如果一直等着的话就要两个小时甚至更久都关不了机&#xff0c;非常…...

    2022/11/19 21:17:00
  42. 正在准备配置请勿关闭计算机,正在准备配置windows请勿关闭计算机时间长了解决教程...

    当电脑出现正在准备配置windows请勿关闭计算机时&#xff0c;一般是您正对windows进行升级&#xff0c;但是这个要是长时间没有反应&#xff0c;我们不能再傻等下去了。可能是电脑出了别的问题了&#xff0c;来看看教程的说法。正在准备配置windows请勿关闭计算机时间长了方法一…...

    2022/11/19 21:16:59
  43. 配置失败还原请勿关闭计算机,配置Windows Update失败,还原更改请勿关闭计算机...

    我们使用电脑的过程中有时会遇到这种情况&#xff0c;当我们打开电脑之后&#xff0c;发现一直停留在一个界面&#xff1a;“配置Windows Update失败&#xff0c;还原更改请勿关闭计算机”&#xff0c;等了许久还是无法进入系统。如果我们遇到此类问题应该如何解决呢&#xff0…...

    2022/11/19 21:16:58
  44. 如何在iPhone上关闭“请勿打扰”

    Apple’s “Do Not Disturb While Driving” is a potentially lifesaving iPhone feature, but it doesn’t always turn on automatically at the appropriate time. For example, you might be a passenger in a moving car, but your iPhone may think you’re the one dri…...

    2022/11/19 21:16:57