组件的产出是什么?

一个组件就是一个函数,给我什么样的数据,我就渲染对应的 html 内容
例证 最早的组件,给定一个模板,和数据,输出相应的html内容;当数据改变时,重新渲染模板。
区别 过去的输出为html字符串,现在是Virtual Dom ,组件的产出就是 Virtual DOM(VNode)
本质:为什么要改为输出VNode? 分离组件,增加复用性,减少耦合度。比如一个stepper,可能多处会使用;分层设计,使得渲染过程抽象,可以渲染到web之外的平台(如微信)
**

何为VNode

VNode 是真实 DOM 的描述

  1. // html标签(string)
  2. const elementVnode = {
  3. tag: 'div'
  4. }
  5. // 组件
  6. const componentVnode = {
  7. tag: MyComponent
  8. }

渲染器(抽象) 接收两个参数,分别是将要渲染的 vnode 和 元素挂载点(真实 DOM 被渲染的位置)

  1. // 把 elementVnode 渲染到 id 为 app 的元素下
  2. render(elementVnode, document.getElementById('app'))

设计VNode

用 VNode 描述真实 DOM

一个 html 标签有它的名字、属性、事件、样式、子节点等诸多信息,这些内容都需要在 VNode 中体现。
那么我们设计的VNode可能会有这些属性: tag(标签名div, span等),data(node的对应数据),children(子节点信息,变成了一棵树),当然可以添加更多属性,但是维护更麻烦,所以现在先加入这些属性。

  1. // 红色背景的正方形 div 元素
  2. const elementVNode = {
  3. tag: 'div',
  4. data: {
  5. style: {
  6. width: '100px',
  7. height: '100px',
  8. backgroundColor: 'red'
  9. }
  10. }
  11. }
  12. // 描述一个有子节点的 div 元素
  13. const elementVNode = {
  14. tag: 'div',
  15. data: null,
  16. children: {
  17. tag: 'span',
  18. data: null
  19. }
  20. }
  21. // 若有多个子节点,则可以把 children 属性设计为一个数组:
  22. const elementVNode = {
  23. tag: 'div',
  24. data: null,
  25. children: [
  26. {
  27. tag: 'h1',
  28. data: null
  29. },
  30. {
  31. tag: 'p',
  32. data: null
  33. }
  34. ]
  35. }

文本节点 tag为null

  1. const textVNode = {
  2. tag: null,
  3. data: null,
  4. children: '文本内容'
  5. }

组件节点 对应的tag属性值则引用组件类(或函数)本身

  1. const elementVNode = {
  2. tag: 'div',
  3. data: null,
  4. children: {
  5. tag: MyComponent,
  6. data: null
  7. }
  8. }
  9. // 对应
  10. <div>
  11. <MyComponent />
  12. </div>

Fragment节点

  1. const Fragment = Symbol()
  2. const fragmentVNode = {
  3. // tag 属性值是一个唯一标识
  4. tag: Fragment,
  5. data: null,
  6. children: [
  7. {
  8. tag: 'td',
  9. data: null
  10. },
  11. {
  12. tag: 'td',
  13. data: null
  14. },
  15. {
  16. tag: 'td',
  17. data: null
  18. }
  19. ]
  20. }
  21. const elementVNode = {
  22. tag: 'td',
  23. data: null
  24. }
  1. <-- 使用场景 -->
  2. <template>
  3. <table>
  4. <tr>
  5. <Columns />
  6. </tr>
  7. </table>
  8. </template>
  9. <-- 对应模板 -->
  10. <template>
  11. <td></td>
  12. <td></td>
  13. <td></td>
  14. </template>

