深入框架本源系列 —— Virtual Dom

該系列會逐步更新,完整的講解目前主流框架中底層相通的技術,接下來的代碼內容都會更新在 這里

為什么需要 Virtual Dom

眾所周知,操作 DOM 是很耗費性能的一件事情,既然如此,我們可以考慮通過 JS 對象來模擬 DOM 對象,畢竟操作 JS 對象比操作 DOM 省時的多。

舉個例子



// 假設這里模擬一個 ul,其中包含了 5 個 li
[1, 2, 3, 4, 5] 
// 這里替換上面的 li
[1, 2, 5, 4]

從上述例子中,我們一眼就可以看出先前的 ul 中的第三個 li 被移除了,四五替換了位置。

如果以上操作對應到 DOM 中,那么就是以下代碼


// 刪除第三個 li
ul.childNodes[2].remove()
// 將第四個 li 和第五個交換位置
let fromNode = ul.childNodes[4]
let toNode = node.childNodes[3]
let cloneFromNode = fromNode.cloneNode(true)
let cloenToNode = toNode.cloneNode(true)
ul.replaceChild(cloneFromNode, toNode)
ul.replaceChild(cloenToNode, fromNode)

當然在實際操作中,我們還需要給每個節點一個標識,作為判斷是同一個節點的依據。所以這也是 Vue 和 React 中官方推薦列表里的節點使用唯一的 key 來保證性能。

那么既然 DOM 對象可以通過 JS 對象來模擬,反之也可以通過 JS 對象來渲染出對應的 DOM

以下是一個 JS 對象模擬 DOM 對象的簡單實現



export default class Element {/*** @param {String} tag 'div'* @param {Object} props { class: 'item' }* @param {Array} children [ Element1, 'text']* @param {String} key option*/constructor(tag, props, children, key) {this.tag = tagthis.props = propsif (Array.isArray(children)) {this.children = children} else if (isString(children)) {this.key = childrenthis.children = null}if (key) this.key = key}// 渲染render() {let root = this._createElement(this.tag,this.props,this.children,this.key)document.body.appendChild(root)return root}create() {return this._createElement(this.tag, this.props, this.children, this.key)}// 創建節點_createElement(tag, props, child, key) {// 通過 tag 創建節點let el = document.createElement(tag)// 設置節點屬性for (const key in props) {if (props.hasOwnProperty(key)) {const value = props[key]el.setAttribute(key, value)}}if (key) {el.setAttribute('key', key)}// 遞歸添加子節點if (child) {child.forEach(element => {let childif (element instanceof Element) {child = this._createElement(element.tag,element.props,element.children,element.key)} else {child = document.createTextNode(element)}el.appendChild(child)})}return el}
}

Virtual Dom 算法簡述

既然我們已經通過 JS 來模擬實現了 DOM,那么接下來的難點就在于如何判斷舊的對象和新的對象之間的差異。

DOM 是多叉樹的結構,如果需要完整的對比兩顆樹的差異,那么需要的時間復雜度會是 O(n ^ 3),這個復雜度肯定是不能接受的。于是 React 團隊優化了算法,實現了 O(n) 的復雜度來對比差異。

實現 O(n) 復雜度的關鍵就是只對比同層的節點,而不是跨層對比,這也是考慮到在實際業務中很少會去跨層的移動 DOM 元素。

所以判斷差異的算法就分為了兩步

  • 首先從上至下,從左往右遍歷對象,也就是樹的深度遍歷,這一步中會給每個節點添加索引,便于最后渲染差異
  • 一旦節點有子元素,就去判斷子元素是否有不同

Virtual Dom 算法實現

樹的遞歸

首先我們來實現樹的遞歸算法,在實現該算法前,先來考慮下兩個節點對比會有幾種情況

  1. 新的節點的 tagName 或者 key 和舊的不同,這種情況代表需要替換舊的節點,并且也不再需要遍歷新舊節點的子元素了,因為整個舊節點都被刪掉了
  2. 新的節點的 tagNamekey(可能都沒有)和舊的相同,開始遍歷子樹
  3. 沒有新的節點,那么什么都不用做


