Virtual DOM 及 Diff 算法

Virtual DOM 及 Diff 算法

1. JSX 到底是什么

使用 React 就一定會(huì)寫 JSX,JSX 到底是什么呢?它是一種 JavaScript 語(yǔ)法的擴(kuò)展,React 使用它來(lái)描述用戶界面長(zhǎng)成什么樣子。雖然它看起來(lái)非常像 HTML,但它確實(shí)是 JavaScript 。在 React 代碼執(zhí)行之前,Babel 會(huì)對(duì)將 JSX 編譯為 React API.

<div className="container">
  <h3>Hello React</h3>
  <p>React is great </p>
</div>
React.createElement(
  "div",
  {
    className: "container"
  },
  React.createElement("h3", null, "Hello React"),
  React.createElement("p", null, "React is great")
);

從兩種語(yǔ)法對(duì)比來(lái)看,JSX 語(yǔ)法的出現(xiàn)是為了讓 React 開(kāi)發(fā)人員編寫用戶界面代碼更加輕松。

Babel REPL

2. DOM 操作問(wèn)題

在現(xiàn)代 web 應(yīng)用程序中使用 JavaScript 操作 DOM 是必不可少的,但遺憾的是它比其他大多數(shù) JavaScript 操作要慢的多。

大多數(shù) JavaScript 框架對(duì)于 DOM 的更新遠(yuǎn)遠(yuǎn)超過(guò)其必須進(jìn)行的更新,從而使得這種緩慢操作變得更糟。

例如假設(shè)你有包含十個(gè)項(xiàng)目的列表,你僅僅更改了列表中的第一項(xiàng),大多數(shù) JavaScript 框架會(huì)重建整個(gè)列表,這比必要的工作要多十倍。

更新效率低下已經(jīng)成為嚴(yán)重問(wèn)題,為了解決這個(gè)問(wèn)題,React 普及了一種叫做 Virtual DOM 的東西,Virtual DOM 出現(xiàn)的目的就是為了提高 JavaScript 操作 DOM 對(duì)象的效率。

3. 什么是 Virtual DOM

在 React 中,每個(gè) DOM 對(duì)象都有一個(gè)對(duì)應(yīng)的 Virtual DOM 對(duì)象,它是 DOM 對(duì)象的 JavaScript 對(duì)象表現(xiàn)形式,其實(shí)就是使用 JavaScript 對(duì)象來(lái)描述 DOM 對(duì)象信息,比如 DOM 對(duì)象的類型是什么,它身上有哪些屬性,它擁有哪些子元素。

可以把 Virtual DOM 對(duì)象理解為 DOM 對(duì)象的副本,但是它不能直接顯示在屏幕上。

<div className="container">
  <h3>Hello React</h3>
  <p>React is great </p>
</div>
{
  type: "div",
  props: { className: "container" },
  children: [
    {
      type: "h3",
      props: null,
      children: [
        {
          type: "text",
          props: {
            textContent: "Hello React"
          }
        }
      ]
    },
    {
      type: "p",
      props: null,
      children: [
        {
          type: "text",
          props: {
            textContent: "React is great"
          }
        }
      ]
    }
  ]
}

4. Virtual DOM 如何提升效率

精準(zhǔn)找出發(fā)生變化的 DOM 對(duì)象,只更新發(fā)生變化的部分。

在 React 第一次創(chuàng)建 DOM 對(duì)象后,會(huì)為每個(gè) DOM 對(duì)象創(chuàng)建其對(duì)應(yīng)的 Virtual DOM 對(duì)象,在 DOM 對(duì)象發(fā)生更新之前,React 會(huì)先更新所有的 Virtual DOM 對(duì)象,然后 React 會(huì)將更新后的 Virtual DOM 和 更新前的 Virtual DOM 進(jìn)行比較,從而找出發(fā)生變化的部分,React 會(huì)將發(fā)生變化的部分更新到真實(shí)的 DOM 對(duì)象中,React 僅更新必要更新的部分。

Virtual DOM 對(duì)象的更新和比較僅發(fā)生在內(nèi)存中,不會(huì)在視圖中渲染任何內(nèi)容,所以這一部分的性能損耗成本是微不足道的。

<img src="./images/1.png" style="margin: 20px 0;width: 80%"/>

<div id="container">
    <p>Hello React</p>
</div>
<div id="container">
    <p>Hello Angular</p>
</div>
const before = {
  type: "div",
  props: { id: "container" },
  children: [
    {
      type: "p",
      props: null,
      children: [
        { type: "text", props: { textContent: "Hello React" } }
      ]
    }
  ]
}
const after = {
  type: "div",
  props: { id: "container" },
  children: [
    {
      type: "p",
      props: null,
      children: [
        { type: "text", props: { textContent: "Hello Angular" } }
      ]
    }
  ]
}

5. 創(chuàng)建 Virtual DOM

在 React 代碼執(zhí)行前,JSX 會(huì)被 Babel 轉(zhuǎn)換為 React.createElement 方法的調(diào)用,在調(diào)用 createElement 方法時(shí)會(huì)傳入元素的類型,元素的屬性,以及元素的子元素,createElement 方法的返回值為構(gòu)建好的 Virtual DOM 對(duì)象。

{
  type: "div",
  props: null,
  children: [{type: "text", props: {textContent: "Hello"}}]
}
/**
 * 創(chuàng)建 Virtual DOM
 * @param {string} type 類型
 * @param {object | null} props 屬性
 * @param  {createElement[]} children 子元素
 * @return {object} Virtual DOM
 */
function createElement (type, props, ...children) {
    return {
    type,
    props,
    children
  } 
}

從 createElement 方法的第三個(gè)參數(shù)開(kāi)始就都是子元素了,在定義 createElement 方法時(shí),通過(guò) ...children 將所有的子元素放置到 children 數(shù)組中。

const virtualDOM = (
  <div className="container">
    <h1>你好 Tiny React</h1>
    <h2>(編碼必殺技)</h2>
    <div>
      嵌套1 <div>嵌套 1.1</div>
    </div>
    <h3>(觀察: 這個(gè)將會(huì)被改變)</h3>
    {2 == 1 && <div>如果2和1相等渲染當(dāng)前內(nèi)容</div>}
    {2 == 2 && <div>2</div>}
    <span>這是一段內(nèi)容</span>
    <button onClick={() => alert("你好")}>點(diǎn)擊我</button>
    <h3>這個(gè)將會(huì)被刪除</h3>
    2, 3
  </div>
)
console.log(virtualDOM)

通過(guò)以上代碼測(cè)試,發(fā)現(xiàn)返回的 Virtual DOM 存在一些問(wèn)題,第一個(gè)問(wèn)題是文本節(jié)點(diǎn)被直接放入到了數(shù)組中

<img src="./images/2.png" width="50%"/>

而我們期望是文本節(jié)點(diǎn)應(yīng)該是這樣的

children: [
  {
    type: "text",
    props: {
      textContent: "React is great"
    }
  }
]

通過(guò)以下代碼對(duì) Virtual DOM 進(jìn)行改造,重新構(gòu)建 Virtual DOM。

