学习目标
- 理解Vue批量异步更新策略
- 掌握虚拟DOM和Diff算法
一、异步更新队列
Vue高效的秘诀是一套批量、异步的更新策略
异步任务的类型
JS 单线程基于事件循环:分为异步和同步
。同步执行完,在执行异步中的内容。
-
宏任务macro task事件:setTimeout、setInterval、setImmediate、I/O、UI rendering、script(整体代码)
-
微任务micro task事件:Promises(浏览器实现的原生Promise)、MutationObserver、process.nextTick
事件循环
- 进入脚本执行宏任务,自上而下运行
- 遇到同步代码按顺序执行,遇到宏任务放入宏任务队列,遇到微任务放入微任务队列
- 执行完当前宏任务,执行微任务中执行完并正在等待执行的任务
- 执行下一个宏任务,这样周而复始的执行顺序被称为事件循环
JS 的执行顺序和声明以及引用的顺序有关,先声明的顺序先执行,后声明的顺序后执行
简而言之:一次事件循环只执行处于 Macrotask 队首的任务,执行完成后,立即执行 Microtask 队列中的所有任务
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(function () {
console.log('promise1');
})
.then(function () {
console.log('promise2');
});
console.log('script end');
async function async1() {
console.log( 'async1 start' )
await async2()
console.log( 'async1 end' )
}
async function async2() {
console.log( 'async2' )
}
console.log( 'script start' )
setTimeout( function () {
console.log( 'setTimeout' )
}, 0 )
async1();
new Promise( function ( resolve ) {
console.log( 'promise1' )
resolve();
} ).then( function () {
console.log( 'promise2' )
} )
console.log( 'script end' )
二、Vue中的具体实现
- 异步:只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一个事件循环中发生的所有数据变更。
- 批量:如果同一个watcher被多次触发,只会被推入到队列中一次。去重对于避免不必要的计算和DOM操作非常重要的。然后,在下一个的事件循环"tick"中,Vue刷新队列执行实际工作。
- 异步策略:Vue在内部队列尝试使用原生的
Promise.then
、MutationObserver
和setImmediate
,如果执行环境不支持,则会采用setTimeout(fn,0)代替。
整体流程
update() src\core\observer\watcher.js
dep.js中的notify()之后watcher执行update(),执行入队操作
src\core\observer\index.js
export function defineReactive (......) {
......
Object.defineProperty(obj, key, {
......
get: function reactiveGetter () {
......
return value
},
set: function reactiveSetter (newVal) {
......
//+ 通知更新
dep.notify()
}
})
}
src\core\observer\dep.js
export default class Dep {
......
notify () {
......
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
export default class Watcher {
......
update () {
......
queueWatcher(this)
......
}
}
queueWatcher() src\core\observer\scheduler.js
执行watcher入队操作
export function queueWatcher (watcher: Watcher) {
......
+ 去重,不存在才入队
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
+ 异步刷新队列
nextTick(flushSchedulerQueue)
}
}
}
nextTick(flushSchedulerQueue) src\core\util\next-tick.js
nextTick按照特定异步策略执行队列操作
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
+ 异步函数
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
timerFunc() src\core\util\next-tick.js
启动微服务
// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
+ 是否支持promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
+ 启动一个微任务
p.then(flushCallbacks)
// In problematic UIWebViews, Promise.then doesn't completely break, but
// it can get stuck in a weird state where callbacks are pushed into the
// microtask queue but the queue isn't being flushed, until the browser
// needs to do some other work, e.g. handle a timer. Therefore we can
// "force" the microtask queue to be flushed by adding an empty timer.
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && ( + 特殊浏览器
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// Fallback to setImmediate.
// Technically it leverages the (macro) task queue,
// but it is still a better choice than setTimeout.
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else { + 最后的选择 不得不使用宏任务 setTimeout
// Fallback to setTimeout.
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
flushCallbacks() src\core\util\next-tick.js
循环所有要执行的回调
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]() +
}
}
案例
03-timerFunc.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>异步更新</title>
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="demo">
<h1>异步更新</h1>
<p id="p1">{{foo}}</p>
</div>
<script>
const app = new Vue({
el: "#demo",
data: {
foo: "ready"
},
mounted() {
setInterval(() => {
this.foo = Math.random();//入队一次
this.foo = Math.random();//入队不能入队,队列覆盖
this.foo = Math.random();//不能入队,已经存在了
//+ 异步行为,此时内容没变
console.log("1111",p1.innerHTML);
this.$nextTick(() => {
//+ 这里才是最新的值
console.log("2222",p1.innerHTML);
});
}, 3000)
}
});
</script>
</body>
</html>
watcher中update执行三次,但run仅执行一次
相关API:vm.$nextTick(cb)
三、虚拟DOM
概念
虚拟DOM(Virtual DOM)是对DOM的JS抽象表示,他们是JS对象,能够描述DOM结构和关系。应用的各种状态变化会作用于虚拟DOM,最终映射到DOM上。
体验虚拟DOM
kvue
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<!-- 虚拟 DOM 库 -->
<script src="node_modules/snabbdom/dist/snabbdom.js"></script>
<script>
const obj = {};
const {
init,
h
} = snabbdom;
//1.获取patch函数
const patch = init([]);
let vnode; //保存之前旧的虚拟dom
function defineReactiove(obj, key, val) {
//对传入的obj进行访问的拦截
Object.defineProperty(obj, key, {
get() {
console.log('get ' + key);
return val
},
set(newValue) {
if (newValue !== val) {
console.log('set ' + key + ":" + newValue);
val = newValue;
//更新函数,更新界面
update();
}
}
});
}
//使用虚拟dom做更新
function update() {
// app.innerText = obj.foo --- dom操作
//改为虚拟dom
vnode = patch(vnode, h("div#app", obj.foo));
//patch参数1:老的vnode,参数2:新的vnode
}
defineReactiove(obj, 'foo', new Date().toLocaleTimeString());
// obj.foo = new Date().toLocaleTimeString();
//执行初始化
vnode = patch(app, h("div#app", obj.foo)); //创建虚拟dom
//参数1:
//如果是真实dom,就不会去做比较,而是直接转换为真实dom,替换宿主
setInterval(() => {
// obj.foo = new Date().toLocaleTimeString();
}, 1000);
</script>
</body>
</html>
优点
- 虚拟DOM轻量、快速:当它们发生变化时通过新旧虚拟DOM比对可以得到最小DOM操作量,从而提升性能
vnode = patch(vnode, h("div#app", obj.foo));
- 跨平台:将虚拟DOM更新转换为不同操作平台时特殊操作实现跨平台
const patch = init([snabbdom_style.default]);
vnode = patch(vnode, h("div#app", {style:{color:'res'}}, obj.foo));
- 兼容性:还可以加入兼容性代码增强操作的兼容性
必要性
vue1.0中有细粒度的数据变化侦测,它是不需要虚拟DOM的,但是细粒度造成了大量开销,这对于大型项目来说是不可接受的。因此,vue2.0选择了中等细粒度的解决方案,每一个组件一个watcher实例,这样状态变化时只能通知到组件,在通过引入虚拟DOM去进行比对和渲染。
整体流程
数据发生变化:
watcher.run() => componentUpdate() => render() => update() => patch()
mountComponent() src\core\instance\lifecycle.js
渲染、更新组件
+ 定义更新函数
updateComponent = () => {
+ 实际调用是在lifecycleMixin中定义的_update和renderMixin中的_render
vm._update(vm._render(), hydrating)
}
export function mountComponent (......): Component {
......
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)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
+ 用户 $mount() 时,定义updateComponent
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
+ 创建watcher,传入updateComponent
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
......
}
_render src\core\instance\render.js
生成虚拟dom
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}
_update src\core\instance\lifecycle.js
update负责更新dom,转换vnode为dom
Vue.prototype._update = function (......) {
......
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false )
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
......
}
patch src\platforms\web\runtime\index.js
__patch__是在平台特有代码中指定的
//1+ 指定补丁方法:传入的虚拟dom转换为真实dom
//+ 1.初始化,2.更新
Vue.prototype.__patch__ = inBrowser ? patch : noop
patch src\core\vdom\patch.js
patch实现
四、diff算法的整体流程
4.1 diff算法简介
diff算法是一种通过同层的树节点进行比较的高效算法,避免了对树进行逐层遍历,所以时间复杂度只有O(n)。diff算法的很多场景下都有应用,例如在vue虚拟dom渲染成真实dom的新旧VNode节点更新时,就用到了该算法。diff算法有两个比较显著的特点:
-
比较只会在同层级进行,不会跨层级比较。
-
在diff比较过程中,循环从两边向中间收拢。
4.2 diff流程
通过对vue源码的解读和实例分析来理清楚diff算法的整个流程。下面将整个diff流程分成三步来具体分析:
第一步
vue的虚拟 dom 渲染真实 dom 的过程中首先会对新老VNode的开始和结束位置进行标记:
oldStartIdx、oldEndIdx、newStartIdx、newEndIdx。
//+ 4个指针
let oldStartIdx = 0 //旧节点开始下标
let newStartIdx = 0 //新节点开始下标
let oldEndIdx = oldCh.length - 1 //旧节点结束下标
let newEndIdx = newCh.length - 1 //新节点结束下标
//+ 4个节点
let oldStartVnode = oldCh[0] //旧节点开始vnode
let oldEndVnode = oldCh[oldEndIdx] //旧节点结束vnode
let newStartVnode = newCh[0] //新节点开始vnode
let newEndVnode = newCh[newEndIdx] //新节点结束vnode
经过第一步之后,我们初始的新旧 VNode 节点如下图所示:
第二步
标记好节点位置之后,就开始进入到while循环处理中,这里是diff算法的核心流程,分情况进行了新老节点的比较并移动对应的 VNode 节点。while 循环的退出条件是直到老节点或者新节点的开始位置大于结束位置。
//+ 循环条件 :开始索引不能大于结束索引
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
......//+ 处理逻辑
}
接下来具体介绍 while 循环汇总的处理逻辑
//+ 头尾指针调整 isUndef 不存在
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
//+ 接下来是头尾比较的4种情况
}
循环过程中对新老 VNode 节点的头尾进行比较,寻找相同节点,如果有相同节点满足 sameVnode (可以复用的相同节点)则直接进行 patchVnode (该方法进行节点复用处理),并且根据具体情形,移动新老节点的 VNode 索引,以便进入下一次循环处理,一共有 2 * 2 = 4 中情形。下面根据代码展开分析:
情形一:当新老 VNode 节点的 start 满足 sameVnode 时,直接 patchVnode 即可,同时新老 VNode 节点的 开始索引 都加1。
//+ 接下来是头尾比较的4种情况
else if (sameVnode(oldStartVnode, newStartVnode)) {
//+ 两个开头相同
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
//+ 新老开始索引+1
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}
情形二:当新老 VNode 节点的 end 满足 sameVnode 时,直接 patchVnode 即可,同时新老 VNode 节点的 结束索引 都减1。
else if (sameVnode(oldEndVnode, newEndVnode)) {
//+ 两个结尾相同
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
//+ 新老结束索引-1
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}
情形三:当老 VNode 节点的 start 和新 VNode 节点的 end 满足 sameVnode 时,这说明这次数据更新后的 oldStartVnode 已经跑到 oldEndVnode 后面去了。这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到oldEndVnode 的后面,同时 老 VNode节点开始索引 加1 , 新 VNode 节点的结束索引 减1。
else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
//+ 老的开始和新的结束相同,除了打补丁之外,还要移动到队尾
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
//+ 老节点索引+1,新节点索引-1
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}
情形四:当老的 VNode 节点的 end 和新 VNode 节点的 start 满足 sameVnode时,这说明这次数据更新后的oldEndVnode 跑到了 oldStartVnode 的前面去了。这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到oldStartVnode 的前面,同时 老 VNode节点结束索引 减1 , 新 VNode 节点的开始索引 加1。
else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
//+ 老的结束和新的开始相同,除了patchVnode之外,还要移动到队首
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
//+ 老的结束索引-1,新的开始索引+1
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
如果都不满足以上四种情形,那说明没有相同的节点可以复用,于是则通过查找事先建立好的以旧的 VNode 为key 值,对应 index 序列为 value 值的哈希表。从这个哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点,如果两者满足 sameVnode 的条件,在进行 patchVnode的同时会将这个真实 dom 移动到 oldStartVnode 对应的真实 dom 的前面;如果没有找到,这说明当前索引下的新的 VNode 节点在旧的 VNode 队列不存在,无法进行节点的复用,name就只能调用 createElm 创建一个新的 dom 节点放到 当前 newStartIdx 的位置。
else {
//+ 4种猜想之后没有找到相同的,不得不开始循环查找
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
//+ 查找在老的孩子数组中的索引
//+ idxInOld :如果newStartVnode新的VNode节点存在key并且这个key在oldVnode中能找到则返回这个及诶单的idxInOld(即第几个节点,下标)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
:findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
//+ 没找到则创建新元素
//+ newStartVnode没有key或者是该key没有在老节点中找到则创建一个新节点
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
//+ 找到除了打补丁,还要移动到对首。
//+ 获取同key的老节点
vnodeToMove = oldCh[idxInOld]
//+ 如果新VNode与得到的有相同key的节点是同一个VNode则进行patchVnode
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
//+ 因为已经patchVnode进去了,所以将这个老节点赋值undefined
oldCh[idxInOld] = undefined
//+ 当有标识位canMove实可以直接插入oldStartVnode对应的真实Dom节点前面
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
//+ 当新的VNode与找到的同样key的VNode不是sameVNode的时候(比如说tag不一样或者是有不一样type的input标签),创建一个新的节点
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
再来看我们的实例,第一次循环后,找到旧节点的末尾和新节点的开头(都是D),于是直接复用 D 节点作为 diff 后创建的第一个真实节点。同时旧节点的 endIndex 移动到 C ,新节点的 startIndex 移动到了 C。
紧接着第二次循环,第二次循环后,同样是旧节点的末尾和新节点的开头 (都是C),同理,diff后创建 C 的真实节点插入到第一次创建的 B 节点后面。同时旧节点的 endIndex 移动到了 B,新节点的 startIndex 移动到了 E。
接下来第三次循环,发现 patchVnode 的4中情形都不符合,于是在旧节点队列查找当前新节点E,结果发现没找到,这时候只能创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndex 和 endIndex 都保持不变。
第四次循环中,发现新旧节点的开头(都是A),于是 diff 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex 移动到了 B ,新节点的 startIndex 移动到了 B。
第五次循环中,情形同第四次循环一样,因此 diif 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex 移动到了 C ,新节点的 startIndex 移动到了 F。
这时候发现新节点的 startIndex 已经大于 endIndex了。不在满足循环条件。因此结束循环,接下来走后面逻辑。
第三步
当while循环结束后,根据新老节点的数目不同,做响应的节点删除或者添加。若新节点数目大于老节点则需要把多出来的节点创建出来加入到真实 dom 中,反之若老节点数据大于新节点则需要吧多出来的老节点从真实 dom 中删除。至此整个diff过程就已经全部完成了。
//+ 整理工作:必定有数组还有剩下的元素未处理
if (oldStartIdx > oldEndIdx) {
//+ 老的结束了,这种情况说明新的数组还有剩下的节点
//+ 全部比较完成以后,发现oldStartIdx > oldEndIdx的话,说明老节点已经遍历完了,新节点比老节点多, 所以这时候多出来的新节点需要一个一个创建出来加入到真实Dom中
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)
}
五、作业
- patch函数式怎么获取的?
- 节点属性是如何更新的
- 组件化机制是如何实现的
- 口述diff
补充:
async和await
async和await是es6新增的关键字,用于把异步变成同步
async在函数定义时使用,用async定义的函数默认返回一个promise实例,可以直接.then。
如果async定义的函数执行返回的不是一个promise对象,name就会给返回值包装成一个promise对象(将返回值放进promise实例的resolve方法中当做参数)
await:要和async一起使用
await 会等待,等它右侧的代码执行完
用法:
1.如果await右侧是同步代码,就会让右侧代码执行;如果执行的是一个函数,还会把函数的返回值给到await左边的变量
let p;
async function f3() {
p = await 18;
console.log(p);
}
f3();
console.log(1);
console.log(p);
执行结果是1, undefined , 18
------
f3()
js执行的时候是从右往左执行的,先执行18,然后将await左边连同下面的代码都放进微任务
console.log(1); 打印1
console.log(p); 打印undefined
清空微任务
给p赋值
console.log(p); 打印18
2.如果await右侧是一个promise实例,或者返回了promise实例,await会等着promise实例reslove,并且在实例reslove之前,await后面的代码不执行;并且还会拿到promise在reslove时传入的值,并且赋值给等号左边变量
变量声明
let p;
函数声明
async function f(){
console.log("f",p) 第三个打印
return 10;
}
函数声明
async function f3() {
console.log("f3--1",p) 第二个打印
p = await f(); 执行右边函数,await之后的代码放在微任务
console.log("f3---2",p);
}
console.log("66666") 第一个打印
f3();
console.log("77777",p) 第四个打印
---------------
同步执行完,清空微任务
p = 10 赋值
console.log("f3---2",p); 第五个打印
--------------------------
结果
66666
f3--1 undefined
f undefined
77777 undefined
f3---2 10
3.await会把await下面的代码编程为任务
4.应用
如果我们有a,b,c 三个异步操作,要求 b 依赖 a 的返回值,c 依赖 b 的返回值
使用promise链式调用
function a(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
reslove(1)
},1000);
});
}
function b(result){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log("b",result)
reslove(2)
},2000);
});
}
function c(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
console.log("c",result)
reslove(3)
},3000);
});
}
let ap = a();
ap.then(b).then(c);
使用async和await
function a() {
return new Promise(((resolve, reject) => {
setTimeout(()=>{
resolve(1);
},1000)
}))
}
function b(result) {
return new Promise(((resolve, reject) => {
setTimeout(()=>{
console.log(result);
resolve(2);
},2000)
}))
}
function c(result) {
return new Promise(((resolve, reject) => {
setTimeout(()=>{
console.log(result);
resolve(3);
},3000)
}))
}
async function f(){
let aP = await a();
let bP = await b(aP);
let cP = await c(bP);
}
f();