import { StateEnums, isString, move } from './util'
import Element from './element'export default function diff(oldDomTree, newDomTree) {// 用于記錄差異let pathchs = {}// 一開始的索引為 0dfs(oldDomTree, newDomTree, 0, pathchs)return pathchs
}function dfs(oldNode, newNode, index, patches) {// 用于保存子樹的更改let curPatches = []// 需要判斷三種情況// 1.沒有新的節點,那么什么都不用做// 2.新的節點的 tagName 和 `key` 和舊的不同,就替換// 3.新的節點的 tagName 和 key(可能都沒有) 和舊的相同,開始遍歷子樹if (!newNode) {} else if (newNode.tag === oldNode.tag && newNode.key === oldNode.key) {// 判斷屬性是否變更let props = diffProps(oldNode.props, newNode.props)if (props.length) curPatches.push({ type: StateEnums.ChangeProps, props })// 遍歷子樹diffChildren(oldNode.children, newNode.children, index, patches)} else {// 節點不同,需要替換curPatches.push({ type: StateEnums.Replace, node: newNode })}if (curPatches.length) {if (patches[index]) {patches[index] = patches[index].concat(curPatches)} else {patches[index] = curPatches}}
}

判斷屬性的更改

判斷屬性的更改也分三個步驟

  1. 遍歷舊的屬性列表,查看每個屬性是否還存在于新的屬性列表中
  2. 遍歷新的屬性列表,判斷兩個列表中都存在的屬性的值是否有變化
  3. 在第二步中同時查看是否有屬性不存在與舊的屬性列列表中

function diffProps(oldProps, newProps) {// 判斷 Props 分以下三步驟// 先遍歷 oldProps 查看是否存在刪除的屬性// 然后遍歷 newProps 查看是否有屬性值被修改// 最后查看是否有屬性新增let change = []for (const key in oldProps) {if (oldProps.hasOwnProperty(key) && !newProps[key]) {change.push({prop: key})}}for (const key in newProps) {if (newProps.hasOwnProperty(key)) {const prop = newProps[key]if (oldProps[key] && oldProps[key] !== newProps[key]) {change.push({prop: key,value: newProps[key]})} else if (!oldProps[key]) {change.push({prop: key,value: newProps[key]})}}}return change
}

判斷列表差異算法實現

這個算法是整個 Virtual Dom 中最核心的算法,且讓我一一為你道來。 這里的主要步驟其實和判斷屬性差異是類似的,也是分為三步

  1. 遍歷舊的節點列表,查看每個節點是否還存在于新的節點列表中
  2. 遍歷新的節點列表,判斷是否有新的節點
  3. 在第二步中同時判斷節點是否有移動

PS:該算法只對有 key 的節點做處理