// 將原有 children 拷貝一份 不要在原有數(shù)組上進(jìn)行操作
const childElements = [].concat(...children).map(child => {
  // 判斷 child 是否是對(duì)象類型
  if (child instanceof Object) {
    // 如果是 什么都不需要做 直接返回即可
    return child
  } else {
    // 如果不是對(duì)象就是文本 手動(dòng)調(diào)用 createElement 方法將文本轉(zhuǎn)換為 Virtual DOM
    return createElement("text", { textContent: child })
  }
})
return {
  type,
  props,
  children: childElements
}

<img src="./images/3.png" width="50%"/>

通過(guò)觀察返回的 Virtual DOM,文本節(jié)點(diǎn)已經(jīng)被轉(zhuǎn)化成了對(duì)象類型的 Virtual DOM,但是布爾值也被當(dāng)做文本節(jié)點(diǎn)被轉(zhuǎn)化了,在 JSX 中,如果 Virtual DOM 被轉(zhuǎn)化為了布爾值或者null,是不應(yīng)該被更新到真實(shí) DOM 中的,所以接下來(lái)要做的事情就是清除 Virtual DOM 中的布爾值和null。

// 由于 map 方法無(wú)法從數(shù)據(jù)中刨除元素, 所以此處將 map 方法更改為 reduce 方法
const childElements = [].concat(...children).reduce((result, child) => {
  // 判斷子元素類型 刨除 null true false
  if (child != null && child != false && child != true) {
    if (child instanceof Object) {
      result.push(child)
    } else {
      result.push(createElement("text", { textContent: child }))
    }
  }
  // 將需要保留的 Virtual DOM 放入 result 數(shù)組
  return result
}, [])

在 React 組件中,可以通過(guò) props.children 獲取子元素,所以還需要將子元素存儲(chǔ)在 props 對(duì)象中。

return {
  type,
  props: Object.assign({ children: childElements }, props),
  children: childElements
}

6. 渲染 Virtual DOM 對(duì)象為 DOM 對(duì)象

通過(guò)調(diào)用 render 方法可以將 Virtual DOM 對(duì)象更新為真實(shí) DOM 對(duì)象。

在更新之前需要確定是否存在舊的 Virtual DOM,如果存在需要比對(duì)差異,如果不存在可以直接將 Virtual DOM 轉(zhuǎn)換為 DOM 對(duì)象。

目前先只考慮不存在舊的 Virtual DOM 的情況,就是說(shuō)先直接將 Virtual DOM 對(duì)象更新為真實(shí) DOM 對(duì)象。

// render.js
export default function render(virtualDOM, container, oldDOM = container.firstChild) {
  // 在 diff 方法內(nèi)部判斷是否需要對(duì)比 對(duì)比也好 不對(duì)比也好 都在 diff 方法中進(jìn)行操作  
  diff(virtualDOM, container, oldDOM)
}
// diff.js
import mountElement from "./mountElement"

export default function diff(virtualDOM, container, oldDOM) {
  // 判斷 oldDOM 是否存在
  if (!oldDOM) {
    // 如果不存在 不需要對(duì)比 直接將 Virtual DOM 轉(zhuǎn)換為真實(shí) DOM
    mountElement(virtualDOM, container)
  }
}

在進(jìn)行 virtual DOM 轉(zhuǎn)換之前還需要確定 Virtual DOM 的類 Component VS Native Element。

類型不同需要做不同的處理 如果是 Native Element 直接轉(zhuǎn)換。

如果是組件 還需要得到組件實(shí)例對(duì)象 通過(guò)組件實(shí)例對(duì)象獲取組件返回的 virtual DOM 然后再進(jìn)行轉(zhuǎn)換。

目前先只考慮 Native Element 的情況。

// mountElement.js
import mountNativeElement from "./mountNativeElement"

export default function mountElement(virtualDOM, container) {
  // 通過(guò)調(diào)用 mountNativeElement 方法轉(zhuǎn)換 Native Element
  mountNativeElement(virtualDOM, container)
}
// mountNativeElement.js
import createDOMElement from "./createDOMElement"

export default function mountNativeElement(virtualDOM, container) {
  const newElement = createDOMElement(virtualDOM)
  container.appendChild(newElement)
}
// createDOMElement.js
import mountElement from "./mountElement"
import updateElementNode from "./updateElementNode"

export default function createDOMElement(virtualDOM) {
  let newElement = null
  if (virtualDOM.type === "text") {
    // 創(chuàng)建文本節(jié)點(diǎn)
    newElement = document.createTextNode(virtualDOM.props.textContent)
  } else {
    // 創(chuàng)建元素節(jié)點(diǎn)
    newElement = document.createElement(virtualDOM.type)
    // 更新元素屬性
    updateElementNode(newElement, virtualDOM)
  }
  // 遞歸渲染子節(jié)點(diǎn)
  virtualDOM.children.forEach(child => {
    // 因?yàn)椴淮_定子元素是 NativeElement 還是 Component 所以調(diào)用 mountElement 方法進(jìn)行確定
    mountElement(child, newElement)
  })
  return newElement
}

7. 為元素節(jié)點(diǎn)添加屬性

// createDOMElement.js
// 看看節(jié)點(diǎn)類型是文本類型還是元素類型
if (virtualDOM.type === "text") {
  // 創(chuàng)建文本節(jié)點(diǎn) 設(shè)置節(jié)點(diǎn)內(nèi)容
  newElement = document.createTextNode(virtualDOM.props.textContent)
} else {
  // 根據(jù) Virtual DOM type 屬性值創(chuàng)建 DOM 元素
  newElement = document.createElement(virtualDOM.type)
  // 為元素設(shè)置屬性
  updateElementNode(newElement, virtualDOM)
}
export default function updateElementNode(element, virtualDOM) {
  // 獲取要解析的 VirtualDOM 對(duì)象中的屬性對(duì)象
  const newProps = virtualDOM.props
  // 將屬性對(duì)象中的屬性名稱放到一個(gè)數(shù)組中并循環(huán)數(shù)組
  Object.keys(newProps).forEach(propName => {
    const newPropsValue = newProps[propName]
    // 考慮屬性名稱是否以 on 開(kāi)頭 如果是就表示是個(gè)事件屬性 onClick -> click
    if (propName.slice(0, 2) === "on") {
      const eventName = propName.toLowerCase().slice(2)
      element.addEventListener(eventName, newPropsValue)
      // 如果屬性名稱是 value 或者 checked 需要通過(guò) [] 的形式添加
    } else if (propName === "value" || propName === "checked") {
      element[propName] = newPropsValue
      // 刨除 children 因?yàn)樗亲釉?不是屬性
    } else if (propName !== "children") {
      // className 屬性單獨(dú)處理 不直接在元素上添加 class 屬性是因?yàn)?class 是 JavaScript 中的關(guān)鍵字
      if (propName === "className") {
        element.setAttribute("class", newPropsValue)
      } else {
        // 普通屬性
        element.setAttribute(propName, newPropsValue)
      }
    }
  })
}

8. 渲染組件

8.1 函數(shù)組件

在渲染組件之前首先要明確的是,組件的 Virtual DOM 類型值為函數(shù),函數(shù)組件和類組件都是這樣的。

// 原始組件
const Heart = () => <span>&hearts;</span>
<Heart />
// 組件的 Virtual DOM
{
  type: f function() {},
  props: {}
  children: []
}

