我报名参加金石方案1期挑战——瓜分10万奖池,这是我的第2篇文章,点击检查活动详情

hey! 我是小黄瓜。不定期更新,期待重视➕ 点赞,一同生长~

写在前面

本文的目标是完成一个基本的 vue3 的虚拟DOM的节点烘托与更新,包含最根底的状况的处理,本文是系列文章,本系列已全面运用vue3组合式语法,假如你对 vue3根底语法及呼应式相关逻辑还不了解,那么请移步:

超详细整理vue3根底知识

狂肝半个月!1.3万字深度剖析vue3呼应式(附脑图)

手写mini-vue3第三弹!万字完成烘托器初次烘托流程

大结局!完成vue3模版编译功用

本文仅仅整个vue3烘托器的下篇内容,包含组件及节点的更新,更新优化,diff算法的内容。

食用提示!必看

看本篇之前必须要看上一篇初次烘托!!!

因为整个烘托过程中的函数完成以及流程过长,有许多函数的完成内容在相关的章节并不会悉数展示,而且存在大量的伪代码,相关章节只会重视当时功用代码的显现和完成。

可是!为了便于了解,我在github上上传了每章节的详细完成。(请把交心打在评论区 )把每一章节的完成都存放在了独自的文件夹:

更新!更新!完成vue3虚拟DOM更新&diff算法优化

只运用了单纯的htmljs来完成功用,只需求在index.html中替换相关章节的文件路径(替换以下三个文件),在浏览器中翻开,就能够自行调试和检查代码。见下图:

更新!更新!完成vue3虚拟DOM更新&diff算法优化

地址在这儿,欢迎star!

vue3-analysis(component)

mini-vue3的正式项目地址在这儿!现在只完成了呼应式和烘托器部分!

k-vue

欢迎star!

本文你将学到

  • 更新element
  • 更新component
  • diff算法
  • 最长递加子序列应用
  • 完成nextTick

一. 更新element流程建立

更新!更新!完成vue3虚拟DOM更新&diff算法优化

在完成更新之前,有必要先来想一下,到底怎样算是更新?怎样算是触发更新的操作?在上一篇文章中,咱们只完成了初度烘托,也便是翻开页面默许烘托出来的内容,不依靠动作去触发的部分。在这一部分,咱们首要履行了setup函数,把它当作数据源来初始化render函数进行烘托,依据vnode的特点值来顺次生成DOM节点,DOM特点,递归烘托子节点。最终将整个vnode烘托到页面上。也便是说现在完成的烘托仅仅纯静态的。

所谓更新便是在用户进行手动触发时,当数据产生改动时,DOM节点产生时对页面上的DOM元素进行修正或许增加或许删去的操作。运用原生的DOM的API来操作DOM时十分的简单,无论你怎样产生改动,我直接拼接DOM字符串,运用innerHTML怼上去就完事了。可是在vue这种老练的框架处理这种问题时,需求考虑的问题比单纯的完成要多的多。比如最直观的问题,假如咱们的DOM层级比较深,整个页面结构十分杂乱,直接运用拼接烘托的方法并不高明,这意味着无论我做了多么小的改动,都会将页面从头烘托一遍,这样带来的功用损耗将是十分惊人的。那么怎样进行优化就将会是整个更新DOM过程中十分重要的工作。本文的后半部分的一大核心问题也便是处理更新的优化部分。

说了这么多,还有一个最核心的问题没有处理,怎样能知道数据更新了呢?关于更新时优化这都是后话了,首要咱们得知道数据啥时分更新了才行,否则啥都白扯。更新的核心其实便是当数据产生改动时,从头履行render函数进行比照差异,然后烘托到页面上。等等,这句话怎样这么耳熟?这不便是呼应式的完成逻辑吗!(对呼应式不太熟悉的老铁们请先学习一下呼应式部分)特别是咱们的数据都是以ref或许reactive函数进行包裹的,那么只需求将从头履行render函数部分逻辑放在effect函数内就能够了。这样就能够达到数据与更新DOM的逻辑想绑定,数据更新,视图更新的功用了。

在此之前,仍是先来完成一个比如:

const App = {
 setup() {
  let value = reactive({
   count: 0
   })
    // 点击按钮count+1
  const changeCount = ()=>{
   value.count = value.count + 1
   }

  return {
   value,
   changeCount
   }
  },
 render() {
  return h('div', { id: 'root' }, [
   h('p', {}, 'count:' + this.value.count),
   h('button', { onClick: this.changeCount, }, 'change')
   ])
  }
}

上面的比如中咱们完成了一个按钮,点击按钮呼应式数据value.count将会加一,相应的,页面中也会触发DOM更新的相关逻辑。

咱们在初始化component实例的时分初始化特点isMounted,用于标识是否处于更新状况

const createComponentInstance = function (vnode, parent) { // 修正
 const component = {
  vnode,
  type: vnode.type,
  props: {},
  setupState: {},
  provides: parent ? parent.provides : {},
  parent: parent ? parent : {},
  // 是否初次烘托?
  isMounted: false, // 新增
  subTree: {},
  slots: {},
  emit: () => {}
  }

 component.emit = emit.bind(null, component)

 return component
}

接下来找到履行render函数的当地,绑定呼应式数据的履行函数effect:

const setupRenderEffect = function (instance, vnode, container) {

 effect(()=>{
  // 依据isMounted来判别现在处于更新/初始化?
  if(!instance.isMounted) {
   console.log("init");
   const { proxy } = instance;
   const subTree = (instance.subTree = instance.render.call(proxy));

   patch(null, subTree, container, instance);

   vnode.el = subTree.el;

   instance.isMounted = true;
   } else {
   console.log("update");
      
   const { proxy } = instance
   // render函数履行成果
   const subTree = instance.render.call(proxy)
   const prevSubTree = instance.subTree
      // 更新subTree
   instance.subTree = subTree;
   // 传入patch
   patch(prevSubTree, subTree, container, instance)

   }
  })
}

在履行setupRenderEffect函数时将会履行那几件工作呢,首要运用component中的subTree来保存上次的履行成果,用于调用patch函数时,当作旧的vnode参加比照操作,最终需求将新的vnode来更新特点subTree

因为咱们的patch函数参数又产生了变更,加入了上一次更新的vnode,所以需求再次对一切调用patch函数的当地进行参数修正:

render:

const render = function (vnode, container) {
 // 初次烘托,第一个参数传递null
 patch(null, vnode, container, null) // 修正
}