function listDiff(oldList, newList, index, patches) {// 為了遍歷方便,先取出兩個 list 的所有 keyslet oldKeys = getKeys(oldList)let newKeys = getKeys(newList)let changes = []// 用于保存變更后的節點數據// 使用該數組保存有以下好處// 1.可以正確獲得被刪除節點索引// 2.交換節點位置只需要操作一遍 DOM// 3.用于 `diffChildren` 函數中的判斷,只需要遍歷// 兩個樹中都存在的節點,而對于新增或者刪除的節點來說,完全沒必要// 再去判斷一遍let list = []oldList &&oldList.forEach(item => {let key = item.keyif (isString(item)) {key = item}// 尋找新的 children 中是否含有當前節點// 沒有的話需要刪除let index = newKeys.indexOf(key)if (index === -1) {list.push(null)} else list.push(key)})// 遍歷變更后的數組let length = list.length// 因為刪除數組元素是會更改索引的// 所有從后往前刪可以保證索引不變for (let i = length - 1; i >= 0; i--) {// 判斷當前元素是否為空,為空表示需要刪除if (!list[i]) {list.splice(i, 1)changes.push({type: StateEnums.Remove,index: i})}}// 遍歷新的 list,判斷是否有節點新增或移動// 同時也對 `list` 做節點新增和移動節點的操作newList &&newList.forEach((item, i) => {let key = item.keyif (isString(item)) {key = item}// 尋找舊的 children 中是否含有當前節點let index = list.indexOf(key)// 沒找到代表新節點,需要插入if (index === -1  || key == null) {changes.push({type: StateEnums.Insert,node: item,index: i})list.splice(i, 0, key)} else {// 找到了,需要判斷是否需要移動if (index !== i) {changes.push({type: StateEnums.Move,from: index,to: i})move(list, index, i)}}})return { changes, list }
}function getKeys(list) {let keys = []let textlist &&list.forEach(item => {let keyif (isString(item)) {key = [item]} else if (item instanceof Element) {key = item.key}keys.push(key)})return keys
}

遍歷子元素打標識

對于這個函數來說,主要功能就兩個

  1. 判斷兩個列表差異
  2. 給節點打上標記

總體來說,該函數實現的功能很簡單



function diffChildren(oldChild, newChild, index, patches) {let { changes, list } = listDiff(oldChild, newChild, index, patches)if (changes.length) {if (patches[index]) {patches[index] = patches[index].concat(changes)} else {patches[index] = changes}}// 記錄上一個遍歷過的節點let last = nulloldChild &&oldChild.forEach((item, i) => {let child = item && item.childrenif (child) {index =last && last.children ? index + last.children.length + 1 : index + 1let keyIndex = list.indexOf(item.key)let node = newChild[keyIndex]// 只遍歷新舊中都存在的節點,其他新增或者刪除的沒必要遍歷if (node) {dfs(item, node, index, patches)}} else index += 1last = item})
}

渲染差異

通過之前的算法,我們已經可以得出兩個樹的差異了。既然知道了差異,就需要局部去更新 DOM 了,下面就讓我們來看看 Virtual Dom 算法的最后一步驟

這個函數主要兩個功能

  1. 深度遍歷樹,將需要做變更操作的取出來
  2. 局部更新 DOM

整體來說這部分代碼還是很好理解的


let index = 0
export default function patch(node, patchs) {let changes = patchs[index]let childNodes = node && node.childNodes// 這里的深度遍歷和 diff 中是一樣的if (!childNodes) index += 1if (changes && changes.length && patchs[index]) {changeDom(node, changes)}let last = nullif (childNodes && childNodes.length) {childNodes.forEach((item, i) => {index =last && last.children ? index + last.children.length + 1 : index + 1patch(item, patchs)last = item})}
}function changeDom(node, changes, noChild) {changes &&changes.forEach(change => {let { type } = changeswitch (type) {case StateEnums.ChangeProps:let { props } = changeprops.forEach(item => {if (item.value) {node.setAttribute(item.prop, item.value)} else {node.removeAttribute(item.prop)}})breakcase StateEnums.Remove:node.childNodes[change.index].remove()breakcase StateEnums.Insert:let domif (isString(change.node)) {dom = document.createTextNode(change.node)} else if (change.node instanceof Element) {dom = change.node.create()}node.insertBefore(dom, node.childNodes[change.index])breakcase StateEnums.Replace:node.parentNode.replaceChild(change.node.create(), node)breakcase StateEnums.Move:let fromNode = node.childNodes[change.from]let toNode = node.childNodes[change.to]let cloneFromNode = fromNode.cloneNode(true)let cloenToNode = toNode.cloneNode(true)node.replaceChild(cloneFromNode, toNode)node.replaceChild(cloenToNode, fromNode)breakdefault:break}})
}

最后

Virtual Dom 算法的實現也就是以下三步

  1. 通過 JS 來模擬創建 DOM 對象
  2. 判斷兩個對象的差異
  3. 渲染差異