Potral节点
其最终效果是,无论你在何处使用 组件,它都会把内容渲染到 id=”app-root” 的元素下。由此可知,所谓 Portal 就是把子节点渲染到给定的目标,我们可以使用如下 VNode 对象来描述上面这段模板

  1. const Portal = Symbol()
  2. const portalVNode = {
  3. tag: Portal,
  4. data: {
  5. target: '#app-root'
  6. },
  7. children: {
  8. tag: 'div',
  9. data: {
  10. class: 'overlay'
  11. }
  12. }
  13. }
  1. <-- 使用场景 -->
  2. <template>
  3. <div id="box" style="z-index: -1;">
  4. <Overlay />
  5. </div>
  6. </template>
  7. <-- 对应模板 -->
  8. <template>
  9. <Portal target="#app-root">
  10. <div class="overlay"></div>
  11. </Portal>
  12. </template>

VNode的种类

总的来说,我们可以把 VNode 分成五类,分别是:html/svg 元素、组件、纯文本、Fragment 以及 Portal:
image.png

VNodeFlag

以现有的节点表示,如果需要知道VNode是什么类型,需要在挂载阶段,使用排除法得到,为了能够更快辨明节点类型,节省Node的render时间,可以通过flag实现
例如:

  1. // html 元素节点
  2. const htmlVnode = {
  3. flags: VNodeFlags.ELEMENT_HTML,
  4. tag: 'div',
  5. data: null
  6. }
  7. // svg 元素节点
  8. const svgVnode = {
  9. flags: VNodeFlags.ELEMENT_SVG,
  10. tag: 'svg',
  11. data: null
  12. }

VNode的children和对应flag

有无children,有单个/多个子节点,多个子节点有无key

VNode对象

  1. export interface VNode {
  2. // _isVNode 属性在上文中没有提到,它是一个始终为 true 的值,有了它,我们就可以判断一个对象是否是 VNode 对象
  3. _isVNode: true
  4. // el 属性在上文中也没有提到,当一个 VNode 被渲染为真实 DOM 之后,el 属性的值会引用该真实DOM
  5. el: Element | null
  6. flags: VNodeFlags
  7. tag: string | FunctionalComponent | ComponentClass | null
  8. data: VNodeData | null
  9. children: VNodeChildren
  10. childFlags: ChildrenFlags
  11. }

辅助创建 VNode 的 h 函数

何为h函数

我们可以看到这一个个的VNode,但是可以发现它非常长,生成起来相当复杂,冗余度也很高。不难想象,我们希望有一种形式化写法,可以简化这一问题,这就是h函数的需求。
用来创建VNode对象的函数封装,简单模型如下:

  1. // 创建一个空的 <h1></h1> 标签
  2. function h() {
  3. return {
  4. _isVNode: true,
  5. flags: VNodeFlags.ELEMENT_HTML,
  6. tag: 'h1',
  7. data: null,
  8. children: null,
  9. childFlags: ChildrenFlags.NO_CHILDREN,
  10. el: null
  11. }
  12. }

考虑,如果希望h函数的可用性变强,可以生成更多VNode可以增加输入,如:

  1. function h(tag, data = null, children = null) {
  2. //...
  3. }

在返回的值中,flags和childrenFlags都是根据tag和children判断得到的。
举例:

  1. <template>
  2. <div>
  3. <span></span>
  4. </div>
  5. </template>
  1. const elementVNode = h('div', null, h('span'))
  2. const elementVNode = {
  3. _isVNode: true,
  4. flags: 1, // VNodeFlags.ELEMENT_HTML
  5. tag: 'div',
  6. data: null,
  7. children: {
  8. _isVNode: true,
  9. flags: 1, // VNodeFlags.ELEMENT_HTML
  10. tag: 'span',
  11. data: null,
  12. children: null,
  13. childFlags: 1, // ChildrenFlags.NO_CHILDREN
  14. el: null
  15. },
  16. childFlags: 2, // ChildrenFlags.SINGLE_VNODE
  17. el: null
  18. }

生成组件,应当注意:

  1. class MyStatefulComponent extends Component {}
  2. class Component {
  3. render() {
  4. throw '组件缺少 render 函数'
  5. }
  6. }