在渲染組件時(shí),要先將 Component 與 Native Element 區(qū)分開(kāi),如果是 Native Element 可以直接開(kāi)始渲染,如果是組件,特別處理。

// mountElement.js
export default function mountElement(virtualDOM, container) {
  // 無(wú)論是類組件還是函數(shù)組件 其實(shí)本質(zhì)上都是函數(shù) 
  // 如果 Virtual DOM 的 type 屬性值為函數(shù) 就說(shuō)明當(dāng)前這個(gè) Virtual DOM 為組件
  if (isFunction(virtualDOM)) {
    // 如果是組件 調(diào)用 mountComponent 方法進(jìn)行組件渲染
    mountComponent(virtualDOM, container)
  } else {
    mountNativeElement(virtualDOM, container)
  }
}

// Virtual DOM 是否為函數(shù)類型
export function isFunction(virtualDOM) {
  return virtualDOM && typeof virtualDOM.type === "function"
}

在 mountComponent 方法中再進(jìn)行函數(shù)組件和類型的區(qū)分,然后再分別進(jìn)行處理。

// mountComponent.js
import mountNativeElement from "./mountNativeElement"

export default function mountComponent(virtualDOM, container) {
  // 存放組件調(diào)用后返回的 Virtual DOM 的容器
  let nextVirtualDOM = null
  // 區(qū)分函數(shù)型組件和類組件
  if (isFunctionalComponent(virtualDOM)) {
    // 函數(shù)組件 調(diào)用 buildFunctionalComponent 方法處理函數(shù)組件
    nextVirtualDOM = buildFunctionalComponent(virtualDOM)
  } else {
    // 類組件
  }
  // 判斷得到的 Virtual Dom 是否是組件
  if (isFunction(nextVirtualDOM)) {
    // 如果是組件 繼續(xù)調(diào)用 mountComponent 解剖組件
    mountComponent(nextVirtualDOM, container)
  } else {
    // 如果是 Navtive Element 就去渲染
    mountNativeElement(nextVirtualDOM, container)
  }
}

// Virtual DOM 是否為函數(shù)型組件
// 條件有兩個(gè): 1. Virtual DOM 的 type 屬性值為函數(shù) 2. 函數(shù)的原型對(duì)象中不能有render方法
// 只有類組件的原型對(duì)象中有render方法 
export function isFunctionalComponent(virtualDOM) {
  const type = virtualDOM && virtualDOM.type
  return (
    type && isFunction(virtualDOM) && !(type.prototype && type.prototype.render)
  )
}

// 函數(shù)組件處理 
function buildFunctionalComponent(virtualDOM) {
  // 通過(guò) Virtual DOM 中的 type 屬性獲取到組件函數(shù)并調(diào)用
  // 調(diào)用組件函數(shù)時(shí)將 Virtual DOM 對(duì)象中的 props 屬性傳遞給組件函數(shù) 這樣在組件中就可以通過(guò) props 屬性獲取數(shù)據(jù)了
  // 組件返回要渲染的 Virtual DOM
  return virtualDOM && virtualDOM.type(virtualDOM.props || {})
}

8.2 類組件

類組件本身也是 Virtual DOM,可以通過(guò) Virtual DOM 中的 type 屬性值確定當(dāng)前要渲染的組件是類組件還是函數(shù)組件。

在確定當(dāng)前要渲染的組件為類組件以后,需要實(shí)例化類組件得到類組件實(shí)例對(duì)象,通過(guò)類組件實(shí)例對(duì)象調(diào)用類組件中的 render 方法,獲取組件要渲染的 Virtual DOM。

類組件需要繼承 Component 父類,子類需要通過(guò) super 方法將自身的 props 屬性傳遞給 Component 父類,父類會(huì)將 props 屬性掛載為父類屬性,子類繼承了父類,自己本身也就自然擁有props屬性了。這樣做的好處是當(dāng) props 發(fā)生更新后,父類可以根據(jù)更新后的 props 幫助子類更新視圖。

假設(shè)以下代碼就是我們要渲染的類組件:

class Alert extends TinyReact.Component {
  constructor(props) {
    // 將 props 傳遞給父類 子類繼承父類的 props 子類自然就有 props 數(shù)據(jù)了
    // 否則 props 僅僅是 constructor 函數(shù)的參數(shù)而已
    // 將 props 傳遞給父類的好處是 當(dāng) props 發(fā)生更改時(shí) 父類可以幫助更新 props 更新組件視圖
    super(props)
    this.state = {
      title: "default title"
    }
  }
  render() {
    return (
      <div>
        <h2>{this.state.title}</h2>
        <p>{this.props.message}</p>
      </div>
    )
  }
}

TinyReact.render(<Alert message="Hello React" />, root)
// Component.js 父類 Component 實(shí)現(xiàn)
export default class Component {
  constructor(props) {
    this.props = props
  }
}

在 mountComponent 方法中通過(guò)調(diào)用 buildStatefulComponent 方法得到類組件要渲染的 Virtual DOM

// mountComponent.js
export default function mountComponent(virtualDOM, container) {
  let nextVirtualDOM = null
  // 區(qū)分函數(shù)型組件和類組件
  if (isFunctionalComponent(virtualDOM)) {
    // 函數(shù)組件
    nextVirtualDOM = buildFunctionalComponent(virtualDOM)
  } else {
    // 類組件
    nextVirtualDOM = buildStatefulComponent(virtualDOM)
  }
  // 判斷得到的 Virtual Dom 是否是組件
  if (isFunction(nextVirtualDOM)) {
    mountComponent(nextVirtualDOM, container)
  } else {
    mountNativeElement(nextVirtualDOM, container)
  }
}

// 處理類組件
function buildStatefulComponent(virtualDOM) {
  // 實(shí)例化類組件 得到類組件實(shí)例對(duì)象 并將 props 屬性傳遞進(jìn)類組件
  const component = new virtualDOM.type(virtualDOM.props)
  // 調(diào)用類組件中的render方法得到要渲染的 Virtual DOM
  const nextVirtualDOM = component.render()
  // 返回要渲染的 Virtual DOM
  return nextVirtualDOM
}

9. Virtual DOM 比對(duì)

在進(jìn)行 Virtual DOM 比對(duì)時(shí),需要用到更新后的 Virtual DOM 和更新前的 Virtual DOM,更新后的 Virtual DOM 目前我們可以通過(guò) render 方法進(jìn)行傳遞,現(xiàn)在的問(wèn)題是更新前的 Virtual DOM 要如何獲取呢?

對(duì)于更新前的 Virtual DOM,對(duì)應(yīng)的其實(shí)就是已經(jīng)在頁(yè)面中顯示的真實(shí) DOM 對(duì)象。既然是這樣,那么我們?cè)趧?chuàng)建真實(shí)DOM對(duì)象時(shí),就可以將 Virtual DOM 添加到真實(shí) DOM 對(duì)象的屬性中。在進(jìn)行 Virtual DOM 對(duì)比之前,就可以通過(guò)真實(shí) DOM 對(duì)象獲取其對(duì)應(yīng)的 Virtual DOM 對(duì)象了,其實(shí)就是通過(guò)render方法的第三個(gè)參數(shù)獲取的,container.firstChild。