patch:

// n2 代表当时处理的vnode
const patch = function (n1, n2, container, parentComponent) { // 修正

 const { type, shapeFlag } = n2 // 修正

 switch(type) {
  case Fragment:
   processFragment(n1, n2, container, parentComponent); // 修正
   break
  case Text:
   processText(n1, n2, container); // 修正
   break
  default:
   if (shapeFlag & ShapeFlags.ELEMENT) {
    processElement(n1, n2, container, parentComponent) // 修正
    } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    processComponent(n1, n2, container, parentComponent) // 修正
    }
   break
  }
}

processFragment:

const processFragment = function(n1, n2, container, parentComponent) { // 修正
 mountChildren(n2.children, container, parentComponent) // 修正
}

processText:

const processText = function(n1, n2, container) { // 修正
 const { children } = n2 // 修正
 const textVNode = (n2.el = document.createTextNode(children)) // 修正
 container.append(textVNode) 
}

mountChildren:

const mountChildren = function (children, container, parentComponent) {
 children.forEach(v => {
  // 初次烘托无需传递第一个参数
  patch(null, v, container, parentComponent) // 修正
  })
}

因为在处理element的时分初始化的烘托和更新是不同的,所以在这个函数中需求进行别离处理,假如没有传递n1,则阐明是初次烘托,反之则是更新。

const processElement = function (n1, n2, container, parentComponent) { // 修正
  if(!n1) { // 修正
    mountElement(n2, container, parentComponent)  // 修正
  } else { // 修正
    patchElement(n1, n2, container) // 修正
  }
}

patchElement中打印一下更新时的vnode:

const patchElement = function(n1, n2, container) {
  console.log(n1);
  console.log(n2);
}

更新!更新!完成vue3虚拟DOM更新&diff算法优化

能够看到依据上面的比如,点击按钮后,输出了两个vnode,里边的children是不同的,这正是需求更新的内容。

二. 更新element 的props

更新!更新!完成vue3虚拟DOM更新&diff算法优化
更新props,也便是更新DOM特点,其实首要便是来过那种状况,旧的vnode与新的vnode不共同,直接设置新的props,假如新的props中没有旧的props,直接进行删去操作。

接下来仍是国际惯例,首要完成一个案例,用需求来驱动功用完成:

const App = {
 setup() {
  let props = reactive({
   foo: 'foo',
   bar: 'bar'
   })

  const changeProps1 = () => {
   props.foo = 'new-foo'
   }

  const changeProps2 = () => {
   props.foo = undefined
   }

  return {
   props,
   changeProps1,
   changeProps2,
   }
  },
 render() {
  return h('div', { id: 'root', ...this.props }, [
   h("button", { onClick: this.changeProps1 }, "修正"),
   h("button", { onClick: this.changeProps2 }, "删去"),
   h("button", { onClick: this.changeProps3 }, "增加"),
   ])
  }
}

咱们在div中设置了特点foo和特点bar,而且界说了两个按钮,别离对特点进行修正和删去操作。

关于props的处理,咱们是在mountElement函数中进行的,因为处理特点的过程还需求在别的当地进行运用,所以需求将本来存在于mountElement函数中的逻辑抽离出来,用于处理更新props目标。

const mountElement = function (vnode, container, parentComponent) {
 // 省掉...

 // props
 for (const key in props) {
  const prop = props[key]
    // 初次烘托只需求传递更新后的特点值
  mountProps(el, key, null, prop) // 修正
  }

 container.append(el)
}

// 修正
// mountProps第三个参数为更新前的props的key值,第四个参数nextVal为更新后
const mountProps = function(el, key, prevVal, nextVal) {
 const isOn = key => /^on[A-Z]/.test(key)
 // 运用on进行绑定事情
 if(isOn(key)) {
  const event = key.slice(2).toLowerCase()
  el.addEventListener(event, nextVal)
  } else {
  // 增加判别,假如新的props为null或许undefined。那么依据key来删去DOM特点
  if(nextVal === undefined || nextVal === null) {
   el.removeAttribute(key)
   } else {
   el.setAttribute(key, nextVal)
   }
  }
}

抽离出来mountProps函数,首要负责处理props目标生成DOM特点,增加判别当时特点是否存在,假如不存在直接依据key来删去DOM特点。

接下来在patchElement处理更新:

const EMPTY_OBJ = {}
const patchElement = function(n1, n2, container) {
 // 取出新旧vnode中的props
 const oldProps = n1.props || EMPTY_OBJ
 const newProps = n2.props || EMPTY_OBJ
 // 更新el
 const el = (n2.el = n1.el)
 // 调用patchProps找出差异
 patchProps(el, oldProps, newProps)
}

关于为啥要创立一个空目标,然后作为新旧props的默许值,其实意图是让新旧的props在运用默许值目标的时分能够运用同一个引证,便于在下文中运用==来判别新旧目标的值。

const patchProps = function(el, oldProps, newProps) {
 // 两个目标不相等时,才会进行比照
 if(oldProps !== newProps) {
  // 循环新的props
  for(let key in newProps) {
   // 依据新的props中的key别离在新旧props中取值
   const prevProp = oldProps[key]
   const nextProp = newProps[key]
      // 不想等则进行更新
   if(prevProp !== nextProp) {
    mountProps(el, key, prevProp, nextProp)
    }
   }
    // 判别是否存在需求删去的特点
  if(oldProps !== EMPTY_OBJ) {
   for(let key in oldProps) {
    if(!(key in newProps)) {
     mountProps(el, key, oldProps[key], null)
     }
    }
   }
  }
}

在进行比照的时分,首要遍历新的props目标,取一切的key值在新旧的props中进行查找,假如不相等,直接调用mountProps函数进行更新。接下来处理旧的props中存在而心的props节点不存在的状况,遍历旧props判别是否存在于新props中,不存在直接调用mountProps函数进行删去。

初次烘托:

更新!更新!完成vue3虚拟DOM更新&diff算法优化

点击修正,修正foo特点后:

更新!更新!完成vue3虚拟DOM更新&diff算法优化

foo特点的值更新为new-foo

点击删去,将foo特点删去后:

更新!更新!完成vue3虚拟DOM更新&diff算法优化

页面中foo特点现已被删去。

三. 更新 children

更新!更新!完成vue3虚拟DOM更新&diff算法优化