一个组件来说它的 render 函数就是它的一切,它说明了一个组件如何渲染在页面上,状态型组件继承与Component类,其实现方法中应当由render()方法,如果没有,在渲染时会报错。

渲染器的挂载

何为渲染器

渲染器,简单的说就是将 Virtual DOM 渲染成特定平台下真实 DOM 的工具(就是一个函数,通常叫 render)

渲染器的工作流程分为两个阶段:mount 和 patch,如果旧的 VNode 存在,则会使用新的 VNode 与旧的 VNode 进行对比,试图以最小的资源开销完成 DOM 的更新,这个过程就叫 patch,或“打补丁”。如果旧的 VNode 不存在,则直接将新的 VNode 挂载成全新的 DOM,这个过程叫做 mount。

通常渲染器接收两个参数,第一个参数是将要被渲染的 VNode 对象,第二个参数是一个用来承载内容的容器(container),通常也叫挂载点,如下:

  1. function render(vnode, container) {
  2. const prevVNode = container.vnode
  3. if (prevVNode == null) {
  4. if (vnode) {
  5. // 没有旧的 VNode,只有新的 VNode。使用 `mount` 函数挂载全新的 VNode
  6. mount(vnode, container)
  7. // 将新的 VNode 添加到 container.vnode 属性下,这样下一次渲染时旧的 VNode 就存在了
  8. container.vnode = vnode
  9. }
  10. } else {
  11. if (vnode) {
  12. // 有旧的 VNode,也有新的 VNode。则调用 `patch` 函数打补丁
  13. patch(prevVNode, vnode, container)
  14. // 更新 container.vnode
  15. container.vnode = vnode
  16. } else {
  17. // 有旧的 VNode 但是没有新的 VNode,这说明应该移除 DOM,在浏览器中可以使用 removeChild 函数。
  18. container.removeChild(prevVNode.el)
  19. container.vnode = null
  20. }
  21. }
  22. }

思路如下:
image.png

挂载普通标签元素

不同类型的VNode,mount的方法各不相同:

  1. function mount(vnode, container) {
  2. const { flags } = vnode
  3. if (flags & VNodeFlags.ELEMENT) {
  4. // 挂载普通标签
  5. mountElement(vnode, container)
  6. } else if (flags & VNodeFlags.COMPONENT) {
  7. // 挂载组件
  8. mountComponent(vnode, container)
  9. } else if (flags & VNodeFlags.TEXT) {
  10. // 挂载纯文本
  11. mountText(vnode, container)
  12. } else if (flags & VNodeFlags.FRAGMENT) {
  13. // 挂载 Fragment
  14. mountFragment(vnode, container)
  15. } else if (flags & VNodeFlags.PORTAL) {
  16. // 挂载 Portal
  17. mountPortal(vnode, container)
  18. }
  19. }

先看mountElement基础代码:

  1. function mountElement(vnode, container) {
  2. // 生成DOM
  3. const el = document.createElement(vnode.tag)
  4. // 将DOM放到应该放的地方
  5. container.appendChild(el)
  6. }

这段代码有4个问题:
1、VNode 被渲染为真实DOM之后,没有引用真实DOM元素
2、没有将 VNodeData 应用到真实DOM元素上
3、没有继续挂载子节点,即 children
4、不能严谨地处理 SVG 标签

对应2,节点有style如下:

  1. const elementVnode = h(
  2. 'div',
  3. {
  4. style: {
  5. height: '100px',
  6. width: '100px',
  7. background: 'red'
  8. },
  9. class: 'cls-a cls-b'
  10. type: 'checkbox',
  11. }
  12. )

