vue2.x源码解读:数据驱动

在说数据驱动之前先很大家讲一下vue的两种编译模式,不看源码是真的不晓得🤔这两种编译模式到底是有怎样的区别噻,想知道就往下⬇️:

Runtime Only与 Runtime+compuler编译方式

Runtime Only的优点:压缩体积小,运行速度快.

只能识别render函数,不能识别template。.vue文件中的也是被 vue-template-compiler 翻译成了 render函数,所以只能在.vue里写 template。

vue对template的解析方式:template->ast->render->虚拟dom->真实dom。需要5步才能将内容展示给用户,使用runtime-only则会省略前面两步。

在runtimeOnly中,render会调用一个函数创建元素:

new Vue({
el:'#app',
router,
render:createElement=>{
	return createElement(
		'div',
		{class:'div'},
		['wo',
		 createElement('h2',["ni"])]
		)
	}
})

Runtime-only:

new Vue({
	el:"#app",
	router,
	render:h=>h(App)
})

Runtime-compiler:

new Vue({
 el:'#app',
 template:'<App/>',
 components:{App}
})

数据驱动

数据驱动:是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作DOM,而是通过修改数据。

弄懂模版和数据如何渲染成最终的DOM

vue初始化及挂载

vue初始化 :new Vue讲解

从下面的源码可以看出来,Vue只能通过new进行初始化。【路径:src/core/instance/index.js】

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

初始化的时候用_init方法进行初始化:_init中的源码【路径:src/core/instance/init.js】,我们主要关注initMixin中的_init方法:

    initLifecycle(vm) //初始化生命周期
    initEvents(vm) //初始化事件
    initRender(vm) //初始化渲染
    callHook(vm, 'beforeCreate') //调用生命周期钩子函数
    initInjections(vm) // 初始化Injections
    initState(vm)    //初始化data,props,methods,watch、computed
    initProvide(vm) // 初始化provide
    callHook(vm, 'created')  //调用生命周期钩子函数

初始化的事情做完之后,就调用$mount方法挂载vm,即就是要把模板渲染成最终的DOM结构:【路径:src/core/instance/init.js】

if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
vue实例挂载:$mount

(1)分析compiler版本$mount的实现:【路径:src/platforms/web/entry-runtime-with-compiler.js】

// 缓存原型上的$mount方法,再重新定义该方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  // 限制Vue不能挂在到body或者html节点上
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // 解析模板并将其渲染成render
  // 没有render方法,就将el/template转换成render方法
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      // vue所有组件的渲染丢需要render方法,无论是.vue方式开发组件还是写了el/template属性,都会被转换成render
      // compileToFunctions这个方法是一个在线编译的过程,来生成render函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  //调用vue本身的mount函数,返回一个Component
  return mount.call(this, el, hydrating)
}

(2)分析runtime only版本$mount的实现:【路径:src/platforms/web/runtime/index.js】

由mountComponent去挂载组件

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

再看看mountComponent:核心是实例化一个渲染watcher。

watcher在这里的作用:

(1)初始化时会执行回调函数

(2)当vm实例中检测的数据发生变化的时候执行回调函数

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 判断实例上是否存在渲染函数,如果不存在;
  // 则设置一个默认的渲染函数createEmptyVNode,该渲染函数会创建一个注释类型的VNode节点
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  //调用callHook函数来触发beforeMount生命周期钩子函数,可以为理解此时只是render 函数并没有形成虚拟dom,也没有将页面内容真正渲染上
  callHook(vm, 'beforeMount')

// 定义updateComponent,这个函数中参数'vm._render()'将会为我们得到一份最新的VNode节点树,'如果调用了updateComponent函数,就会将最新的模板内容渲染到视图页面中'
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      //创建VNode
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      //通过_update方法渲染DOM
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  
  // updateComponent函数作为第二个参数传给Watcher类从而创建了watcher实例,那么
  // updateComponent函数中读取的所有数据都将被watcher所监控
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // 
  if (vm.$vnode == null) { //null表示当前根Vue的实例
    vm._isMounted = true //为true表示实例已经挂在,同时执行mounted的
    callHook(vm, 'mounted')
  }
  return vm
}

接下来我们分析一下render

render和 Virtual DOM

组件初始化的时候就会调用Vue.prototype.__init,vm.__init中调用了initRender

在实例初始化时,给实例绑定__c方法,所以vm可以直接调用到__c,__c内部调用了createElement.

