virtualdom diff算法實(shí)現(xiàn)分析(三)

React系列

React簡單模擬語法(一)
Jsx, 合成事件與Refs(二)
virtualdom diff算法實(shí)現(xiàn)分析(三)
從Mixin到HOC再到HOOKS(四)
createElement, ReactElement與Component部分源碼解析(五)

完整代碼可查看virtualdom-diff

渲染DOM

經(jīng)歷過PHP模板開發(fā)或者JQuery的洗禮的人都知道,它們實(shí)現(xiàn)重新渲染采用最簡單粗暴的辦法就是重新構(gòu)建DOM替換舊DOM,問題也很明顯

  • 性能消耗高
  • 無法保存狀態(tài)(聚焦,滾動等)

我們先看看創(chuàng)建一個元素所包含的實(shí)例屬性有多少個

const div = document.createElement('div');
let num = 0;
for (let k in div) {
  num++;
}
console.log(num); // 241

然后瀏覽器根據(jù)CSS規(guī)則查找匹配節(jié)點(diǎn),計(jì)算合并樣式布局,為了避免重新計(jì)算一般瀏覽器會保存這些數(shù)據(jù).但這是整個過程下來依然會耗費(fèi)大量的內(nèi)存和 CPU 資源.

Virtual DOM

實(shí)際也是操作Dom樹進(jìn)行渲染更新,但是它只是針對修改部分進(jìn)行局部渲染,將影響降到最低,雖然實(shí)現(xiàn)方式各有不同,但是大體步驟如下:

  1. 用Javascript對象結(jié)構(gòu)描述Dom樹結(jié)構(gòu),然后用它來構(gòu)建真正的Dom樹插入文檔
  2. 當(dāng)狀態(tài)發(fā)生改變之后,重新構(gòu)造新的Javascript對象結(jié)構(gòu)和舊的作對比得出差異
  3. 針對差異之處進(jìn)行重新構(gòu)建更新視圖

無非就是利用Js做一層映射比較,操作簡單并且速度遠(yuǎn)遠(yuǎn)高于直接比較Dom樹

基礎(chǔ)工具函數(shù)

無非就是一些類型判斷,循環(huán)遍歷的簡化函數(shù)

function type(obj) {
  return Object.prototype.toString.call(obj).replace(/\[object\s|\]/g, "");
}

function isArray(list) {
  return type(list) === "Array";
}

function isObject(obj) {
  return type(obj) === "Object";
}

function isString(str) {
  return type(str) === "String";
}

function isNotEmptyObj(obj) {
  return isObject(obj) && JSON.stringify(obj) != "{}";
}

function objForEach(obj, fn) {
  isNotEmptyObj(obj) && Object.keys(obj).forEach(fn);
}

function aryForEach(ary, fn) {
  ary.length && ary.forEach(fn);
}

function setAttr(node, key, value) {
  switch (key) {
    case "style":
      node.style.cssText = value;
      break;
    case "value":
      var tagName = node.tagName || "";
      tagName = tagName.toLowerCase();
      if (tagName === "input" || tagName === "textarea") {
        node.value = value;
      } else {
        // if it is not a input or textarea, use `setAttribute` to set
        node.setAttribute(key, value);
      }
      break;
    default:
      node.setAttribute(key, value);
      break;
  }
}

function toArray(data) {
  if (!data) {
    return [];
  }
  const ary = [];
  aryForEach(data, item => {
    ary.push(item);
  });

  return ary;
}

export {
  isArray,
  isObject,
  isString,
  isNotEmptyObj,
  objForEach,
  aryForEach,
  setAttr,
  toArray
};

相關(guān)代碼可以查看util.js

Javascript對象結(jié)構(gòu)描述

我之前講JSX的時候舉過這么個例子,然后我們就以這個來實(shí)現(xiàn)效果吧

  123456
"use strict";

React.createElement("div", {
  className: "num",
  index: 1
}, React.createElement("span", null, "123456"));

創(chuàng)建一個Element類負(fù)責(zé)將Javascript對象結(jié)構(gòu)轉(zhuǎn)換為Dom樹結(jié)構(gòu)

import {
  isObject,
  isString,
  isArray,
  isNotEmptyObj,
  objForEach,
  aryForEach
} from "./util";
import { NOKEY } from "./common";

