在说数据驱动之前先很大家讲一下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)
}
}
看到了这里是不是就很心累啊!其实我也心累真的是看源码看的心塞哦!但是还是得看啊,必须得提升啊!所以看到这篇文章的同学,不点赞都没有关系,只要你看了有帮助就可以了!
有什么看源码更好的意见和建议,方便的话,各位大神们评论区给我指点指点,菜鸟求指教哦!