export function initRender (vm: Component) {
  // ...
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

createElementt方法呢?其实是对_createElement方法的封装,这里我们直接来看__createElement,查看源码会发现主要是调用new VNode,生成vnode并返回。这里只展示了核心部分:

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  ......
  if (typeof tag === 'string') {
  let Ctor
  ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
  if (config.isReservedTag(tag)) {
    // 创建普通的VNode节点
    vnode = new VNode(
      config.parsePlatformTagName(tag), data, children,
      undefined, undefined, context
    )
  } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
    // 创建组件VNode
    vnode = createComponent(Ctor, data, context, children, tag)
  } else {
    // unknown or unlisted namespaced elements
    // check at runtime because it may get assigned a namespace when its
    // parent normalizes children
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
} else {
  // 创建组件VNode
  vnode = createComponent(tag, data, context, children)
}
 ......
}

VNode其实就是对真实DOM的一种抽象描述。本质上其实就是一个普通的Javascript对象,对象的核心定义就是:标签名、数据、子节点、键值等,其它属性都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。

到这里我们就知道了在mountComponent中,vm._render是在哪里去创建VNode的,接下来就看怎样把VNode渲染成一个真实的DOM。这个过程我们需要在vm._update中看看:

update

_update被调用的时机有2个:首次渲染、数据更新;作用:就是把VNode渲染成真实的DOM

由于在数据更新的时候要进行新旧VNode的对比,然后进行差异化更新视图,_update的核心就是调用vm.__patch__方法(diff算法过程),这个方法在不同的平台定义不同:

._update函数
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
   ......
  if (!prevVnode) {
    // 首次初始化
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // 页面数据更新
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  ......
  }
.__patch__函数
Vue.prototype.__patch__ = inBrowser ? patch : noop
//由于服务端渲染中不存在真实的浏览器环境,因此不需要渲染成真实的DOM,因此是一个空函数
patch

vm.__patch__方法其实调用的是createPatchFunction的返回值

const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })
//nodeOps封装了DOM操作的方法,modules定义了模块的钩子函数的实现

patch是与平台相关的,在web和weex平台将虚拟DOM映射到“平台DOM”的方法是不同的,并且对“DOM”包括的属性模块创建和更新也不尽相同。因此每个平台的nodeOps, modules各不相同。代码托管在src/platforms目录下。而不同平台patch主逻辑部分相同,所以公共部分托管在core下。

createPatchFunction

modules:定义的模块钩子函数的实现,向外暴露一些特有的方法
nodeOps:封装的一些列DOM操作方法

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
  const { modules, nodeOps } = backend
  // hooks =['create', 'activate', 'update', 'remove', 'destroy']
  //遍历钩子hooks找到modules的各模块对应的方法
  for (i = 0; i < hooks.length; ++i) {
    // 比如cbs.create=[]
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        // 遍历各个 modules,找出各个 module 中的 create 方法,然后添加到 cbs.create 数组中
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  
  ......
  
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    //1.新节点不存在,旧节点存在,移除旧节点
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []
    // 2.新节点存在,旧节点不存在,添加新节点
    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      // 新节点和旧节点都存在,判断旧节点是否为真实的元素
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        //旧节点不是真实元素且与新旧节点是同一节点,然后就去对比修改
        //patchVnode就是diff算法的过程
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 旧节点是真实的元素
        if (isRealElement) {
          // 挂载到真实的DOM和处理服务器端渲染
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          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 ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // 不是服务器端渲染或者渲染失败,就把oldVnode转换为VNode对象
          oldVnode = emptyNodeAt(oldVnode)
        }

        // 旧节点是真实的元素
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // 通过虚拟节点创建真实的 DOM 并插入到它的父节点中parentElm
        createElm(
          vnode,
          insertedVnodeQueue,
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // vnode.parent存在又不是同一节点,递归更新父占位符节点元素(异步组件)
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // start at index 1 to avoid re-invoking component mounted hook
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // 移除旧节点
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)//移除节点方法
        }
      }
    }
    // 
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

createPatchFunction返回的patch方法,主要做了以下操作:

  • 新节点不存在,旧节点存在,移除旧节点
  • 新节点存在,旧节点不存在,添加新节点
  • 新节点和旧节点都存在,旧节点不是真实元素且与新旧节点是同一节点,然后就去对比修改
  • 新节点和旧节点都存在,旧节点是真实的元素。旧挂载到真实的DOM和处理服务器端渲染。旧节点是真实DOM也就是传进来的vm.$el对应的元素。例如:
    ;这里还有一种情况就是vnode.parent存在,又不是同一节点,表示更新。比如异步组件
createElm

createElm的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中。createElm的关键逻辑:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
//createComponent是用来创建子组件的,初始化时返回true/false。存在则走下面
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }
//来判断 vnode 是否包含 tag,如果包含,先简单对 tag 的合法性在非生产环境下做校验,看是否是一个合法标签;
//然后再去调用平台 DOM 的操作去创建一个占位符元素。
//创建元素节点
vnode.elm = vnode.ns
  ? nodeOps.createElementNS(vnode.ns, tag)
  : nodeOps.createElement(tag, vnode)
  ......
  //接下来递归调用createChildren创建子元素
  createChildren(vnode, children, insertedVnodeQueue)
    if (isDef(data)) {
  //调用invokeCreateHooks,执行所有的 create 的钩子并把 vnode push 到 insertedVnodeQueue 中
  //调用各个模块的 create 方法,比如创建属性的、创建样式的、指令的等等
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
  //调用insert方法把DOM插入到父节点中
  insert(parentElm, vnode.elm, refElm)
  ......
  }