在处理完props的更新后,接下来开端着手处理children的更新,因为咱们生成vnode时只支持字符串和数组的方法来创立子节点,字符串代表文本节点,数组代表子节点,所以在处理更新时也就只存在四种更新状况:

  • Array -> String
  • String -> String
  • String -> Array
  • Array -> Array

因为Array -> Array较为杂乱,咱们先来处理与文本节点相关的更新操作。

依旧是先来写一个:

// ex1 Array -> String
const prevChild = [h("div", {}, "A"), h("div", {}, "B")];
const nextChild = "newChildren";

const App = {
 setup() {
  // 界说呼应式数据
  const isChange = reactive({
   value: false
   })
  // 挂载到大局目标window上
  window.isChange = isChange;

  return {
   isChange
   }
  },

 render() {
  let self = this
    // isChange.value的值产生改动,更新children
  return self.isChange.value === true ? 
     h('div', {}, nextChild) :
     h('div', {}, prevChild)
  }
}

创立呼应式数据isChange.value,当该数据产生改动时,更改children,把isChange挂载到window上的原因是,能够在控制台经过打印直接运用window.isChange.value = true来更该数据,触发更新。

处理children更新的逻辑仍是在patchElement函数中触发:

const patchElement = function(n1, n2, container, parentComponent) {

 const oldProps = n1.props || EMPTY_OBJ
 const newProps = n2.props || EMPTY_OBJ

 const el = (n2.el = n1.el)
 // children update
 patchChildren(n1, n2, el, parentComponent) // 修正
 // props update
 patchProps(el, oldProps, newProps)
}

那么当存在子节点时,更新为文本节点怎样来处理呢?答案是将之前的子节点悉数删去,然后从头设置文本节点。

const patchChildren = function(n1, n2, container, parentComponent) {
 // 取出新旧节点的shapeFlag
 const prevShapFlag = n1.shapeFlag
 const c1 = n1.children
 const c2 = n2.children
 const { shapeFlag } = n2
 // 当新的vnode的children为string类型时
 if(shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  // 之前children为array
  if(prevShapFlag & ShapeFlags.ARRAY_CHILDREN) {
   unmountChildren(n1.children)
   }
    // 判别新旧children是否共同?
  if(c1 !== c2) {
   setElementText(container, c2)
   }
  }
}

首要取出新旧vnodeshapeFlag用于子节点的类型判别,假如更新类型为Array->String,那么会删去本来的节点,然后设置新的文本节点。

删去子节点的操作长这样:

const unmountChildren = function(children) {
  // 循环children
  for(let i = 0; i < children.length; i++) {
    // 找到el
    let el = children[i].el
		// 根绝DOM节点找到其父节点
    let parent = el.parentNode
    // 删去
    if(parent) parent.removeChild(el)
  }
}

循环查找每个子节点的el特点,也便是节点的真实DOM节点,依据parentNode获取父节点,然后进行删去。

最终设置文本节点:

const setElementText = function(el, text) {
  el.textContent = text
}

其实上面的代码也现已将String->String完成了,因为进入旧的childrenString这个逻辑判别后,假如新的children也为String而且不相等,那么就会触发setElementText函数,更新文本节点。

将上面比如中的新旧vnode替换成:

// ex3 Text -> Text
const prevChild = "oldChildren"
const nextChild = "newChildren"

页面也会成功更新。

最终Text -> Array的逻辑也很简单,运用setElementText函数清空文本节点,然后运用mountChildren来从头烘托children

const patchChildren = function(n1, n2, container, parentComponent) {
  const prevShapFlag = n1.shapeFlag
  const c1 = n1.children
  const c2 = n2.children
  const { shapeFlag } = n2
  if(shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    if(prevShapFlag & ShapeFlags.ARRAY_CHILDREN) {
      unmountChildren(n1.children)
    }
    if(c1 !== c2) {
      setElementText(container, c2)
    }
  } else {
    // 旧children为string
    if(prevShapFlag & ShapeFlags.TEXT_CHILDREN) {
      setElementText(container, "")
      mountChildren(c2, container, parentComponent)
    }
  }
}

从头设置新旧vnode

// ex2 Text -> Array
const prevChild = "oldChildren"
const nextChild = [h("div", {}, "A"), h("div", {}, "B")];

页面也现已成功进行更新。

四. 更新children – 两头比照diff算法

上文中咱们现已处理了和childrenstring类型相关的更新操作。现在只剩下了重头戏,也便是节点更新时需求优化的要点。便是Array -> Array这种状况,因为在vnode的更新操作中,节点的层级通常是十分多的,不行能直接将一切的旧节点悉数删去,然后从头将新的vnode烘托出来。所以diff算法就出现了,旨在最小化的变更DOM。

相同咱们也会将节点的比照划分为几种状况。首要处理的是需求增加或许删去的状况。

增加指的是新旧的vnode的差异只存在于新的vnode在后边增加了几个子节点,除此之外其他的节点悉数共同,比如咱们以ABC当作节点为例:

更新!更新!完成vue3虚拟DOM更新&diff算法优化

此次更新增加了DE节点,除此之外和旧的vnode共同。

更新!更新!完成vue3虚拟DOM更新&diff算法优化

此次变更,新的vnode在前面增加了AB子节点,除此之外其他的节点与之前共同。

这种更新其实逻辑仍是比较简单的,无非便是找到新增加的节点,然后创立DOM就完事了,可是怎样去找这就又是一门学问了,前面现已说过了,为了寻求功用上满足的好,咱们不行能去每一个节点都拿往来不断新旧循环比照,这样的价值太大了,不如有很小的改动,就要循环整个vnode,不免有点太蠢了。那么怎样能够高效的找到增加节点开端的方位呢

其实最首要的意图便是要确认增加的节点规模,咱们能够运用双端指针。

界说指针i,它的作用是从开端节点开端向后进行比照,假如新旧节点在i方位相同,那么i++,往前走一步,即new[i] === old[i],这儿咱们运用newold来表明新旧节点。

界说指针e1e2e1代表指向旧的vnode最终一个子节点,而e2代表指向新的vnode最终一个子节点,也便是别离指向新旧vnode的末端。然后顺次进行比照,假如相同,则撤退一步,即old[e1] === new[e2]

更新!更新!完成vue3虚拟DOM更新&diff算法优化

假如i指针暂停,那么代表找到了不同节点的开端方位,而e1e2指针的暂停,则表明找到了新旧vnode不同节点的完毕方位。

更新!更新!完成vue3虚拟DOM更新&diff算法优化

依据上面的比如,咱们找到了新旧节点差异的开端D和完毕E。