在創(chuàng)建真實(shí) DOM 對(duì)象時(shí)為其添加對(duì)應(yīng)的 Virtual DOM 對(duì)象

// mountElement.js
import mountElement from "./mountElement"

export default function mountNativeElement(virtualDOM, container) {
 // 將 Virtual DOM 掛載到真實(shí) DOM 對(duì)象的屬性中 方便在對(duì)比時(shí)獲取其 Virtual DOM
 newElement._virtualDOM = virtualDOM
}

<img src="./images/8.png" width="80%" style="margin-bottom: 30px"/>

9.1 Virtual DOM 類型相同

Virtual DOM 類型相同,如果是元素節(jié)點(diǎn),就對(duì)比元素節(jié)點(diǎn)屬性是否發(fā)生變化,如果是文本節(jié)點(diǎn)就對(duì)比文本節(jié)點(diǎn)內(nèi)容是否發(fā)生變化

要實(shí)現(xiàn)對(duì)比,需要先從已存在 DOM 對(duì)象中獲取其對(duì)應(yīng)的 Virtual DOM 對(duì)象。

// diff.js
// 獲取未更新前的 Virtual DOM
const oldVirtualDOM = oldDOM && oldDOM._virtualDOM

判斷 oldVirtualDOM 是否存在, 如果存在則繼續(xù)判斷要對(duì)比的 Virtual DOM 類型是否相同,如果類型相同判斷節(jié)點(diǎn)類型是否是文本,如果是文本節(jié)點(diǎn)對(duì)比,就調(diào)用 updateTextNode 方法,如果是元素節(jié)點(diǎn)對(duì)比就調(diào)用 setAttributeForElement 方法

// diff.js
else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
  if (virtualDOM.type === "text") {
    // 文本節(jié)點(diǎn) 對(duì)比文本內(nèi)容是否發(fā)生變化
    updateTextNode(virtualDOM, oldVirtualDOM, oldDOM)
  } else {
    // 元素節(jié)點(diǎn) 對(duì)比元素屬性是否發(fā)生變化
    setAttributeForElement(oldDOM, virtualDOM, oldVirtualDOM)
  }

updateTextNode 方法用于對(duì)比文本節(jié)點(diǎn)內(nèi)容是否發(fā)生變化,如果發(fā)生變化則更新真實(shí) DOM 對(duì)象中的內(nèi)容,既然真實(shí) DOM 對(duì)象發(fā)生了變化,還要將最新的 Virtual DOM 同步給真實(shí) DOM 對(duì)象。

function updateTextNode(virtualDOM, oldVirtualDOM, oldDOM) {
  // 如果文本節(jié)點(diǎn)內(nèi)容不同
  if (virtualDOM.props.textContent !== oldVirtualDOM.props.textContent) {
    // 更新真實(shí) DOM 對(duì)象中的內(nèi)容
    oldDOM.textContent = virtualDOM.props.textContent
  }
  // 同步真實(shí) DOM 對(duì)應(yīng)的 Virtual DOM
  oldDOM._virtualDOM = virtualDOM
}

setAttributeForElement 方法用于設(shè)置/更新元素節(jié)點(diǎn)屬性

思路是先分別獲取更新后的和更新前的 Virtual DOM 中的 props 屬性,循環(huán)新 Virtual DOM 中的 props 屬性,通過(guò)對(duì)比看一下新 Virtual DOM 中的屬性值是否發(fā)生了變化,如果發(fā)生變化 需要將變化的值更新到真實(shí) DOM 對(duì)象中

再循環(huán)未更新前的 Virtual DOM 對(duì)象,通過(guò)對(duì)比看看新的 Virtual DOM 中是否有被刪除的屬性,如果存在刪除的屬性 需要將 DOM 對(duì)象中對(duì)應(yīng)的屬性也刪除掉

// updateNodeElement.js
export default function updateNodeElement(
  newElement,
  virtualDOM,
  oldVirtualDOM = {}
) {
  // 獲取節(jié)點(diǎn)對(duì)應(yīng)的屬性對(duì)象
  const newProps = virtualDOM.props || {}
  const oldProps = oldVirtualDOM.props || {}
  Object.keys(newProps).forEach(propName => {
    // 獲取屬性值
    const newPropsValue = newProps[propName]
    const oldPropsValue = oldProps[propName]
    if (newPropsValue !== oldPropsValue) {
      // 判斷屬性是否是否事件屬性 onClick -> click
      if (propName.slice(0, 2) === "on") {
        // 事件名稱
        const eventName = propName.toLowerCase().slice(2)
        // 為元素添加事件
        newElement.addEventListener(eventName, newPropsValue)
        // 刪除原有的事件的事件處理函數(shù)
        if (oldPropsValue) {
          newElement.removeEventListener(eventName, oldPropsValue)
        }
      } else if (propName === "value" || propName === "checked") {
        newElement[propName] = newPropsValue
      } else if (propName !== "children") {
        if (propName === "className") {
          newElement.setAttribute("class", newPropsValue)
        } else {
          newElement.setAttribute(propName, newPropsValue)
        }
      }
    }
  })
  // 判斷屬性被刪除的情況
  Object.keys(oldProps).forEach(propName => {
    const newPropsValue = newProps[propName]
    const oldPropsValue = oldProps[propName]
    if (!newPropsValue) {
      // 屬性被刪除了
      if (propName.slice(0, 2) === "on") {
        const eventName = propName.toLowerCase().slice(2)
        newElement.removeEventListener(eventName, oldPropsValue)
      } else if (propName !== "children") {
        newElement.removeAttribute(propName)
      }
    }
  })
}

以上對(duì)比的僅僅是最上層元素,上層元素對(duì)比完成以后還需要遞歸對(duì)比子元素

else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
    // 遞歸對(duì)比 Virtual DOM 的子元素
    virtualDOM.children.forEach((child, i) => {
      diff(child, oldDOM, oldDOM.childNodes[i])
    })
  }

<img src="./images/7.png"/>

9.2 Virtual DOM 類型不同

當(dāng)對(duì)比的元素節(jié)點(diǎn)類型不同時(shí),就不需要繼續(xù)對(duì)比了,直接使用新的 Virtual DOM 創(chuàng)建 DOM 對(duì)象,用新的 DOM 對(duì)象直接替換舊的 DOM 對(duì)象。當(dāng)前這種情況要將組件刨除,組件要被單獨(dú)處理。

// diff.js
else if (
  // 如果 Virtual DOM 類型不一樣
  virtualDOM.type !== oldVirtualDOM.type &&
  // 并且 Virtual DOM 不是組件 因?yàn)榻M件要單獨(dú)進(jìn)行處理
  typeof virtualDOM.type !== "function"
) {
  // 根據(jù) Virtual DOM 創(chuàng)建真實(shí) DOM 元素
  const newDOMElement = createDOMElement(virtualDOM)
  // 用創(chuàng)建出來(lái)的真實(shí) DOM 元素 替換舊的 DOM 元素
  oldDOM.parentNode.replaceChild(newDOMElement, oldDOM)
} 

9.3 刪除節(jié)點(diǎn)