新代码:

  1. function mountElement(vnode, container, isSVG) {
  2. const el = document.createElement(vnode.tag)
  3. // 问题4 处理SVG 之所以增加输入isSVG是因为所有<svg>的子标签(如<circle>都应该是svg)
  4. isSVG = isSVG || vnode.flags & VNodeFlags.ELEMENT_SVG
  5. const el = isSVG
  6. ? document.createElementNS('http://www.w3.org/2000/svg', vnode.tag)
  7. : document.createElement(vnode.tag)
  8. // 问题1 将真是DOM绑定到VNode上,用来索引
  9. vnode.el = el
  10. // 问题2
  11. const data = vnode.data
  12. if (data) {
  13. // 如果 VNodeData 存在,则遍历之
  14. for(let key in data) {
  15. // key 可能是 class、style、on 等等
  16. switch(key) {
  17. case 'style':
  18. // 如果 key 的值是 style,说明是内联样式,逐个将样式规则应用到 el
  19. for(let k in data.style) {
  20. el.style[k] = data.style[k]
  21. }
  22. break
  23. case 'class':
  24. el.className = data[key]
  25. // 判断是否为DOM属性
  26. default:
  27. if (key[0] === 'o' && key[1] === 'n') {
  28. // 事件
  29. el.addEventListener(key.slice(2), data[key])
  30. } else if (domPropsRE.test(key)) {
  31. // 当作 DOM Prop 处理
  32. el[key] = data[key]
  33. } else {
  34. // 当作 Attr 处理
  35. el.setAttribute(key, data[key])
  36. }
  37. break
  38. }
  39. }
  40. }
  41. // 问题3 拿到 children 和 childFlags
  42. const childFlags = vnode.childFlags
  43. const children = vnode.children
  44. // 检测如果没有子节点则无需递归挂载
  45. if (childFlags !== ChildrenFlags.NO_CHILDREN) {
  46. if (childFlags & ChildrenFlags.SINGLE_VNODE) {
  47. // 如果是单个子节点则调用 mount 函数挂载
  48. mount(children, el)
  49. } else if (childFlags & ChildrenFlags.MULTIPLE_VNODES) {
  50. // 如果是单多个子节点则遍历并调用 mount 函数挂载
  51. for (let i = 0; i < children.length; i++) {
  52. mount(child, el)
  53. }
  54. }
  55. }
  56. container.appendChild(el)
  57. }

应用层面的设计

如果某些class会动态变化,那么VNode的data结构和render的方法都应该发生相应改变。

  1. <template>
  2. <div class="cls-a" :class="dynamicClass"></div>
  3. </template>
  1. // 动态class可以是数组,也可以是对象
  2. dynamicClass = ['class-b', 'class-c']
  3. dynamicClass = {
  4. 'class-b': true,
  5. 'class-c': true
  6. }
  7. // VNode h函数
  8. h('div', {
  9. class: ['class-a', dynamicClass]
  10. })
  11. // 对象对应VNode输出
  12. h('div', {
  13. class: [
  14. 'class-a',
  15. {
  16. 'class-b': true,
  17. 'class-c': true
  18. }
  19. ]
  20. })

Attributes 和 DOM Properties

attr: 每个标签中都可能包含一些属性,如果这些属性是标准属性,那么解析生成的DOM对象中也会包含与之对应的属性,例如:

  1. <body id="page"></body>

可以通过 document.body.id 来访问它的值,这些属性可以认为是DOM对象的标准属性。

prop: 非标准属性,通过document.body.custom访问,会得到undefined

<body custom="val"></body>

但是可以通过setAttribute 设置属性为标准属性。但是设置的属性值会被先转为字符串,再传给属性值:

const checkboxEl = document.querySelector('input')

checkboxEl.setAttribute('checked', false)
// 等价于
checkboxEl.setAttribute('checked', 'false')
console.log(checkboxEl.checked) // true

事件的处理

在 mount 阶段为 DOM 元素添加事件很容易,我们只需要在元素对象上调用 addEventListener 方法即可

<div @click="handler"></div>
// 在事件的属性前增加on 与其他属性进行区分
const elementVNode = h('div', {
  onclick: handler
})

渲染器的patch

何为patch

渲染器需要对新旧 VNode 进行比对,并以合适的方式更新DOM,也就是我们常说的 patch。