这中心还有一个关键的问题,怎样判别两个节点是相同的?

答案是:现在的做法是运用特点keytype。看见没,这便是日常开发中在for遍历循环的时分写key的重要性,真的是实打实的提升功用啊。

接下来就到了完成代码的环节了,在此之前,仍是先写一个小比如:

// 界说新旧children
const prevChild = [
  h("p", { key: "A" }, "A"),
  h("p", { key: "B" }, "B"),
  h("p", { key: "C" }, "C"),
];
const nextChild = [
  h("p", { key: "A" }, "A"),
  h("p", { key: "B" }, "B"),
  h("p", { key: "C" }, "C"),
  h("p", { key: "D" }, "D"),
  h("p", { key: "E" }, "E"),
];
const App = {
  setup() {
    // 设置呼应式数据
    const isChange = reactive({
      value: false
    })
    window.isChange = isChange;
    return {
      isChange
    }
  },
  render() {
    let self = this
		// 呼应式数据改动时,更新children
    return self.isChange.value === true ? 
          h('div', {}, nextChild) :
          h('div', {}, prevChild)
  }
}

更新的逻辑仍是在patchChildren中来完成,上文中咱们现已完成了和文本节点相关的逻辑:

const patchChildren = function(n1, n2, container, parentComponent, anchor) { // 修正
 
 // 省掉...
 if(shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  // 省掉...
  } else {
  // 当时children为array
  if(prevShapFlag & ShapeFlags.TEXT_CHILDREN) {
   setElementText(container, "")
   mountChildren(c2, container, parentComponent, anchor) // 修正
   } else {
   // Array -> Array
   // diff children
   patchKeyedChildren(c1, c2, container, parentComponent, anchor) // 修正
   }
  }
}

patchKeyedChildren函数便是咱们处理更新的战场。

第一步,首要依据指针确认开端/完毕方位:

function patchKeyedChildren(
 c1,
 c2,
 container,
 parentComponent
) {
 const l2 = c2.length;
 // 界说前指针i
 let i = 0;
 // 界说后指针e1,e2
 // 别离指向新旧节点的尾部
 let e1 = c1.length - 1;
 let e2 = l2 - 1;
 // 判别是否为相同节点
 function isSomeVNodeType(n1, n2) {
  return n1.type === n2.type && n1.key === n2.key;
  }
  // 移动i
 while (i <= e1 && i <= e2) {
  const n1 = c1[i];
  const n2 = c2[i];
    // 假如为相同节点,则递归调用patch,因为此子节点纷歧定为最终的文本节点
  if (isSomeVNodeType(n1, n2)) {
   patch(n1, n2, container, parentComponent, parentAnchor);
   } else {
   break;
   }

  i++;
  }
 // e1, e2前移
 while (i <= e1 && i <= e2) {
  const n1 = c1[e1];
  const n2 = c2[e2];

  if (isSomeVNodeType(n1, n2)) {
   patch(n1, n2, container, parentComponent, parentAnchor);
   } else {
   break;
   }

  e1--;
  e2--;
  }
}

当i和e1,e2都移动完毕后,此刻三个指针的方位别离为:

i === 3
e1 === 2
e2 === 4

这三个指针的方位能够阐明许多问题,能够反映新旧vnode的差异到底是什么样的状况:

  • 当i指针大于e1指针,小于等于e2指针时,代表需求创立新节点,此刻从e1指针后边的方位开端创立新节点即可,完毕方位坐落e2。
  • 当i指针小于e1指针而且大于e2指针时,代表需求删去多余的旧节点。
  • 剩下的状况代表差异方位不在开端或许完毕方位,而在中心方位(此状况下文完成)。

拿我上面的来看:

// 旧vnode
const prevChild = [
 h("p", { key: "A" }, "A"),
 h("p", { key: "B" }, "B"),
 h("p", { key: "C" }, "C"),
];
// 新vnode
const nextChild = [
 h("p", { key: "D" }, "D"),
 h("p", { key: "A" }, "A"),
 h("p", { key: "B" }, "B"),
 h("p", { key: "C" }, "C"),
];

// 指针方位别离为
i === 0
e1 === -1
e2 === 0
// 旧vnode
const prevChild = [
 h("p", { key: "A" }, "A"),
 h("p", { key: "B" }, "B"),
 h("p", { key: "C" }, "C"),
];
// 新vnode
const nextChild = [
 h("p", { key: "A" }, "A"),
 h("p", { key: "B" }, "B"),
 h("p", { key: "C" }, "C"),
 h("p", { key: "D" }, "D"),
 h("p", { key: "E" }, "E"),
];

// 指针方位别离为
i === 3
e1 === 2
e2 === 4

全都契合咱们第一种状况。

完成前两种状况:

function patchKeyedChildren(
 c1,
 c2,
 container,
 parentComponent,
 parentAnchor
) {
 const l2 = c2.length;
 let i = 0;
 let e1 = c1.length - 1;
 let e2 = l2 - 1;

 function isSomeVNodeType(n1, n2) {
  return n1.type === n2.type && n1.key === n2.key;
  }

 while (i <= e1 && i <= e2) {
  const n1 = c1[i];
  const n2 = c2[i];

  if (isSomeVNodeType(n1, n2)) {
   patch(n1, n2, container, parentComponent, parentAnchor);
   } else {
   break;
   }

  i++;
  }

 while (i <= e1 && i <= e2) {
  const n1 = c1[e1];
  const n2 = c2[e2];

  if (isSomeVNodeType(n1, n2)) {
   patch(n1, n2, container, parentComponent, parentAnchor);
   } else {
   break;
   }

  e1--;
  e2--;
  }

 if (i > e1) {
  if (i <= e2) {
   // 获取刺进DOM节点的方位
   const nextPos = e2 + 1;
   // 此刺进方位的核算首要针对坐落vnode前端增加节点的状况
   // 在vnode后端增加节点直接传入null,作用等同于append
   const anchor = nextPos < l2 ? c2[nextPos].el : null;
   while (i <= e2) {
    patch(null, c2[i], container, parentComponent, anchor);
    i++;
    }
   }
  } else if (i > e2) {
  while (i <= e1) {
   remove(c1[i].el);
   i++;
   }
  } else {
  // 中心比照
  }
}
// 依据父节点删去
const remove = function(child) {
 const parent = child.parentNode

 if(parent) parent.removeChild(child)
}