class Element {
  constructor(tagName, props, children) {
    // 解析參數(shù)
    this.tagName = tagName;
    // 字段處理,可省略參數(shù)
    this.props = isObject(props) ? props : {};
    this.children =
      children ||
      (!isNotEmptyObj(this.props) &&
        ((isString(props) && [props]) || (isArray(props) && props))) ||
      [];
    // 無論void后的表達(dá)式是什么,void操作符都會返回undefined
    this.key = props ? props.key : void NOKEY;

    // 計(jì)算節(jié)點(diǎn)數(shù)
    let count = 0;
    aryForEach(this.children, (item, index) => {
      if (item instanceof Element) {
        count += item.count;
      } else {
        this.children[index] = "" + item;
      }
      count++;
    });
    this.count = count;
  }

  render() {
    // 根據(jù)tagName構(gòu)建
    const dom = document.createElement(this.tagName);

    // 設(shè)置props
    objForEach(this.props, propName =>
      dom.setAttribute(propName, this.props[propName])
    );

    // 渲染children
    aryForEach(this.children, child => {
      const childDom =
        child instanceof Element
          ? child.render() // 如果子節(jié)點(diǎn)也是虛擬DOM,遞歸構(gòu)建DOM節(jié)點(diǎn)
          : document.createTextNode(child); // 如果字符串,只構(gòu)建文本節(jié)點(diǎn)
      dom.appendChild(childDom);
    });
    return dom;
  }
}

// 改變傳參方式,免去手動實(shí)例化
export default function CreateElement(tagName, props, children) {
  return new Element( tagName, props, children );
}

新建示例,調(diào)用方式如下

// 1. 構(gòu)建虛擬DOM
const tree = createElement("div", { id: "root" }, [
  createElement("h1", { style: "color: blue" }, ["Tittle1"]),
  createElement("p", ["Hello, virtual-dom"]),
  createElement("ul", [
    createElement("li", { key: 1 }, ["li1"]),
    createElement("li", { key: 2 }, ["li2"]),
    createElement("li", { key: 3 }, ["li3"]),
    createElement("li", { key: 4 }, ["li4"])
  ])
]);

// 2. 通過虛擬DOM構(gòu)建真正的DOM
const root = tree.render();
document.body.appendChild(root);

運(yùn)行之后能正常得出結(jié)果了,那么第一步驟算是完成了,具體還有更多不同類型標(biāo)簽,對應(yīng)事件狀態(tài)先略過.

界面如圖

Javascript結(jié)構(gòu)如圖

結(jié)構(gòu)原型如下
相關(guān)代碼可以查看element.js

diff算法

這是整個實(shí)現(xiàn)里面最關(guān)鍵的一步,因?yàn)檫@決定了計(jì)算的速度和操作Dom的數(shù)量

我們創(chuàng)建新的Dom樹作對比

// 3. 生成新的虛擬DOM
const newTree = createElement("div", { id: "container" }, [
  createElement("h1", { style: "color: red" }, ["Title2"]),
  createElement("h3", ["Hello, virtual-dom"]),
  createElement("ul", [
    createElement("li", { key: 3 }, ["li3"]),
    createElement("li", { key: 1 }, ["li1"]),
    createElement("li", { key: 2 }, ["li2"]),
    createElement("li", { key: 5 }, ["li5"])
  ])
]);

Javascript結(jié)構(gòu)如圖

tree diff

傳統(tǒng) diff 算法的復(fù)雜度為 O(n^3),但是一般Dom跨層級的情況是非常少見的。愛掏網(wǎng) - it200.com所以React 只針對同層級Dom節(jié)點(diǎn)做比較,將 O(n^3) 復(fù)雜度的問題轉(zhuǎn)換成 O(n) 復(fù)雜度的問題。愛掏網(wǎng) - it200.com

比較大的問題就是當(dāng)節(jié)點(diǎn)跨層級移動并不會進(jìn)行移動而是直接替換整個節(jié)點(diǎn),所以切記這點(diǎn)性能問題

component diff

  • 某個組件發(fā)生變化,會導(dǎo)致自其從上往下整體替換
  • 同一類型組件會進(jìn)行Virtual DOM進(jìn)行比較
  • React提供了一個shouldComponentUpdate決定是否更新

盡可能將動態(tài)組件往底層節(jié)點(diǎn)遷移,有利于提高性能

element diff

元素操作無非就是幾種,我們定義幾個類型做狀態(tài)標(biāo)記

const REPLACE = "replace";
const REORDER = "reorder";
const PROPS = "props";
const TEXT = "text";
const NOKEY = "no_key"

export {
  REPLACE,
  REORDER,
  PROPS,
  TEXT,
  NOKEY
}

其中NOKEY就是專門給那些沒有定義key的組件做默認(rèn),React對同一層級的同組子節(jié)點(diǎn),添加唯一 key 進(jìn)行區(qū)分進(jìn)行位移而不是直接替換,這點(diǎn)對于整體性能尤為關(guān)鍵