let test4 = new Element('div', { class: 'my-div' }, ['test4'])
let test5 = new Element('ul', { class: 'my-div' }, ['test5'])let test1 = new Element('div', { class: 'my-div' }, [test4])let test2 = new Element('div', { id: '11' }, [test5, test4])let root = test1.render()let pathchs = diff(test1, test2)
console.log(pathchs)setTimeout(() => {console.log('開始更新')patch(root, pathchs)console.log('結束更新')
}, 1000)

當然目前的實現還略顯粗糙,但是對于理解 Virtual Dom 算法來說已經是完全足夠的了。

文章中的代碼你可以在?這里?閱讀。本系列更新的文章都會更新在這個倉庫中,有興趣的可以關注下。

下篇文章的內容將會是狀態管理,敬請期待。


原文發布時間為:2018年06月02日
原文作者:夕陽
本文來源:?掘金?如需轉載請聯系原作者

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/390229.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/390229.shtml
英文地址,請注明出處:http://en.pswp.cn/news/390229.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

網絡工程師常備工具_網絡安全工程師應該知道的10種工具

網絡工程師常備工具If youre a penetration tester, there are numerous tools you can use to help you accomplish your goals. 如果您是滲透測試人員,則可以使用許多工具來幫助您實現目標。 From scanning to post-exploitation, here are ten tools you must k…

configure: error: You need a C++ compiler for C++ support.

安裝pcre包的時候提示缺少c編譯器 報錯信息如下: configure: error: You need a C compiler for C support. 解決辦法,使用yum安裝:yum -y install gcc-c 轉載于:https://www.cnblogs.com/mkl34367803/p/8428264.html

程序編寫經驗教訓_編寫您永遠都不會忘記的有效績效評估的經驗教訓。

程序編寫經驗教訓This article is intended for two audiences: people who need to write self-evaluations, and people who need to provide feedback to their colleagues. 本文面向兩個受眾:需要編寫自我評估的人員和需要向同事提供反饋的人員。 For the purp…

刪除文件及文件夾命令

方法一: echo off ::演示:刪除指定路徑下指定天數之前(以文件的最后修改日期為準)的文件。 ::如果演示結果無誤,把del前面的echo去掉,即可實現真正刪除。 ::本例需要Win2003/Vista/Win7系統自帶的forfiles命…

BZOJ 3527: [ZJOI2014]力(FFT)

題意 給出\(n\)個數\(q_i\),給出\(Fj\)的定義如下&#xff1a; \[F_j\sum \limits _ {i < j} \frac{q_iq_j}{(i-j)^2}-\sum \limits _{i >j} \frac{q_iq_j}{(i-j)^2}.\] 令\(E_iF_i/q_i\)&#xff0c;求\(E_i\). 題解 一開始沒發現求\(E_i\)... 其實題目還更容易想了... …

c# 實現刷卡_如何在RecyclerView中實現“刷卡選項”

c# 實現刷卡Lets say a user of your site wants to edit a list item without opening the item and looking for editing options. If you can enable this functionality, it gives that user a good User Experience. 假設您網站的用戶想要在不打開列表項并尋找編輯選項的情…

批處理命令無法連續執行

如題&#xff0c;博主一開始的批處理命令是這樣的&#xff1a; cd node_modules cd heapdump node-gyp rebuild cd .. cd v8-profiler-node8 node-pre-gyp rebuild cd .. cd utf-8-validate node-gyp rebuild cd .. cd bufferutil node-gyp rebuild pause執行結果&#xff1…

sql語句中的in用法示例_示例中JavaScript in操作符

sql語句中的in用法示例One of the first topics you’ll come across when learning JavaScript (or any other programming language) are operators. 學習JavaScript(或任何其他編程語言)時遇到的第一個主題之一是運算符。 The most common operators are the arithmetic, l…

vue項目實戰總結