还记得之前咱们在mountElement函数中挂载DOM时,是直接运用的append,现在来看是不契合现在的要求的,试想,假如咱们在vnode的前端增加节点,你还能都给我加到末尾吗?明显是不合理的。

所以咱们需求记载一个坐标,用于DOM的insert刺进节点,依据上面咱们指针的移动规则,这个坐标记载e2的下一位即可,因为咱们要在e2的方位增加节点,而insertBefore是增加到指定节点之前,所以咱们要记载e2指针的下一个方位。

先修正mountElement函数的挂载方法:

const mountElement = function (vnode, container, parentComponent, anchor) { // 修正
 // 省掉...

 // props
 for (const key in props) {
  const prop = props[key]

  mountProps(el, key, null, prop)
  }

 // container.append(el)
 insert(container, el, anchor) // 修正
}

// 刺进
const insert = function(parent, child, anchor) {
 parent.insertBefore(child, anchor || null)
}

因为咱们有增肌了参数anchor,而patch函数和mountElement函数并不直接有调用关系,所以…又是一轮大规模的参数传递…

render函数:

const render = function (vnode, container) {
 // 初始化烘托不需求传递坐标
 patch(null, vnode, container, null, null) // 修正
}

patch函数:

const patch = function (n1, n2, container, parentComponent, anchor) { // 修正

 const { type, shapeFlag } = n2 

 switch(type) {
  case Fragment:
   processFragment(n1, n2, container, parentComponent, anchor); // 修正
   break
  case Text:
   processText(n1, n2, container);
   break
  default:
   if (shapeFlag & ShapeFlags.ELEMENT) {
    processElement(n1, n2, container, parentComponent, anchor) // 修正
    } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
    processComponent(n1, n2, container, parentComponent, anchor) // 修正
    }
   break
  }
}

processFragment函数:

const processFragment = function(n1, n2, container, parentComponent, anchor) {  // 修正
  mountChildren(n2.children, container, parentComponent, anchor)  // 修正
}

processElement函数:

const processElement = function (n1, n2, container, parentComponent, anchor) {  // 修正
  if(!n1) {
    mountElement(n2, container, parentComponent, anchor)  // 修正
  } else { 
    patchElement(n1, n2, container, parentComponent, anchor)  // 修正
  }
}

patchElement函数:

const patchElement = function(n1, n2, container, parentComponent, anchor) { // 修正
  // 省掉...
 // children update
 patchChildren(n1, n2, el, parentComponent, anchor) // 修正
 // props update
 patchProps(el, oldProps, newProps)
}

patchChildren函数:

const patchChildren = function(n1, n2, container, parentComponent, anchor) { // 修正
 
 // 省掉...

 if(shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  // 省掉...
  } else {
  // 当时children为array
  if(prevShapFlag & ShapeFlags.TEXT_CHILDREN) {
   setElementText(container, "")
   mountChildren(c2, container, parentComponent, anchor) // 修正
   } else {
   // Array -> Array
   // diff children
   patchKeyedChildren(c1, c2, container, parentComponent, anchor) // 修正
   }
  }
}

mountElement函数:

const mountElement = function (vnode, container, parentComponent, anchor) { // 修正
 // 省掉...

 if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  el.textContent = children
  } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  mountChildren(children, el, parentComponent, anchor) // 修正
  }

 // props
 for (const key in props) {
  const prop = props[key]

  mountProps(el, key, null, prop)
  }

 // container.append(el)
 insert(container, el, anchor) // 修正
}

mountChildren函数:

const mountChildren = function (children, container, parentComponent,anchor) { // 修正
 children.forEach(v => {
  patch(null, v, container, parentComponent, anchor) // 修正
  })
}

processComponent函数:

const processComponent = function (n1, n2, container, parentComponent, anchor) { // 修正
 mountComponent(n2, container, parentComponent, anchor) // 修正
}

mountComponent函数:

const mountComponent = function (vnode, container, parentComponent, anchor) { // 修正
 // 创立组件实例
 const instance = createComponentInstance(vnode, parentComponent)

 setupComponent(instance)
 setupRenderEffect(instance, vnode, container, anchor) // 修正
}

setupRenderEffect函数:

const setupRenderEffect = function (instance, vnode, container, anchor) { // 修正

 effect(()=>{
  if(!instance.isMounted) {
   // 省掉...

   patch(null, subTree, container, instance, anchor); // 修正
   vnode.el = subTree.el;
   instance.isMounted = true;
   } else {
   // 省掉...

   instance.subTree = subTree;
   patch(prevSubTree, subTree, container, instance, anchor) // 修正

   }
  })
}

完整代码见:github.com/konvyi/vue3…

五. 更新children – 中心比照(修正/删去)

处理完两头的节点,再看看一下中心节点,这种状况是指差异的部分坐落中心。

处理中心节点的差异也有三种状况:

  • 删去
  • 新建
  • 修正
  • 复用

咱们先来看一下删去节点和修该节点的状况。

删去节点是指节点存在于旧的vnode中,而不存在新的vnode中,修正是指节点是相同的(具有相同的key,而修正节点的props或许children)。

例如:

//旧
const prevChild = [
 h("p", { key: "A" }, "A"),
 h("p", { key: "B" }, "B"),

 h("p", { key: "Z" }, "Z"),
 h("p", { key: "C", id: "c-prev" }, "C"),
 h("p", { key: "D" }, "D"),

 h("p", { key: "F" }, "F"),
 h("p", { key: "G" }, "G"),
];
// 新
const nextChild = [
 h("p", { key: "A" }, "A"),
 h("p", { key: "B" }, "B"),

 h("p", { key: "C", id:"c-next" }, "C"),

 h("p", { key: "F" }, "F"),
 h("p", { key: "G" }, "G"),
];

在上面这个比如中,咱们删去了Z和D节点,修正了C节点的id特点。两头的节点是相同的。

patchKeyedChildren这个函数中,咱们现已处理完了双端比照:

function patchKeyedChildren(
 c1,
 c2,
 container,
 parentComponent,
 parentAnchor
) {
 
 // 省掉...
 if (i > e1) {
  // 省掉...
  } else if (i > e2) {
  // 省掉...
  } else {
  // 既不大于e1,也不大于e2,阐明差异的开端方位坐落中心
  // 中心比照
  let s1 = i
  let s2 = i
    // 核算需求处理的长度
  const toBePatched = e2 - s2 + 1
  // 当时处理的方位
  let patched = 0
  // map映射,用于保存新vnode中的节点方位
  const keyToNewIndexMap = new Map()
    // 保存
  for(let i = s2; i <= e2; i++) {
   let nextChild = c2[i]
   keyToNewIndexMap.set(nextChild.key, i)
   }
    // 遍历旧vnode
  for(let i = s1; i <= e1; i++) {
   const prevChild = c1[i]
      // 优化,假如patched的值大于需求处理的长度
   // 代表旧的剩下的需求删去
   if(patched >= toBePatched) {
    remove(prevChild.el)
    continue
    }

   let newIndex
   // 首要判别是否在map映射中查找到key值,
   // 假如没有查找到则只能遍历一切新vnode查找
   if(prevChild.key != null) {
    newIndex = keyToNewIndexMap.get(prevChild.key)
    } else {
    for(let j = s2; j <= e2; j++) {
     if(isSomeVNodeType(prevChild, c2[j])) {
      newIndex = j
      break
      }
     }
    }
      // 没有查找到下标,直接删去旧节点
   if(newIndex === undefined) {
    remove(prevChild.el)
    } else {
    // 查找到则进一步递归比照,patched++
    patch(prevChild, c2[newIndex], container, parentComponent, null)
    patched++
    }
   }
  }
}

删去的思路只要是验证旧的vnode节点是否坐落新的vnode之中,两种方法来验证,一是运用key与新节点的下标进行映射,当便当旧节点时,运用旧节点的key进行查找,假如旧节点中没有key,则是能遍历一切新节点进行寻找。(再一次印证了key的重要性)至于节点中propschildren的更新,直接走patch函数中的更新流程的即可。

接下来就瓜熟蒂落了,能够查找到就继续调用patch进行深层比照,没有查找到则阐明在新的vnode中该节点现已不存在,直接删去。

六. 更新children – 中心比照(增加/移动)

可能有许多人难以了解标题的内容,增加节点能够了解,可是移动是怎样一回事?

要答复这个问题,仍是先看一下这个:

const prevChild = [
 h("p", { key: "A" }, "A"),
 h("p", { key: "B" }, "B"),

 h("p", { key: "C" }, "C"),
 h("p", { key: "D" }, "D"),
 h("p", { key: "E" }, "E"),
 h("p", { key: "Z" }, "Z"),

 h("p", { key: "F" }, "F"),
 h("p", { key: "G" }, "G"),
];

const nextChild = [
 h("p", { key: "A" }, "A"),
 h("p", { key: "B" }, "B"),

 h("p", { key: "D" }, "D"),
 h("p", { key: "C" }, "C"),
 h("p", { key: "Y" }, "Y"),
 h("p", { key: "E" }, "E"),

 h("p", { key: "F" }, "F"),
 h("p", { key: "G" }, "G"),
];

const App = {
 setup() {
  const isChange = reactive({
   value: false
   })
  window.isChange = isChange;

  return {
   isChange
   }
  },

 render() {
  let self = this

  return self.isChange.value === true ?
   h('div', {}, nextChild) :
   h('div', {}, prevChild)
  }
}

这个比如其实和之前的更新思路是共同的(开端和完毕方位的节点未产生改动),只不过产生改动的节点方位产生了改动:

// 旧的vnode
h("p", { key: "C" }, "C"),
h("p", { key: "D" }, "D"),
h("p", { key: "E" }, "E"),
h("p", { key: "Z" }, "Z"),

// 新的vnode
h("p", { key: "D" }, "D"),
h("p", { key: "C" }, "C"),
h("p", { key: "Y" }, "Y"),
h("p", { key: "E" }, "E"),

调查上面的节点可知,Z节点为需求删去的节点,Y节点是需求创立的节点,而C,D,E节点都是未产生改动的,假如咱们仅仅单纯的将一切开端与完毕节点不同的一段vnode悉数删去重建,不免有点太浪费了,怎样高效的对一切现已创立的节点进行运用呢?其实这就答复了上面的问题,移动。

更新!更新!完成vue3虚拟DOM更新&diff算法优化

如图所示,其实刨开Z的删去和Y的新增,只需求将D移动至开端就能够了,最小化的修正了咱们的DOM。

function patchKeyedChildren(
 c1,
 c2,
 container,
 parentComponent,
 parentAnchor
) {
  // 省掉...
  if (i > e1) {
   // 省掉...
   } else if (i > e2) {
   // 省掉...
   } else {
   // 中心比照
  let s1 = i
  let s2 = i

  const toBePatched = e2 - s2 + 1
  let patched = 0
  const keyToNewIndexMap = new Map()
    // 用于保存需求处理的节点下标
  const newIndexToOldIndexMap = new Array(toBePatched) // 新增
  // 数组补0
  for (let i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0 // 新增


  for (let i = s2; i <= e2; i++) {
   let nextChild = c2[i]
   keyToNewIndexMap.set(nextChild.key, i)
   }

  for (let i = s1; i <= e1; i++) {
   const prevChild = c1[i]

   if (patched >= toBePatched) {
    remove(prevChild.el)
    continue
    }

   let newIndex
   if (prevChild.key != null) {
    newIndex = keyToNewIndexMap.get(prevChild.key)
    } else {
    for (let j = s2; j <= e2; j++) {
     if (isSomeVNodeType(prevChild, c2[j])) {
      newIndex = j
      break
      }
     }
    }

   if (newIndex === undefined) {
    remove(prevChild.el)
    } else {
    // 依据新vnode中的方位来保存旧vnode节点的下标
    newIndexToOldIndexMap[newIndex - s2] = i + 1 // 新增

    patch(prevChild, c2[newIndex], container, parentComponent, null)
    patched++
    }
   }
    // 获取最长递加子序列
  const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap) // 新增

  let j = increasingNewIndexSequence.length - 1
  for (let i = toBePatched - 1; i >= 0; i--) {
   const nextIndex = i + s2
   const nextChild = c2[nextIndex]
     // 刺进节点,记载锚点
   const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null
   // 假如在newIndexToOldIndexMap为0,则阐明未找到newIndex,为新增节点
   if (newIndexToOldIndexMap[i] === 0) {
    patch(null, nextChild, container, parentComponent, anchor)
    } else {
    if (j < 0 || increasingNewIndexSequence[j] !== i) {
     insert(container, nextChild.el, anchor)
     } else {
     j--
     }
    }
   }
   }
  }