VNode的对比

function patch(prevVNode, nextVNode, container) {
  // 分别拿到新旧 VNode 的类型,即 flags
  const nextFlags = nextVNode.flags
  const prevFlags = prevVNode.flags

  // 检查新旧 VNode 的类型是否相同,如果类型不同,则直接调用 replaceVNode 函数替换 VNode
  // 如果新旧 VNode 的类型相同,则根据不同的类型调用不同的比对函数
  if (prevFlags !== nextFlags) {
    replaceVNode(prevVNode, nextVNode, container)
  // &是由于flag设计时用的是1<<k的循环移位方式设计,同时有的flag是多种flag的或值,因此用&可以判断是否包含
  } else if (nextFlags & VNodeFlags.ELEMENT) {
    patchElement(prevVNode, nextVNode, container)
  } else if (nextFlags & VNodeFlags.COMPONENT) {
    patchComponent(prevVNode, nextVNode, container)
  } else if (nextFlags & VNodeFlags.TEXT) {
    patchText(prevVNode, nextVNode)
  } else if (nextFlags & VNodeFlags.FRAGMENT) {
    patchFragment(prevVNode, nextVNode, container)
  } else if (nextFlags & VNodeFlags.PORTAL) {
    patchPortal(prevVNode, nextVNode)
  }
}

image.png

替换VNode

新旧节点类型不同时,先删除旧节点,再挂载新节点

function replaceVNode(prevVNode, nextVNode, container) {
  // 将旧的 VNode 所渲染的 DOM 从容器中移除
  container.removeChild(prevVNode.el)
  // 再把新的 VNode 挂载到容器中
  mount(nextVNode, container)
}

patch基础标签

新旧VNode

// 旧的 VNode
const prevVNode = h('div', {
  style: {
    width: '100px',
    height: '100px',
    backgroundColor: 'red'
  }
})

// 新的 VNode
const nextVNode = h('div', {
  style: {
    width: '100px',
    height: '100px',
    border: '1px solid green'
  }
})

仅针对这个案例而言,我们的更新规则应该是:先将红色背景从元素上移除,再为元素添加绿色边框。此即:将新的 VNodeData 全部应用到元素上,再把那些已经不存在于新的 VNodeData 上的数据从元素上移除

function patchElement(prevVNode, nextVNode, container) {
  // 如果新旧 VNode 描述的是不同的标签,则调用 replaceVNode 函数,使用新的 VNode 替换旧的 VNode
  if (prevVNode.tag !== nextVNode.tag) {
    replaceVNode(prevVNode, nextVNode, container)
    return
  }

  // 拿到 el 元素,注意这时要让 nextVNode.el 也引用该元素
  const el = (nextVNode.el = prevVNode.el)
  // 拿到 新旧 VNodeData
  const prevData = prevVNode.data
  const nextData = nextVNode.data
  // 新的 VNodeData 存在时才有必要更新
  if (nextData) {
    // 遍历新的 VNodeData
    for (let key in nextData) {
      // 根据 key 拿到新旧 VNodeData 值
      const prevValue = prevData[key]
      const nextValue = nextData[key]
      switch (key) {
        case 'style':
          // 遍历新 VNodeData 中的 style 数据,将新的样式应用到元素
          for (let k in nextValue) {
            el.style[k] = nextValue[k]
          }
          // 遍历旧 VNodeData 中的 style 数据,将已经不存在于新的 VNodeData 的数据移除
          for (let k in prevValue) {
            if (!nextValue.hasOwnProperty(k)) {
              el.style[k] = ''
            }
          }
          break
        default:
          break
      }
    }
  }
}