我們首先針對不同類型做些區(qū)分處理

import { isString, objForEach, aryForEach, isNotEmptyObj } from "./util";
import { REPLACE, REORDER, PROPS, TEXT } from "./common";
import listDiff from "list-diff2";

/**
 *
 * @param {舊Dom樹} oTree
 * @param {新Dom樹} nTree
 * 返回差異記錄
 */
function diff(oTree, nTree) {
  // 節(jié)點(diǎn)位置
  let index = 0;
  // 差異記錄
  const patches = {};
  dfsWalk(oTree, nTree, index, patches);
  return patches;
}

function dfsWalk(oNode, nNode, index, patches) {
  const currentPatch = [];

  // 首次渲染
  if (nNode === null) return;

  // 都是字符串形式并且不相同直接替換文字
  if (isString(oNode) && isString(nNode)) {
    oNode !== nNode &&
      currentPatch.push({
        type: TEXT,
        content: nNode
      });
    // 同種標(biāo)簽并且key相同
  } else if (oNode.tagName === nNode.tagName && oNode.key === nNode.key) {
    // 至少一方有值
    if (isNotEmptyObj(oNode.props) || isNotEmptyObj(nNode.props)) {
      // 計(jì)算props結(jié)果
      const propsPatches = diffProps(oNode, nNode);
      // 有差異則重新排序
      propsPatches &&
        currentPatch.push({
          type: PROPS,
          props: propsPatches
        });
    }
    // children對比
    if (
      !(!isNotEmptyObj(nNode.props) && nNode.props.hasOwnProperty("ignore"))
    ) {
      (oNode.children.length || nNode.children.length) &&
        diffChildren(
          oNode.children,
          nNode.children,
          index,
          patches,
          currentPatch
        );
    }
  } else {
    // 都不符合上面情況就直接替換
    currentPatch.push({ type: REPLACE, node: nNode });
  }

  // 最終對比結(jié)果
  currentPatch.length && (patches[index] = currentPatch);
}

新舊節(jié)點(diǎn)的props屬性比較

/**
 *
 * @param {舊節(jié)點(diǎn)} oNode
 * @param {新節(jié)點(diǎn)} nNode
 */