首要界说一个数组用来存储需求更新的节点下标,初始化时运用0进行填充,假如能够获取到newIndex的话,阐明该节点在新旧的vnode中同时存在,咱们会对这种节点的下标进行填充,而假如是新增节点的话,咱们并不能处理到数组中为0的值,也便是该方位不会被填充,仍然为0,这也是下文中咱们判别新增节点与移动节点的依据。

接下来就到了移动节点的操作,这需求咱们筛选出到底不需求移动的是哪些?这需求一个重要的辅佐函数-最长递加子序列。

function getSequence(arr) {
 const p = arr.slice();
 const result = [0];
 let i, j, u, v, c;
 const len = arr.length;
 for (i = 0; i < len; i++) {
  const arrI = arr[i];
  if (arrI !== 0) {
   j = result[result.length - 1];
   if (arr[j] < arrI) {
    p[i] = j;
    result.push(i);
    continue;
    }
   u = 0;
   v = result.length - 1;
   while (u < v) {
    c = (u + v) >> 1;
    if (arr[result[c]] < arrI) {
     u = c + 1;
     } else {
     v = c;
     }
    }
   if (arrI < arr[result[u]]) {
    if (u > 0) {
     p[i] = result[u - 1];
     }
    result[u] = i;
    }
   }
  }
 u = result.length;
 v = result[u - 1];
 while (u-- > 0) {
  result[u] = v;
  v = p[v];
  }
 return result;
}

关于这个函数是怎样完成的,因为本篇篇幅有点太长了,我预备在vue3系列完毕之后独自写一篇文章,这儿其实对了解整个更新过程影响不是特别大,只需求知道这个函数能够将记载数组中整个递加的数字的下标,就拿咱们上文中的比如来说:

// 记载完下标后
newIndexToOldIndexMap = [4, 3, 0, 5]

// 这也就对应了
[D, C, Y, E]
[4, 3, 0, 5]
// Y为0,代表新增
// 其他别离代表在旧的vnode中的下标方位

经过处理之后:

const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap)
increasingNewIndexSequence = [1, 3]

代表节点C,E是无需改变的节点。

最终倒序移动需求移动的节点D就能够了。

七. 完成组件更新

更新!更新!完成vue3虚拟DOM更新&diff算法优化

以上的更新都是属于element的更新,那么componet(组件)怎样进行更新呢?

比如有以下:

const App = {
 setup() {
  // 界说呼应式数据
  let count = reactive({
   value: 'pino'
   })
  // 修正count.value
  let changeCount = function() {
   count.value = 'new-pino'
   }

  return {
   count,
   changeCount
   }
  },
 
 render() {
  return h('div', {}, [
   h('p', {}, 'App'),
   // 子组件,设置props
   h(Child, { count: this.count.value }),
   h('button', { onClick: this.changeCount }, 'change')
   ])
  }
}

const Child = {
 setup() {},
 render() {
  // 运用this.$props运用props数据
  return h('div', {}, `Child->props:${this.$props.count}`)
  }
}

在组件App中,咱们设置了呼应式数据count并将其作为props传递给子组件Child,当点击按钮change时分。改动count的值,那么在子组件Child中的值也应该产生改动。这便是咱们完成的作用。

依据上面的剖析不难看出,其实首要更新的便是组件的props特点。

首要为获取数据时增加props拦截:(此为上文中的内容,拜见上一篇初次烘托的文章)

const publicPropertiesMap = {
 $el: i => i.vnode.el,
 $slots: i => i.slots,
 // 增加$props拦截,经过$props来访问props数据
 $props: i => i.props // 增加
}

component实例中增加next特点,用于保存最新更新的vnode

const component = {
 vnode,
 type: vnode.type,
 next: null, // 新增
 props: {},
 setupState: {},
 provides: parent ? parent.provides : {},
 parent: parent ? parent : {},
 isMounted: false,
 subTree: {},
 slots: {},
 emit: () => {}
}

在创立vnode时增加component特点用于保存component特点:

const vnode = {
 type,
 props,
 children,
 component: null, // 增加
 key: props && props.key,
 shapeFlag: getShapeFlag(type),
 el: null,
}

processComponent函数中判别是否存在n1,假如存在的话则阐明为更新操作,需求处理更新的逻辑,假如没有n1则阐明是初次烘托:

const processComponent = function (n1, n2, container, parentComponent, anchor) {
 if(!n1) { // 修正
  mountComponent(n2, container, parentComponent, anchor) // 修正
  } else { // 修正
  // 处理更新
  updateComponent(n1, n2) // 修正
  } // 修正
}

在初次烘托时保存component实例:

const mountComponent = function (vnode, container, parentComponent, anchor) {
 // 创立组件实例
 const instance = (vnode.component = createComponentInstance(vnode, parentComponent)) // 修正

 setupComponent(instance)
 setupRenderEffect(instance, vnode, container, anchor)
}

updateComponent函数处理组件更新逻辑:

const updateComponent = function(n1, n2) {
  // 获取component实例
 const instance = (n2.component = n1.component)
 // 判别是否需求更新?
 if(shouldUpdateComponent(n1, n2)) {
  // 保存最新vnode
  instance.next = n2;
  // 更新DOM
  instance.update();
  } else {
  // 不需求更新则复用el
  // 将n2设置为实例的vnode特点(更新vnode)
  n2.el = n1.el;
  instance.vnode = n2;
  }
}

此刻的n1n2(新旧vnode):

更新!更新!完成vue3虚拟DOM更新&diff算法优化

此刻props中的特点count现已产生了改动。

整个更新函数还有两个关键点,怎样判别是需求更新?怎样进行更新?

判别更新便是将新的props目标进行遍历,再与旧的props进行比照,假如不相同则需求更新。

const shouldUpdateComponent = function(prevVNode, nextVNode) {
 const { props: prevProps } = prevVNode;
 const { props: nextProps } = nextVNode;
  // 遍历新的props
 for (const key in nextProps) {
  // 假如有不同的特点,回来true(需求更新)
  if (nextProps[key] !== prevProps[key]) {
   return true;
   }
  }

 return false;
}

判别是完成了,那么怎样更新呢,因为更新props不行能仅仅把实例中的特点更改了就完事了,还需求再对页面中的作用进行更新,因为最终是要显现在页面上的。

能够想一下咱们在element的更新中是怎样更新页面的呢?是用effect进行绑定函数的方法,监听到数据的改动,再次履行烘托函数履行render函数的。那么咱们的component的更新是不是也能够借用呼应式呢?