馬上過年了&#xff0c;最近工作不太忙&#xff0c;再加上本人最近比較懶&#xff0c;毫無斗志&#xff0c;不愿學習新東西&#xff0c;或許是要過年的緣故(感覺像是在找接口)。 就把前一段時間做過的vue項目&#xff0c;進行一次完整的總結。 這次算是詳細總結&#xff0c;會從…

Linux !的使用

轉自&#xff1a;https://www.linuxidc.com/Linux/2015-05/117774.htm 一、history    78 cd /mnt/ 79 ls 80 cd / 81 history 82 ls 83 ls /mnt/ !78 相當于執行cd /mnt !-6 也相當于執行cd /mnt 二、!$ cd /mnt ls !$ 相當于執行 ls /mnt轉載于:https://www.cnblogs.…

881. 救生艇

881. 救生艇 第 i 個人的體重為 people[i]&#xff0c;每艘船可以承載的最大重量為 limit。 每艘船最多可同時載兩人&#xff0c;但條件是這些人的重量之和最多為 limit。 返回載到每一個人所需的最小船數。(保證每個人都能被船載)。 示例 1&#xff1a; 輸入&#xff1a;…

使用python數據分析_如何使用Python提升您的數據分析技能

使用python數據分析If youre learning Python, youve likely heard about sci-kit-learn, NumPy and Pandas. And these are all important libraries to learn. But there is more to them than you might initially realize.如果您正在學習Python&#xff0c;則可能聽說過sci…

openresty 日志輸出的處理

最近出了個故障&#xff0c;有個接口的請求居然出現了長達幾十秒的處理時間&#xff0c;由于日志缺乏&#xff0c;網絡故障也解除了&#xff0c;就沒法再重現這個故障了。為了可以在下次出現問題的時候能追查到問題&#xff0c;所以需要添加一些追蹤日志。添加這些追蹤日志&…

誰是贏家_贏家的真正作品是股東

誰是贏家As I wrote in the article “5 Skills to Look For When Hiring Remote Talent,” remote work is a fast emerging segment of the labor market. Today roughly eight million Americans work remotely full-time. And among the most commonly held jobs include m…

博客園代碼黑色主題高亮設置

參考鏈接&#xff1a; https://segmentfault.com/a/1190000013001367 先發鏈接&#xff0c;有空實踐后會整理。我的GitHub地址&#xff1a;https://github.com/heizemingjun我的博客園地址&#xff1a;http://www.cnblogs.com/chenmingjun我的螞蟻筆記博客地址&#xff1a;http…

Matplotlib課程–學習Python數據可視化

Learn the basics of Matplotlib in this crash course tutorial. Matplotlib is an amazing data visualization library for Python. You will also learn how to apply Matplotlib to real-world problems.在此速成班教程中學習Matplotlib的基礎知識。 Matplotlib是一個很棒…

Android 開發使用 Gradle 配置構建庫模塊的工作方式

Android 開發過程中&#xff0c;我們不可避免地需要引入其他人的工作成果。減少重復“造輪子”的時間&#xff0c;投入到更有意義的核心任務當中。Android 庫模塊在結構上與 Android 應用模塊相同。提供構建應用所需的一切內容&#xff0c;包括源代碼&#xff08;src&#xff0…

vue 組件庫發布_如何創建和發布Vue組件庫

vue 組件庫發布Component libraries are all the rage these days. They make it easy to maintain a consistent look and feel across an application. 如今&#xff0c;組件庫風行一時。 它們使在整個應用程序中保持一致的外觀和感覺變得容易。 Ive used a variety of diff…

angular

<input type"file" id"one-input" accept"image/*" file-model"images" οnchange"angular.element(this).scope().img_upload(this.files)"/>轉載于:https://www.cnblogs.com/loweringye/p/8441437.html

Java網絡編程 — Netty入門

認識Netty Netty簡介 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty is a NIO client server framework which enables quick and easy development o…