如上高亮代码所示,我们在更新 VNodeData 时的思路分为以下几步:
第 1 步:当新的 VNodeData 存在时,遍历新的 VNodeData。
第 2 步:根据新 VNodeData 中的 key,分别尝试读取旧值和新值,即 prevValue 和 nextValue。
第 3 步:使用 switch…case 语句匹配不同的数据进行不同的更新操作
以样式(style)的更新为例,如上代码所展示的更新过程是:
1 :遍历新的样式数据(prevValue),将新的样式数据全部应用到元素上
2 :遍历旧的样式数据(nextValue),将那些已经不存在于新的样式数据中的样式从元素上移除,最终我们完成了元素样式的更新。
这个过程实际上就是更新标签元素的基本规则。

更新VNodeData

和mount渲染data的方式类似,知识变为了patch

function patchElement(prevVNode, nextVNode, container) {
  // 如果新旧 VNode 描述的是不同的标签,则调用 replaceVNode 函数,使用新的 VNode 替换旧的 VNode
  if (prevVNode.tag !== nextVNode.tag) {
    replaceVNode(prevVNode, nextVNode, container)
    return
  }

  // 拿到 el 元素,注意这时要让 nextVNode.el 也引用该元素
  const el = (nextVNode.el = prevVNode.el)
  const prevData = prevVNode.data
  const nextData = nextVNode.data

  if (nextData) {
    // 遍历新的 VNodeData,将旧值和新值都传递给 patchData 函数
    for (let key in nextData) {
      const prevValue = prevData[key]
      const nextValue = nextData[key]
      patchData(el, key, prevValue, nextValue)
    }
  }
  if (prevData) {
    // 遍历旧的 VNodeData,将已经不存在于新的 VNodeData 中的数据移除
    for (let key in prevData) {
      const prevValue = prevData[key]
      if (prevValue && !nextData.hasOwnProperty(key)) {
        // 第四个参数为 null,代表移除数据
        patchData(el, key, prevValue, null)
      }
    }
  }
}

export function patchData(el, key, prevValue, nextValue) {
  switch (key) {
    case 'style':
      // 省略处理样式的代码...
    case 'class':
      // 省略处理 class 的代码...
    default:
      if (key[0] === 'o' && key[1] === 'n') {
        // 事件
        // 移除旧事件
        if (prevValue) {
          el.removeEventListener(key.slice(2), prevValue)
        }
        // 添加新事件
        if (nextValue) {
          el.addEventListener(key.slice(2), nextValue)
        }
      } else if (domPropsRE.test(key)) {
        // 当作 DOM Prop 处理
        el[key] = nextValue
      } else {
        // 当作 Attr 处理
        el.setAttribute(key, nextValue)
      }
      break
  }
}

更新子节点