答案当然是能够的,咱们在初次烘托的时分将effect函数进行保存,在需求更新的时分调用就能够了。还记得在呼应式那一节中effect函数的回来值是啥吗,调用effect函数的回来值还能够履行依赖函数,这也就完成了更新页面的功用。(不熟悉呼应式能够拜见呼应式那一部分的文章)

const setupRenderEffect = function (instance, vnode, container, anchor) {
  // 保存effect函数
  instance.update = effect(() => { // 修正
   if (!instance.isMounted) {
    console.log("init");
    const { proxy } = instance;
    const subTree = (instance.subTree = instance.render.call(proxy));
 
    patch(null, subTree, container, instance, anchor);
    vnode.el = subTree.el;
    instance.isMounted = true;
    } else {
    console.log("update");
 
    const { next, vnode } = instance // 增加
    if(next) { // 增加
     next.el = vnode.el // 增加
         // 在履行更新页面之前首要要先更新component实例中的各个特点
     updateComponentPreRender(instance, next) // 增加
     } // 增加
 
    const { proxy } = instance
    const subTree = instance.render.call(proxy)
    const prevSubTree = instance.subTree
 
    instance.subTree = subTree;
    patch(prevSubTree, subTree, container, instance, anchor)
 
    }
  })
}

const updateComponentPreRender = function(instance, nextVNode) {
 instance.vnode = nextVNode
 instance.next = null
 
 instance.props = nextVNode.props
} 

八. 完成nextTick功用

尽管更新功用现已完成的差不多了,可是其实在履行的时机上仍是有很大的问题,比如咱们有以下的:

const App = {
 setup() {
  // 界说呼应式数据count
  let count = reactive({
   value: 0
   })
    // 
  let changeCount = function() {
   for(let i = 0; i < 10; i++) {
    count.value = count.value + 1
    }
   }

  return {
   count,
   changeCount
   }
  },
 render() {
  return h('div', {}, [
   h('p', {}, `count: ${this.count.value}`),
   h('button', { onClick: this.changeCount }, 'update')
   ])
  }
}

在这个比如中,咱们经过点击按钮update来触发changeCount函数,changeCount函数中循环为count增加10次。

更新!更新!完成vue3虚拟DOM更新&diff算法优化

咱们在更新逻辑时打印”update”,发现更新逻辑竟然履行了10次,也便是出发了10次更新DOM的操作,哪怕产生的改动仅仅仅仅把呼应式数据加一!

这显然是十分离谱的,那么怎样能够削减更新呢,以上面的比如为例,其实咱们只在循环完毕后履行一次更新DOM的操作就能够了。那么其实处理的方案也很简单,把更新操作放到微使命行列就能够了嘛。微使命是等到一切的同步使命悉数履行完毕后才会履行,那么等到微使命里边的更新操作开端履行时,咱们的循环当然现已履行完毕了,所以更新操作只会履行一次。

可是假如故事到这儿就完毕其实也挺美好的,可是工作往往都不会一帆风顺,假如像咱们上面的主意完成的话,那么又会出现一个新的问题,看下边的:

const App = {
 setup() {
  let count = reactive({
   value: 0
   })
  
  let changeCount = function() {
   for(let i = 0; i < 10; i++) {
    count.value = count.value + 1
    }
   // 获取当时component实例
   const instance = getCurrentInstance()
   console.log(instance, 'instance');
   }

  return {
   count,
   changeCount
   }
  },
 render() {
  return h('div', {}, [
   h('p', {}, `count: ${this.count.value}`),
   h('button', { onClick: this.changeCount }, 'update')
   ])
  }
}

假如咱们在for循环后边直接获取最新的component实例,因为咱们的DOM更新操作放到了微使命里边,那么获取的当时实例天然不会是最新的,可是咱们在呼应式数据改动之后获取实例本意肯定是想获取最新的component实例,可是因为咱们更新DOM操作的延后,导致正常的功用会受到影响。

那么怎样处理呢?

还记得nextTick吗?

 this.$nextTick(() => {
  console.log(getCurrentInstance())
 })

nextTick里边能够获取当最新的DOM。

nextTick的完成也很直接,既然你把更新DOM的操作放到了微使命里,那么我也把nextTick里边的履行逻辑也放在微使命里边不就能够了。

咱们在每次履行instance.update函数时,运用queueJobs函数进行处理:

const setupRenderEffect = function (instance, vnode, container, anchor) {
  instance.update = effect(() => { 
   // 省掉...
  }, {
  // 运用scheduler函数进行包裹
  // 履行时会履行此函数
  scheduler() {
   queueJobs(instance.update);
   }
  })
}

这儿运用effect函数的第二个参数进行处理,不熟悉的话需求熟悉一下呼应式的部分。

接下来完成queueJobs函数:

// 初始化履行栈
const queue = []
// 界说一个成功的promise状况
const p = Promise.resolve()
// 界说是否增加使命的状况
let isFlushPending = false

const queueJobs = job => {
 // 假如使命未被增加到行列中
 if(!queue.includes(job)) {
  queue.push(job)
  }

 queueFlush()
}

queueFlush函数首要用于判别是否增加至微使命:

const queueFlush = () => {
 // 假如当时处于true,则直接回来
 if(isFlushPending) return
 isFlushPending = true
  // 调用nextTick函数将flushJobs增加至微使命行列
 nextTick(flushJobs)
}
// 取出一切行列中的使命履行
const flushJobs = () => {
 isFlushPending = false
 let job
 while((job = queue.shift())) {
  job && job()
  }
}

const nextTick = fn => {
 // 运用Promise.then
 return fn ? p.then(fn) : p
}

其实中心思想便是在创立完一次使命增加至微使命行列之后,后续的履行都仅仅将函数增加到queue行列中。当履行微使命后isFlushPending开关变为false,之后能够再次增加使命到微使命中。

而直接履行nextTick函数则会自定创立一个使命到微使命中:

const App = {
 setup() {
  let count = reactive({
   value: 0
   })
  
  let changeCount = function() {
   for(let i = 0; i < 10; i++) {
    count.value = count.value + 1
    }
      // 在nextTick函数中调用
   nextTick(()=>{
    const instance = getCurrentInstance()
    })
   }

  return {
   count,
   changeCount
   }
  },
 render() {
  return h('div', {}, [
   h('p', {}, `count: ${this.count.value}`),
   h('button', { onClick: this.changeCount }, 'update')
   ])
  }
}

写在最终

未来可能会更新完成mini-vue3javascript根底知识系列,希望能一向坚持下去,期待多多点赞,一同前进!