这里的整个patch的方法:首次渲染先调用createElm,传入的parentElm是oldVnode.elm的父元素.实际上整个过程就是递归创建了一个完整的 DOM 树并插入到 Body 上。

最后根据递归createElm生成的vnode插入顺序队列,执行相关的insert钩子函数

new Vue–>init–>$mount–>compiler–>render–>vnode–>patch–>DOM

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oPiITRVP-1635472110014)(/Users/zlj/Desktop/图库/数据驱动.png)]

下面再说一下patchVnode和updateChildren

patchVnode是patch函数的核心方法,也是在这个方法中调用了diff:updateChildren。下面是对patchVnode解读注释

patchVnode
  function patchVnode (
    oldVnode,
    vnode,
    insertedVnodeQueue,
    ownerArray,
    index,
    removeOnly
  ) {
    // 新旧节点相同,直接返回
    if (oldVnode === vnode) {
      return
    }

    if (isDef(vnode.elm) && isDef(ownerArray)) {
      // clone reused vnode
      vnode = ownerArray[index] = cloneVNode(vnode)
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      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.
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }
    // 执行组件的prepatch钩子
    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    // 全量更新新节点的属性
    if (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)
    }
    // patch过程
    if (isUndef(vnode.text)) {//新节点不是文本节点
      if (isDef(oldCh) && isDef(ch)) {
        //1.新旧节点都有子节点,且旧节点不等于新节点,执行updateChildren方法进行diff算法
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {//2.新节点有子节点,旧节点没有,若有文本,清空旧节点的文本内容,插入子节点
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(ch)
        }
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {//3.新节点没有子节点,旧节点有子节点,移除旧节点的子节点
        removeVnodes(oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {//旧节点有文本,新节点没有,清空旧节点文本内容
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {//4.新节点文本内容与旧节点文本内容不一致,更新旧节点文本内容
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      // 执行组件的postpatch钩子函数
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

updateChildren是Vue diff算法中的核心方法:对比新的children和老的children节点数组的差别,然后进行更新。对比的核心就是尽可能的复用可以复用的节点。

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是一个特殊的标志,仅由 <transition-group> 使用,以确保被移除的元素在离开转换期间保持在正确的相对位置
    const canMove = !removeOnly

    if (process.env.NODE_ENV !== 'production') {
      // 检查新节点是否重复
      checkDuplicateKeys(newCh)
    }
    
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (isUndef(oldStartVnode)) {//第一个旧节点不存在,那么当前索引节点不存在,旧开始加1
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {//最后一个节点不存在,调整索引,旧结束减1
        oldEndVnode = oldCh[--oldEndIdx]
      } else if (sameVnode(oldStartVnode, newStartVnode)) {//开始的新旧节点相同,调用patchVnode算法
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        //patch后旧开始和新开始的索引分别加 1
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } else if (sameVnode(oldEndVnode, newEndVnode)) {///结束的新旧节点相同,调用patchVnode算法
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        //patch后旧结束和新结束的索引分别减1
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldStartVnode, newEndVnode)) { // 旧开始和新结束相同,调用patch算法
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
        //处理被 transtion-group 包裹的组件时使用
        canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
        // 旧开始索引加1,新结束索引减1
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } else if (sameVnode(oldEndVnode, newStartVnode)) { //旧结束和新开始相同,调用patch算法
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
        canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        //旧开始索引减1,新结束索引加1
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        //如果上面四种情况都不存在,则通过遍历找到新开始节点在旧节点中的位置
        //建立旧节点每个节点的key和索引之间的映射关系 oldKeyToIdx={key1:idx1,...}
        if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        // 在映射关系中找新开始的key在旧节点中的位置
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) { // 如果在映射关系中没有找到新节点,则创建新元素
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        } else {
          // 如果找到了,
          vnodeToMove = oldCh[idxInOld]
          // 两个节点是同一个节点,执行patchVnode
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldCh[idxInOld] = undefined//执行后将旧节点的该位置设为undefined
            canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
          } else {
            // 两个节点不是是同一个节点,执行createElm新创建节点
            createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else if (newStartIdx > newEndIdx) {
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }

看到了这里是不是就很心累啊!其实我也心累真的是看源码看的心塞哦!但是还是得看啊,必须得提升啊!所以看到这篇文章的同学,不点赞都没有关系,只要你看了有帮助就可以了!

有什么看源码更好的意见和建议,方便的话,各位大神们评论区给我指点指点,菜鸟求指教哦!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值