function patchElement(prevVNode, nextVNode, container) {
  // 如果新旧 VNode 描述的是不同的标签,则调用 replaceVNode 函数,使用新的 VNode 替换旧的 VNode
  if (prevVNode.tag !== nextVNode.tag) {
    replaceVNode(prevVNode, nextVNode, container)
    return
  }

  // 拿到 el 元素,注意这时要让 nextVNode.el 也引用该元素
    // ...处理data变化

  // 调用 patchChildren 函数递归地更新子节点
  patchChildren(
    prevVNode.childFlags, // 旧的 VNode 子节点的类型
    nextVNode.childFlags, // 新的 VNode 子节点的类型
    prevVNode.children,   // 旧的 VNode 子节点
    nextVNode.children,   // 新的 VNode 子节点
    el                    // 当前标签元素,即这些子节点的父节点
  )
}
function patchChildren(
  prevChildFlags,
  nextChildFlags,
  prevChildren,
  nextChildren,
  container
) {
  switch (prevChildFlags) {
    // 旧的 children 是单个子节点,会执行该 case 语句块
    case ChildrenFlags.SINGLE_VNODE:
      switch (nextChildFlags) {
        case ChildrenFlags.SINGLE_VNODE:
          // 此时 prevChildren 和 nextChildren 都是 VNode 对象 直接更新
          patch(prevChildren, nextChildren, container)
          break
        case ChildrenFlags.NO_CHILDREN:
          // 新节点没有孩子 删除原节点孩子
          container.removeChild(prevChildren.el)
          break
        default:
          // 新的 children 中有多个子节点时,会执行该 case 语句块
          // 移除旧的单个子节点
          container.removeChild(prevChildren.el)
          // 遍历新的多个子节点,逐个挂载到容器中
          for (let i = 0; i < nextChildren.length; i++) {
            mount(nextChildren[i], container)
          }
          break
      }
      break
    // 旧的 children 中没有子节点时,会执行该 case 语句块
    case ChildrenFlags.NO_CHILDREN:
      switch (nextChildFlags) {
        case ChildrenFlags.SINGLE_VNODE:
          // 新的 children 是单个子节点时,会执行该 case 语句块
          break
        case ChildrenFlags.NO_CHILDREN:
          // 新的 children 中没有子节点时,会执行该 case 语句块
          break
        default:
          // 新的 children 中有多个子节点时,会执行该 case 语句块
          break
      }
      break
    // 旧的 children 中有多个子节点时,会执行该 case 语句块
    default:
      switch (nextChildFlags) {
        case ChildrenFlags.SINGLE_VNODE:
          for (let i = 0; i < prevChildren.length; i++) {
            container.removeChild(prevChildren[i].el)
          }
          mount(nextChildren, container)
          break
        case ChildrenFlags.NO_CHILDREN:
          for (let i = 0; i < prevChildren.length; i++) {
            container.removeChild(prevChildren[i].el)
          }
          break
        default:
          // 遍历旧的子节点,将其全部移除
          for (let i = 0; i < prevChildren.length; i++) {
            container.removeChild(prevChildren[i].el)
          }
          // 遍历新的子节点,将其全部添加
          for (let i = 0; i < nextChildren.length; i++) {
            mount(nextChildren[i], container)
          }
          break
      }
      break
  }
}

image.png

image.png

有状态组件的更新

有状态组件来说它的更新方式有两种:主动更新被动更新
主动更新: 指的是组件自身的状态发生变化所导致的更新,例如组件的 data 数据发生了变化就必然需要重渲染;
被动更新:对于子组件来讲,它除了自身状态之外,很可能还包含从父组件传递进来的外部状态(props),所以父组件自身状态的变化很可能引起子组件外部状态的变化,此时就需要更新子组件,像这种因为外部状态变化而导致的组件更新就叫做被动更新。

主动更新

function mountStatefulComponent(vnode, container, isSVG) {
  // 创建组件实例
  const instance = new vnode.tag()

  instance._update = function() {
    // 如果 instance._mounted 为真,说明组件已挂载,应该执行更新操作
    if (instance._mounted) {
      // 1、拿到旧的 VNode
      const prevVNode = instance.$vnode
      // 2、重渲染新的 VNode
      const nextVNode = (instance.$vnode = instance.render())
      // 3、patch 更新
      patch(prevVNode, nextVNode, prevVNode.el.parentNode)
      // 4、更新 vnode.el 和 $el
      instance.$el = vnode.el = instance.$vnode.el
    } else {
      // 1、渲染VNode
      instance.$vnode = instance.render()
      // 2、挂载
      mount(instance.$vnode, container, isSVG)
      // 3、组件已挂载的标识
      instance._mounted = true
      // 4、el 属性值 和 组件实例的 $el 属性都引用组件的根DOM元素
      instance.$el = vnode.el = instance.$vnode.el
      // 5、调用 mounted 钩子
      instance.mounted && instance.mounted()
    }
  }

   // mounted 钩子
  mounted() {
    // 两秒钟之后修改本地状态的值,并重新调用 _update() 函数更新组件
    setTimeout(() => {
      this.localState = 'two'
      this._update()
    }, 2000)
  }

  instance._update()
}

组件必定会走_update方法,无论是挂载还是更新,但是_update方法会区分挂载与更新。

被动更新

增加$prop属性,内部保存父组件对应的data,当父组件数据变化时,$prop相应变化,此时子组件的mounted钩子会根据$prop的数据改变,进而改变VNode