刪除節(jié)點(diǎn)發(fā)生在節(jié)點(diǎn)更新以后并且發(fā)生在同一個(gè)父節(jié)點(diǎn)下的所有子節(jié)點(diǎn)身上。

在節(jié)點(diǎn)更新完成以后,如果舊節(jié)點(diǎn)對(duì)象的數(shù)量多于新 VirtualDOM 節(jié)點(diǎn)的數(shù)量,就說(shuō)明有節(jié)點(diǎn)需要被刪除。

<img src="./images/5.png" width="40%" align="left"/>

// 獲取就節(jié)點(diǎn)的數(shù)量
let oldChildNodes = oldDOM.childNodes
// 如果舊節(jié)點(diǎn)的數(shù)量多于要渲染的新節(jié)點(diǎn)的長(zhǎng)度
if (oldChildNodes.length > virtualDOM.children.length) {
  for (
    let i = oldChildNodes.length - 1;
    i > virtualDOM.children.length - 1;
    i--
  ) {
    oldDOM.removeChild(oldChildNodes[i])
  }
}

9.4 類組件狀態(tài)更新

以下代碼是要更新?tīng)顟B(tài)的類組件,在類組件的 state 對(duì)象中有默認(rèn)的 title 狀態(tài),點(diǎn)擊 change title 按鈕調(diào)用 handleChange 方法,在 handleChange 方法中調(diào)用 this.setState 方法更改 title 的狀態(tài)值。

class Alert extends TinyReact.Component {
  constructor(props) {
    super(props)
    this.state = {
      title: "default title"
    }
    // 更改 handleChange 方法中的 this 指向 讓 this 指向類實(shí)例對(duì)象
    this.handleChange = this.handleChange.bind(this)
  }
  handleChange() {
    // 調(diào)用父類中的 setState 方法更改狀態(tài)
    this.setState({
      title: "changed title"
    })
  }
  render() {
    return (
      <div>
        <h2>{this.state.title}</h2>
        <p>{this.props.message}</p>
        <button onClick={this.handleChange}>change title</button>
      </div>
    )
  }
}

setState 方法是定義在父類 Component 中的,該方法的作用是更改子類的 state,產(chǎn)生一個(gè)全新的 state 對(duì)象。

// Component.js
export default class Component {
  constructor(props) {
    this.props = props
  }
  setState (state) {
    // setState 方法被子類調(diào)用 此處this指向子類實(shí)例對(duì)象
    // 所以改變的是子類的 state 對(duì)象
    this.state = Object.assign({}, this.state, state)
  }
}

現(xiàn)在子類已經(jīng)可以調(diào)用父類的 setState 方法更改狀態(tài)值了,當(dāng)組件的 state 對(duì)象發(fā)生更改時(shí),要調(diào)用 render 方法更新組件視圖。

在更新組件之前,要使用更新的 Virtual DOM 對(duì)象和未更新的 Virtual DOM 進(jìn)行對(duì)比找出更新的部分,達(dá)到 DOM 最小化操作的目的。

在 setState 方法中可以通過(guò)調(diào)用 this.render 方法獲取更新后的 Virtual DOM,由于 setState 方法被子類調(diào)用,this 指向子類,所以此處調(diào)用的是子類的 render 方法。

// Component.js
setState(state) {
  // setState 方法被子類調(diào)用 此處this指向子類
  // 所以改變的是子類的 state
  this.state = Object.assign({}, this.state, state)
  // 通過(guò)調(diào)用 render 方法獲取最新的 Virtual DOM
  let virtualDOM = this.render()
}

要實(shí)現(xiàn)對(duì)比,還需要獲取未更新前的 Virtual DOM,按照之前的經(jīng)驗(yàn),我們可以從 DOM 對(duì)象中獲取其對(duì)應(yīng)的 Virtual DOM 對(duì)象,未更新前的 DOM 對(duì)象實(shí)際上就是現(xiàn)在在頁(yè)面中顯示的 DOM 對(duì)象,我們只要能獲取到這個(gè) DOM 對(duì)象就可以獲取到其對(duì)應(yīng)的 Virtual DOM 對(duì)象了。

頁(yè)面中的 DOM 對(duì)象要怎樣獲取呢?頁(yè)面中的 DOM 對(duì)象是通過(guò) mountNativeElement 方法掛載到頁(yè)面中的,所以我們只需要在這個(gè)方法中調(diào)用 Component 類中的方法就可以將 DOM 對(duì)象保存在 Component 類中了。在子類調(diào)用 setState 方法的時(shí)候,在 setState 方法中再調(diào)用另一個(gè)獲取 DOM 對(duì)象的方法就可以獲取到之前保存的 DOM 對(duì)象了。

// Component.js
// 保存 DOM 對(duì)象的方法
setDOM(dom) {
  this._dom = dom
}
// 獲取 DOM 對(duì)象的方法
getDOM() {
  return this._dom
}

接下來(lái)我們要研究一下在 mountNativeElement 方法中如何才能調(diào)用到 setDOM 方法,要調(diào)用 setDOM 方法,必須要得到類的實(shí)例對(duì)象,所以目前的問(wèn)題就是如何在 mountNativeElement 方法中得到類的實(shí)例對(duì)象,這個(gè)類指的不是Component類,因?yàn)槲覀冊(cè)诖a中并不是直接實(shí)例化的Component類,而是實(shí)例化的它的子類,由于子類繼承了父類,所以在子類的實(shí)例對(duì)象中也是可以調(diào)用到 setDOM 方法的。

mountNativeElement 方法接收最新的 Virtual DOM 對(duì)象,如果這個(gè) Virtual DOM 對(duì)象是類組件產(chǎn)生的,在產(chǎn)生這個(gè) Virtual DOM 對(duì)象時(shí)一定會(huì)先得到這個(gè)類的實(shí)例對(duì)象,然后再調(diào)用實(shí)例對(duì)象下面的 render 方法進(jìn)行獲取。我們可以在那個(gè)時(shí)候?qū)㈩惤M件實(shí)例對(duì)象添加到 Virtual DOM 對(duì)象的屬性中,而這個(gè) Virtual DOM 對(duì)象最終會(huì)傳遞給 mountNativeElement 方法,這樣我們就可以在 mountNativeElement 方法中獲取到組件的實(shí)例對(duì)象了,既然類組件的實(shí)例對(duì)象獲取到了,我們就可以調(diào)用 setDOM 方法了。

在 buildClassComponent 方法中為 Virtual DOM 對(duì)象添加 component 屬性, 值為類組件的實(shí)例對(duì)象。

function buildClassComponent(virtualDOM) {
  const component = new virtualDOM.type(virtualDOM.props)
  const nextVirtualDOM = component.render()
  nextVirtualDOM.component = component
  return nextVirtualDOM
}

在 mountNativeElement 方法中獲取組件實(shí)例對(duì)象,通過(guò)實(shí)例調(diào)用調(diào)用 setDOM 方法保存 DOM 對(duì)象,方便在對(duì)比時(shí)通過(guò)它獲取它的 Virtual DOM 對(duì)象

export default function mountNativeElement(virtualDOM, container) {
  // 獲取組件實(shí)例對(duì)象
  const component = virtualDOM.component
  // 如果組件實(shí)例對(duì)象存在
  if (component) {
    // 保存 DOM 對(duì)象
    component.setDOM(newElement)
  }
}