function diffProps(oNode, nNode) {
  let isChange = false;
  const oProps = oNode.props;
  const nProps = nNode.props;
  // 節(jié)點(diǎn)屬性記錄
  const propsPatched = {};

  // 替換/新增屬性
  objForEach(oProps, key => {
    if (nProps[key] !== oProps[key] || !oProps.hasOwnProperty(key)) {
      !isChange && (isChange = true);
      propsPatched[key] = nProps[key];
    }
  });

  return !isChange "code" >/**
 *  同級對比
 * @param {*} oChildren
 * @param {*} nChildren
 * @param {*} index
 * @param {*} patches
 * @param {*} currentPatch
 */
function diffChildren(oChildren, nChildren, index, patches, currentPatch) {
  // 得出相對簡化移動路徑
  const diffs = listDiff(oChildren, nChildren, "key");

  // 保留元素
  nChildren = diffs.children;

  // 記錄排序位移
  diffs.moves.length &&
    currentPatch.push({ type: REORDER, moves: diffs.moves });

  // 深度遍歷
  let leftNode = null;
  let currentNodeIndex = index;
  aryForEach(oChildren, (_item, _index) => {
    const nChild = nChildren[_index];
    currentNodeIndex =
      leftNode && leftNode.count
        ? currentNodeIndex + leftNode.count + 1
        : currentNodeIndex + 1;
    _item !== nChild && dfsWalk(_item, nChild, currentNodeIndex, patches);
    leftNode = _item;
  });
}

深度遍歷的原型圖如下

其中的listDiff來自于list-diff,能通過關(guān)鍵屬性獲得最小移動量,moves就是給第三步更新視圖做鋪墊指示,官方介紹如下

Diff two lists in time O(n). I The algorithm finding the minimal amount of moves is Levenshtein distance which is O(n*m). This algorithm is not the best but is enougth for front-end DOM list manipulation.

This project is mostly influenced by virtual-dom algorithm.

調(diào)用對比方式

// 4. 比較兩棵虛擬DOM樹的不同
const patches = diff(tree, newTree);

得出差異如下
相關(guān)代碼可以查看diff.js

更新視圖

進(jìn)行深度遍歷

import {
  isString,
  isObject,
  objForEach,
  aryForEach,
  setAttr,
  toArray
} from "./util";
import { REPLACE, REORDER, PROPS, TEXT, NOKEY } from "./common";

function patch(node, patches) {
  const walker = { index: 0 };
  dfsWalk(node, walker, patches);
}

// 深度遍歷更新
function dfsWalk(node, walker, patches) {
  const currentPatches = patches[walker.index];

  node.childNodes &&
    aryForEach(node.childNodes, item => {
      walker.index++;
      dfsWalk(item, walker, patches);
    });

  currentPatches && applyPatches(node, currentPatches);
}

針對不同標(biāo)志做對應(yīng)處理

// 更新類型
function applyPatches(node, currentPatches) {
  aryForEach(currentPatches, item => {
    switch (item.type) {
      case REPLACE:
        const nNode = isString(item.node)
          "code" >// 修改屬性
function setProps(node, props) {
  objForEach(props, key => {
    if (props[key] === void NOKEY) {
      node.removeAttribute(key);
    } else {
      setAttr(node, key, props[key]);
    }
  });
}

最后就是列表渲染

// 列表排序渲染
function reorderChildren(node, moves) {
  const staticNodeList = toArray(node.childNodes);
  const maps = {};

  aryForEach(staticNodeList, node => {
    // Element
    if (node.nodeType === 1) {
      const key = node.getAttribute("key");
      key && (maps[key] = node);
    }
  });

  aryForEach(moves, move => {
    const index = move.index;
    // 0:刪除 1:替換
    if (move.type === 0) {
      // 找到對應(yīng)節(jié)點(diǎn)刪除
      staticNodeList[index] === node.childNodes[index] &&
        node.removeChild(node.childNodes[index]);
      staticNodeList.splice(index, 1);
    } else if (move.type === 1) {
      let insertNode;
      if (maps[move.item.key]) {
        // 刪除并返回節(jié)點(diǎn)
        insertNode = node.removeChild(maps[move.item.key]);
        // 獲取刪除節(jié)點(diǎn)位置
        staticNodeList.splice(Array.prototype.indexOf.call(node.childNodes, maps[move.item.key]), 1);
      } else {
        // 創(chuàng)建節(jié)點(diǎn)
        insertNode = isObject(move.item)
          ? move.item.render()
          : document.createTextNode(move.item);
      }
      // 同步staticNodeList信息
      staticNodeList.splice(index, 0, insertNode);
      // 操作Dom
      node.insertBefore(insertNode, node.childNodes[index] || null);
    }
  });
}

export default patch;

當(dāng)這一步完成以后我們可以直接應(yīng)用查看效果

// 4. 比較兩棵虛擬DOM樹的不同
const patches = diff(tree, newTree);

// 5. 在真正的DOM元素上應(yīng)用變更
patch(root, patches);

結(jié)果如圖
相關(guān)代碼可以查看patch.js

參考

深度剖析:如何實(shí)現(xiàn)一個 Virtual DOM 算法

聲明:所有內(nèi)容來自互聯(lián)網(wǎng)搜索結(jié)果,不保證100%準(zhǔn)確性,僅供參考。如若本站內(nèi)容侵犯了原著者的合法權(quán)益,可聯(lián)系我們進(jìn)行處理。
發(fā)表評論
更多 網(wǎng)友評論0 條評論)
暫無評論

返回頂部

主站蜘蛛池模板: a级成人毛片完整版| 亚洲欧美日韩三级| 粉嫩国产白浆在线播放 | 黄网站免费在线| 一级毛片免费播放男男| 色综合久久综合欧美综合网| 日韩精品无码一本二本三本色| 国产精品jizz在线观看网站| 亚洲理论精品午夜电影| 91精品国产免费久久久久久青草| 深夜a级毛片免费视频| 在线精品国产一区二区三区| 亚洲色四在线视频观看| fc2成年免费共享视频18| 特级毛片www| 国内精品视频一区二区三区八戒| 国产精品无码不卡一区二区三区 | 四虎影院国产精品| 尤物193yw在线看| 国产综合久久久久久鬼色| 亚洲欧美中文字幕| 金8国欧美系列在线| 最近高清中文在线国语视频完整版| 国产精品久久久久久影视| 亚洲av无码国产综合专区| 黄色网址免费大全| 日本免费无遮挡吸乳视频电影| 国产一区二区三区福利| 中国嫩模一级毛片| 精品不卡一区二区| 国内露脸中年夫妇交换视频| 亚洲国产精品ⅴa在线观看 | 好男人好资源在线影视官网| 亚洲黄色在线观看| 5252色欧美在线男人的天堂| 欧美一欧美一区二三区性| 国产成人无码av在线播放不卡| 久久久精品电影| 精品无码一区二区三区| 天天干视频在线| 亚洲伊人精品综合在合线|