渲染器的核心 Diff 算法

只有当新旧子节点的类型都是多个子节点时,核心 Diff 算法才派得上用场。

演化:

示例: 排序列表的重排

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>
// origin 1, 2, 3
[
  h('li', null, 1),
  h('li', null, 2),
  h('li', null, 3)
]
// current 3, 1, 2
[
  h('li', null, 3),
  h('li', null, 1),
  h('li', null, 2)
]

简单 Diff 算法:遍历旧的子节点,将其全部移除;再遍历新的子节点,将其全部添加
image.png

当子节点等长,每个对应节点patch
image.png
长度不同,移除长的节点(尽量能修改就修改)
image.png
最佳期望: 移动元素位置,而非更新节点信息
新旧 children 中的节点都是 li 标签,以新 children 的第一个 li 标签为例,你能说出在旧 children 中哪一个 li 标签可被它复用吗?不能,所以,为了明确的知道新旧 children 中节点的映射关系,我们需要在 VNode 创建伊始就为其添加唯一的标识,即 key 属性
image.png

// 旧 children
[
  h('li', { key: 'a' }, 1),
  h('li', { key: 'b' }, 2),
  h('li', { key: 'c' }, 3)
]

// 新 children
[
  h('li', { key: 'c' }, 3)
  h('li', { key: 'a' }, 1),
  h('li', { key: 'b' }, 2)
]

image.png
**
知道了映射关系,我们就很容易判断新 children 中的节点是否可被复用:只需要遍历新 children 中的每一个节点,并去旧 children 中寻找是否存在具有相同 key 值的节点。

// 遍历新的 children
for (let i = 0; i < nextChildren.length; i++) {
  const nextVNode = nextChildren[i]
  let j = 0
  // 遍历旧的 children
  for (j; j < prevChildren.length; j++) {
    const prevVNode = prevChildren[j]
    // 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
    if (nextVNode.key === prevVNode.key) {
      patch(prevVNode, nextVNode, container)
      break // 这里需要 break
    }
  }
}

如何移动:

// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0
// 遍历新的 children
for (let i = 0; i < nextChildren.length; i++) {
  const nextVNode = nextChildren[i]
  let j = 0
  // 遍历旧的 children
  for (j; j < prevChildren.length; j++) {
    const prevVNode = prevChildren[j]
    // 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
    if (nextVNode.key === prevVNode.key) {
      patch(prevVNode, nextVNode, container)
      if (j < lastIndex) {
        // 需要移动
        // refNode 是为了下面调用 insertBefore 函数准备的
        const refNode = nextChildren[i - 1].el.nextSibling
        // 调用 insertBefore 函数移动 DOM
        container.insertBefore(prevVNode.el, refNode)
      } else {
        // 更新 lastIndex
        lastIndex = j
      }
      break // 这里需要 break
    }
  }
}

image.png
举例: 0 1 2 3 4 5 => 3 2 1 5 4
过程: 3(key) 的VNode被更新,绑定原先3的DOM,作为新的基准点;然后1,2VNode分别被更新,其对应的DOM变为3的DOM的右兄弟DOM。然后5被更新,再来一次。
总结:最大值永远总为基准点(不用动),然后小的值落在其右边,无关于顺序(key只是身份的标识)

移动中的新元素
image.png
应该使用 mount 函数将 li-d 节点作为全新的 VNode 挂载到合适的位置
第一,在找节点的算法中添加boolean变量find标识有无找到;
第二,如果没找到,mount新节点,让后插入到合适位置;
第三,如果新节点是第一个节点,应该mount时append到element中.

移除不存在的元素
image.png
在循环结束后,对prev children进行循环,查看有无删除,如果删除了将真实DOM移除。

双端比较

上述演化算法(React)的优化,思想在于原算法在尾部变为头部后会一直移动元素,
image.png
但实际只应把尾部移至新头部即可,减少很多操作。
思想:双端比较,然后递归实现