接下來(lái)在 setState 方法中就可以調(diào)用 getDOM 方法獲取 DOM 對(duì)象了

setState(state) {
  this.state = Object.assign({}, this.state, state)
  let virtualDOM = this.render()
  // 獲取頁(yè)面中正在顯示的 DOM 對(duì)象 通過(guò)它可以獲取其對(duì)象的 Virtual DOM 對(duì)象
  let oldDOM = this.getDOM()
}

現(xiàn)在更新前的 Virtual DOM 對(duì)象和更新后的 Virtual DOM 對(duì)象就都已經(jīng)獲取到了,接下來(lái)還要獲取到真實(shí) DOM 對(duì)象父級(jí)容器對(duì)象,因?yàn)樵谡{(diào)用 diff 方法進(jìn)行對(duì)比的時(shí)候需要用到

setState(state) {
  this.state = Object.assign({}, this.state, state)
  let virtualDOM = this.render()
  let oldDOM = this.getDOM()
  // 獲取真實(shí) DOM 對(duì)象父級(jí)容器對(duì)象
  let container = oldDOM.parentNode
}

接下來(lái)就可以調(diào)用 diff 方法進(jìn)行比對(duì)了,比對(duì)后會(huì)按照我們之前寫好的邏輯進(jìn)行 DOM 對(duì)象更新,我們就可以在頁(yè)面中看到效果了

setState(state) {
    this.state = Object.assign({}, this.state, state)
    let virtualDOM = this.render()
    let oldDOM = this.getDOM()
    let container = oldDOM.parentNode
    // 比對(duì)
    diff(virtualDOM, container, oldDOM)
  }

9.5 組件更新

在 diff 方法中判斷要更新的 Virtual DOM 是否是組件。

如果是組件再判斷要更新的組件和未更新前的組件是否是同一個(gè)組件,如果不是同一個(gè)組件就不需要做組件更新操作,直接調(diào)用 mountElement 方法將組件返回的 Virtual DOM 添加到頁(yè)面中。

如果是同一個(gè)組件,就執(zhí)行更新組件操作,其實(shí)就是將最新的 props 傳遞到組件中,再調(diào)用組件的render方法獲取組件返回的最新的 Virtual DOM 對(duì)象,再將 Virtual DOM 對(duì)象傳遞給 diff 方法,讓 diff 方法找出差異,從而將差異更新到真實(shí) DOM 對(duì)象中。

在更新組件的過(guò)程中還要在不同階段調(diào)用其不同的組件生命周期函數(shù)。

在 diff 方法中判斷要更新的 Virtual DOM 是否是組件,如果是組件又分為多種情況,新增 diffComponent 方法進(jìn)行處理

else if (typeof virtualDOM.type === "function") {
  // 要更新的是組件
  // 1) 組件本身的 virtualDOM 對(duì)象 通過(guò)它可以獲取到組件最新的 props
  // 2) 要更新的組件的實(shí)例對(duì)象 通過(guò)它可以調(diào)用組件的生命周期函數(shù) 可以更新組件的 props 屬性 可以獲取到組件返回的最新的 Virtual DOM
  // 3) 要更新的 DOM 象 在更新組件時(shí) 需要在已有DOM對(duì)象的身上進(jìn)行修改 實(shí)現(xiàn)DOM最小化操作 獲取舊的 Virtual DOM 對(duì)象
  // 4) 如果要更新的組件和舊組件不是同一個(gè)組件 要直接將組件返回的 Virtual DOM 顯示在頁(yè)面中 此時(shí)需要 container 做為父級(jí)容器
  diffComponent(virtualDOM, oldComponent, oldDOM, container)
}

在 diffComponent 方法中判斷要更新的組件是未更新前的組件是否是同一個(gè)組件

// diffComponent.js
export default function diffComponent(virtualDOM, oldComponent, oldDOM, container) {
  // 判斷要更新的組件和未更新的組件是否是同一個(gè)組件 只需要確定兩者使用的是否是同一個(gè)構(gòu)造函數(shù)就可以了
  if (isSameComponent(virtualDOM, oldComponent)) {
    // 屬同一個(gè)組件 做組件更新  
  } else {
    // 不是同一個(gè)組件 直接將組件內(nèi)容顯示在頁(yè)面中
  }
}
// virtualDOM.type 更新后的組件構(gòu)造函數(shù)
// oldComponent.constructor 未更新前的組件構(gòu)造函數(shù)
// 兩者等價(jià)就表示是同一組件
function isSameComponent(virtualDOM, oldComponent) {
  return oldComponent && virtualDOM.type === oldComponent.constructor
}

如果不是同一個(gè)組件的話,就不需要執(zhí)行更新組件的操作,直接將組件內(nèi)容顯示在頁(yè)面中,替換原有內(nèi)容

// diffComponent.js
else {
  // 不是同一個(gè)組件 直接將組件內(nèi)容顯示在頁(yè)面中
  // 這里為 mountElement 方法新增了一個(gè)參數(shù) oldDOM 
  // 作用是在將 DOM 對(duì)象插入到頁(yè)面前 將頁(yè)面中已存在的 DOM 對(duì)象刪除 否則無(wú)論是舊DOM對(duì)象還是新DOM對(duì)象都會(huì)顯示在頁(yè)面中
  mountElement(virtualDOM, container, oldDOM)
}

在 mountNativeElement 方法中刪除原有的舊 DOM 對(duì)象

// mountNavtiveElement.js
export default function mountNativeElement(virtualDOM, container, oldDOM) {
 // 如果舊的DOM對(duì)象存在 刪除
  if (oldDOM) {
    unmount(oldDOM)
  }
}
// unmount.js
export default function unmount(node) {
  node.remove()
}

如果是同一個(gè)組件的話,需要執(zhí)行組件更新操作,需要調(diào)用組件生命周期函數(shù)

先在 Component 類中添加生命周期函數(shù),子類要使用的話直接覆蓋就可以

// Component.js
export default class Component {
  // 生命周期函數(shù)
  componentWillMount() {}
  componentDidMount() {}
  componentWillReceiveProps(nextProps) {}
  shouldComponentUpdate(nextProps, nextState) {
    return nextProps != this.props || nextState != this.state
  }
  componentWillUpdate(nextProps, nextState) {}
  componentDidUpdate(prevProps, preState) {}
  componentWillUnmount() {}
}

新建 updateComponent 方法用于更新組件操作,并在 if 成立后調(diào)用

// diffComponent.js
if (isSameComponent(virtualDOM, oldComponent)) {
  // 屬同一個(gè)組件 做組件更新
  updateComponent(virtualDOM, oldComponent, oldDOM, container)
}

在 updateComponent 方法中調(diào)用組件的生命周期函數(shù),更新組件獲取最新 Virtual DOM,最終調(diào)用 diff 方法進(jìn)行更新

import diff from "./diff"

export default function updateComponent(
  virtualDOM,
  oldComponent,
  oldDOM,
  container
) {
  // 生命周期函數(shù)
  oldComponent.componentWillReceiveProps(virtualDOM.props)
  if (
    // 調(diào)用 shouldComponentUpdate 生命周期函數(shù)判斷是否要執(zhí)行更新操作
    oldComponent.shouldComponentUpdate(virtualDOM.props)
  ) {
    // 將未更新的 props 保存一份
    let prevProps = oldComponent.props
    // 生命周期函數(shù)
    oldComponent.componentWillUpdate(virtualDOM.props)
    // 更新組件的 props 屬性 updateProps 方法定義在 Component 類型
    oldComponent.updateProps(virtualDOM.props)
    // 因?yàn)榻M件的 props 已經(jīng)更新 所以調(diào)用 render 方法獲取最新的 Virtual DOM
    const nextVirtualDOM = oldComponent.render()
    // 將組件實(shí)例對(duì)象掛載到 Virtual DOM 身上
    nextVirtualDOM.component = oldComponent
    // 調(diào)用diff方法更新視圖
    diff(nextVirtualDOM, container, oldDOM)
    // 生命周期函數(shù)
    oldComponent.componentDidUpdate(prevProps)
  }
}
// Component.js
export default class Component {
  updateProps(props) {
    this.props = props
  }
}

10. ref 屬性

為節(jié)點(diǎn)添加 ref 屬性可以獲取到這個(gè)節(jié)點(diǎn)的 DOM 對(duì)象,比如在 DemoRef 類中,為 input 元素添加了 ref 屬性,目的是獲取 input DOM 元素對(duì)象,在點(diǎn)擊按鈕時(shí)獲取用戶在文本框中輸入的內(nèi)容

class DemoRef extends TinyReact.Component {
  handle() {
    let value = this.input.value
    console.log(value)
  }
  render() {
    return (
      <div>
        <input type="text" ref={input => (this.input = input)} />
        <button onClick={this.handle.bind(this)}>按鈕</button>
      </div>
    )
  }
}

實(shí)現(xiàn)思路是在創(chuàng)建節(jié)點(diǎn)時(shí)判斷其 Virtual DOM 對(duì)象中是否有 ref 屬性,如果有就調(diào)用 ref 屬性中所存儲(chǔ)的方法并且將創(chuàng)建出來(lái)的DOM對(duì)象作為參數(shù)傳遞給 ref 方法,這樣在渲染組件節(jié)點(diǎn)的時(shí)候就可以拿到元素對(duì)象并將元素對(duì)象存儲(chǔ)為組件屬性了。

// createDOMElement.js
if (virtualDOM.props && virtualDOM.props.ref) {
  virtualDOM.props.ref(newElement)
}

在類組件的身上也可以添加 ref 屬性,目的是獲取組件的實(shí)例對(duì)象,比如下列代碼中,在 DemoRef 組件中渲染了 Alert 組件,在 Alert 組件中添加了 ref 屬性,目的是在 DemoRef 組件中獲取 Alert 組件實(shí)例對(duì)象。

class DemoRef extends TinyReact.Component {
  handle() {
    let value = this.input.value
    console.log(value)
    console.log(this.alert)
  }
  componentDidMount() {
    console.log("componentDidMount")
  }
  render() {
    return (
      <div>
        <input type="text" ref={input => (this.input = input)} />
        <button onClick={this.handle.bind(this)}>按鈕</button>
        <Alert ref={alert => (this.alert = alert)} />
      </div>
    )
  }
}

實(shí)現(xiàn)思路是在 mountComponent 方法中,如果判斷了當(dāng)前處理的是類組件,就通過(guò)類組件返回的 Virtual DOM 對(duì)象中獲取組件實(shí)例對(duì)象,判斷組件實(shí)例對(duì)象中的 props 屬性中是否存在 ref 屬性,如果存在就調(diào)用 ref 方法并且將組件實(shí)例對(duì)象傳遞給 ref 方法。

// mountComponent.js
let component = null
  if (isFunctionalComponent(virtualDOM)) {}
    else {
    // 類組件
    nextVirtualDOM = buildStatefulComponent(virtualDOM)
    // 獲取組件實(shí)例對(duì)象
    component = nextVirtualDOM.component
  }
    // 如果組件實(shí)例對(duì)象存在的話
    if (component) {
    // 判斷組件實(shí)例對(duì)象身上是否有 props 屬性 props 屬性中是否有 ref 屬性
    if (component.props && component.props.ref) {
      // 調(diào)用 ref 方法并傳遞組件實(shí)例對(duì)象
      component.props.ref(component)
    }
  }

代碼走到這,順便處理一下組件掛載完成的生命周期函數(shù)

// 如果組件實(shí)例對(duì)象存在的話
if (component) {
  component.componentDidMount()
}

11. key 屬性

在 React 中,渲染列表數(shù)據(jù)時(shí)通常會(huì)在被渲染的列表元素上添加 key 屬性,key 屬性就是數(shù)據(jù)的唯一標(biāo)識(shí),幫助 React 識(shí)別哪些數(shù)據(jù)被修改或者刪除了,從而達(dá)到 DOM 最小化操作的目的。

key 屬性不需要全局唯一,但是在同一個(gè)父節(jié)點(diǎn)下的兄弟節(jié)點(diǎn)之間必須是唯一的。

也就是說(shuō),在比對(duì)同一個(gè)父節(jié)點(diǎn)下類型相同的子節(jié)點(diǎn)時(shí)需要用到 key 屬性。

11.1 節(jié)點(diǎn)對(duì)比

實(shí)現(xiàn)思路是在兩個(gè)元素進(jìn)行比對(duì)時(shí),如果類型相同,就循環(huán)舊的 DOM 對(duì)象的子元素,查看其身上是否有key 屬性,如果有就將這個(gè)子元素的 DOM 對(duì)象存儲(chǔ)在一個(gè) JavaScript 對(duì)象中,接著循環(huán)要渲染的 Virtual DOM 對(duì)象的子元素,在循環(huán)過(guò)程中獲取到這個(gè)子元素的 key 屬性,然后使用這個(gè) key 屬性到 JavaScript 對(duì)象中查找 DOM 對(duì)象,如果能夠找到就說(shuō)明這個(gè)元素是已經(jīng)存在的,是不需要重新渲染的。如果通過(guò)key屬性找不到這個(gè)元素,就說(shuō)明這個(gè)元素是新增的是需要渲染的。

// diff.js
else if (oldVirtualDOM && virtualDOM.type === oldVirtualDOM.type) {
  // 將擁有key屬性的元素放入 keyedElements 對(duì)象中
  let keyedElements = {}
  for (let i = 0, len = oldDOM.childNodes.length; i < len; i++) {
    let domElement = oldDOM.childNodes[i]
    if (domElement.nodeType === 1) {
      let key = domElement.getAttribute("key")
      if (key) {
        keyedElements[key] = domElement
      }
    }
  }
}
// diff.js
// 看一看是否有找到了擁有 key 屬性的元素
let hasNoKey = Object.keys(keyedElements).length === 0

// 如果沒(méi)有找到擁有 key 屬性的元素 就按照索引進(jìn)行比較
if (hasNoKey) {
  // 遞歸對(duì)比 Virtual DOM 的子元素
  virtualDOM.children.forEach((child, i) => {
    diff(child, oldDOM, oldDOM.childNodes[i])
  })
} else {
  // 使用key屬性進(jìn)行元素比較
  virtualDOM.children.forEach((child, i) => {
    // 獲取要進(jìn)行比對(duì)的元素的 key 屬性
    let key = child.props.key
    // 如果 key 屬性存在
    if (key) {
      // 到已存在的 DOM 元素對(duì)象中查找對(duì)應(yīng)的 DOM 元素
      let domElement = keyedElements[key]
      // 如果找到元素就說(shuō)明該元素已經(jīng)存在 不需要重新渲染
      if (domElement) {
        // 雖然 DOM 元素不需要重新渲染 但是不能確定元素的位置就一定沒(méi)有發(fā)生變化
        // 所以還要查看一下元素的位置
        // 看一下 oldDOM 對(duì)應(yīng)的(i)子元素和 domElement 是否是同一個(gè)元素 如果不是就說(shuō)明元素位置發(fā)生了變化
        if (oldDOM.childNodes[i] && oldDOM.childNodes[i] !== domElement) {
          // 元素位置發(fā)生了變化
          // 將 domElement 插入到當(dāng)前元素位置的前面 oldDOM.childNodes[i] 就是當(dāng)前位置
          // domElement 就被放入了當(dāng)前位置
          oldDOM.insertBefore(domElement, oldDOM.childNodes[i])
        }
      } else {
        mountElement(child, oldDOM, oldDOM.childNodes[i])
      }
    }
  })
}
// mountNativeElement.js
if (oldDOM) {
  container.insertBefore(newElement, oldDOM)
} else {
  // 將轉(zhuǎn)換之后的DOM對(duì)象放置在頁(yè)面中
  container.appendChild(newElement)
}

11.2 節(jié)點(diǎn)卸載

在比對(duì)節(jié)點(diǎn)的過(guò)程中,如果舊節(jié)點(diǎn)的數(shù)量多于要渲染的新節(jié)點(diǎn)的數(shù)量就說(shuō)明有節(jié)點(diǎn)被刪除了,繼續(xù)判斷 keyedElements 對(duì)象中是否有元素,如果沒(méi)有就使用索引方式刪除,如果有就要使用 key 屬性比對(duì)的方式進(jìn)行刪除。

實(shí)現(xiàn)思路是循環(huán)舊節(jié)點(diǎn),在循環(huán)舊節(jié)點(diǎn)的過(guò)程中獲取舊節(jié)點(diǎn)對(duì)應(yīng)的 key 屬性,然后根據(jù) key 屬性在新節(jié)點(diǎn)中查找這個(gè)舊節(jié)點(diǎn),如果找到就說(shuō)明這個(gè)節(jié)點(diǎn)沒(méi)有被刪除,如果沒(méi)有找到,就說(shuō)明節(jié)點(diǎn)被刪除了,調(diào)用卸載節(jié)點(diǎn)的方法卸載節(jié)點(diǎn)即可。

// 獲取就節(jié)點(diǎn)的數(shù)量
let oldChildNodes = oldDOM.childNodes
// 如果舊節(jié)點(diǎn)的數(shù)量多于要渲染的新節(jié)點(diǎn)的長(zhǎng)度
if (oldChildNodes.length > virtualDOM.children.length) {
  if (hasNoKey) {
    for (
      let i = oldChildNodes.length - 1;
      i >= virtualDOM.children.length;
      i--
    ) {
      oldDOM.removeChild(oldChildNodes[i])
    }
  } else {
    for (let i = 0; i < oldChildNodes.length; i++) {
      let oldChild = oldChildNodes[i]
      let oldChildKey = oldChild._virtualDOM.props.key
      let found = false
      for (let n = 0; n < virtualDOM.children.length; n++) {
        if (oldChildKey === virtualDOM.children[n].props.key) {
          found = true
          break
        }
      }
      if (!found) {
        unmount(oldChild)
        i--
      }
    }
  }
}

卸載節(jié)點(diǎn)并不是說(shuō)將節(jié)點(diǎn)直接刪除就可以了,還需要考慮以下幾種情況

  1. 如果要?jiǎng)h除的節(jié)點(diǎn)是文本節(jié)點(diǎn)的話可以直接刪除
  2. 如果要?jiǎng)h除的節(jié)點(diǎn)由組件生成,需要調(diào)用組件卸載生命周期函數(shù)
  3. 如果要?jiǎng)h除的節(jié)點(diǎn)中包含了其他組件生成的節(jié)點(diǎn),需要調(diào)用其他組件的卸載生命周期函數(shù)
  4. 如果要?jiǎng)h除的節(jié)點(diǎn)身上有 ref 屬性,還需要?jiǎng)h除通過(guò) ref 屬性傳遞給組件的 DOM 節(jié)點(diǎn)對(duì)象
  5. 如果要?jiǎng)h除的節(jié)點(diǎn)身上有事件,需要?jiǎng)h除事件對(duì)應(yīng)的事件處理函數(shù)
export default function unmount(dom) {
  // 獲取節(jié)點(diǎn)對(duì)應(yīng)的 virtualDOM 對(duì)象
  const virtualDOM = dom._virtualDOM
  // 如果要?jiǎng)h除的節(jié)點(diǎn)時(shí)文本
  if (virtualDOM.type === "text") {
    // 直接刪除節(jié)點(diǎn)
    dom.remove()
    // 阻止程序向下運(yùn)行
    return
  }
  // 查看節(jié)點(diǎn)是否由組件生成
  let component = virtualDOM.component
  // 如果由組件生成
  if (component) {
    // 調(diào)用組件卸載生命周期函數(shù)
    component.componentWillUnmount()
  }
  
  // 如果節(jié)點(diǎn)具有 ref 屬性 通過(guò)再次調(diào)用 ref 方法 將傳遞給組件的DOM對(duì)象刪除
  if (virtualDOM.props && virtualDOM.props.ref) {
    virtualDOM.props.ref(null)
  }

  // 事件處理
  Object.keys(virtualDOM.props).forEach(propName => {
    if (propName.slice(0, 2) === "on") {
      const eventName = propName.toLowerCase().slice(2)
      const eventHandler = virtualDOM.props[propName]
      dom.removeEventListener(eventName, eventHandler)
    }
  })
    
  // 遞歸刪除子節(jié)點(diǎn)
  if (dom.childNodes.length > 0) {
    for (let i = 0; i < dom.childNodes.length; i++) {
      unmount(dom.childNodes[i])
      i--
    }
  }
    
  dom.remove()
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險(xiǎn)厭惡者,不喜歡去冒險(xiǎn),但是人生放棄了冒險(xiǎn),也就放棄了無(wú)數(shù)的可能。 ...
    yichen大刀閱讀 7,867評(píng)論 0 4
  • 公元:2019年11月28日19時(shí)42分農(nóng)歷:二零一九年 十一月 初三日 戌時(shí)干支:己亥乙亥己巳甲戌當(dāng)月節(jié)氣:立冬...
    石放閱讀 7,455評(píng)論 0 2
  • 今天上午陪老媽看病,下午健身房跑步,晚上想想今天還沒(méi)有斷舍離,馬上做,衣架和旁邊的的布衣架,一看亂亂,又想想自己是...
    影子3623253閱讀 3,064評(píng)論 3 8

友情鏈接更多精彩內(nèi)容