前端面試題(二)框架篇

MVVM

MVVM 由以下三個(gè)內(nèi)容組成

  • View:界面
  • Model:數(shù)據(jù)模型
  • ViewModel:作為橋梁負(fù)責(zé)溝通 View 和 Model

在 JQuery 時(shí)期稿械,如果需要刷新 UI 時(shí),需要先取到對(duì)應(yīng)的 DOM 再更新 UI冲粤,這樣數(shù)據(jù)和業(yè)務(wù)的邏輯就和頁(yè)面有強(qiáng)耦合美莫。

在 MVVM 中,UI 是通過數(shù)據(jù)驅(qū)動(dòng)的梯捕,數(shù)據(jù)一旦改變就會(huì)相應(yīng)的刷新對(duì)應(yīng)的 UI厢呵,UI 如果改變,也會(huì)改變對(duì)應(yīng)的數(shù)據(jù)傀顾。這種方式就可以在業(yè)務(wù)處理中只關(guān)心數(shù)據(jù)的流轉(zhuǎn)襟铭,而無需直接和頁(yè)面打交道。ViewModel 只關(guān)心數(shù)據(jù)和業(yè)務(wù)的處理,不關(guān)心 View 如何處理數(shù)據(jù)蝌矛,在這種情況下,View 和 Model 都可以獨(dú)立出來错英,任何一方改變了也不一定需要改變另一方入撒,并且可以將一些可復(fù)用的邏輯放在一個(gè) ViewModel 中,讓多個(gè) View 復(fù)用這個(gè) ViewModel椭岩。

在 MVVM 中茅逮,最核心的也就是數(shù)據(jù)雙向綁定,例如 Angluar 的臟數(shù)據(jù)檢測(cè)判哥,Vue 中的數(shù)據(jù)劫持献雅。

臟數(shù)據(jù)檢測(cè)

當(dāng)觸發(fā)了指定事件后會(huì)進(jìn)入臟數(shù)據(jù)檢測(cè),這時(shí)會(huì)調(diào)用 $digest 循環(huán)遍歷所有的數(shù)據(jù)觀察者塌计,判斷當(dāng)前值是否和先前的值有區(qū)別挺身,如果檢測(cè)到變化的話,會(huì)調(diào)用 $watch 函數(shù)锌仅,然后再次調(diào)用 $digest 循環(huán)直到發(fā)現(xiàn)沒有變化章钾。循環(huán)至少為二次 ,至多為十次热芹。

臟數(shù)據(jù)檢測(cè)雖然存在低效的問題贱傀,但是不關(guān)心數(shù)據(jù)是通過什么方式改變的,都可以完成任務(wù)伊脓,但是這在 Vue 中的雙向綁定是存在問題的府寒。并且臟數(shù)據(jù)檢測(cè)可以實(shí)現(xiàn)批量檢測(cè)出更新的值,再去統(tǒng)一更新 UI报腔,大大減少了操作 DOM 的次數(shù)株搔。所以低效也是相對(duì)的,這就仁者見仁智者見智了榄笙。

數(shù)據(jù)劫持

Vue 內(nèi)部使用了 Object.defineProperty() 來實(shí)現(xiàn)雙向綁定邪狞,通過這個(gè)函數(shù)可以監(jiān)聽到 setget 的事件。

var data = { name: 'yck' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value

function observe(obj) {
  // 判斷類型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

function defineReactive(obj, key, val) {
  // 遞歸子屬性
  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
    }
  })
}

以上代碼簡(jiǎn)單的實(shí)現(xiàn)了如何監(jiān)聽數(shù)據(jù)的 setget 的事件茅撞,但是僅僅如此是不夠的帆卓,還需要在適當(dāng)?shù)臅r(shí)候給屬性添加發(fā)布訂閱

<div>
    {{name}}
</div>

::: v-pre
在解析如上模板代碼時(shí),遇到 {{name}} 就會(huì)給屬性 name 添加發(fā)布訂閱米丘。
:::

// 通過 Dep 解耦
class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    // sub 是 Watcher 實(shí)例
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
// 全局屬性剑令,通過該屬性配置 Watcher
Dep.target = null

function update(value) {
  document.querySelector('div').innerText = value
}

class Watcher {
  constructor(obj, key, cb) {
    // 將 Dep.target 指向自己
    // 然后觸發(fā)屬性的 getter 添加監(jiān)聽
    // 最后將 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 獲得新值
    this.value = this.obj[this.key]
    // 調(diào)用 update 方法更新 Dom
    this.cb(this.value)
  }
}
var data = { name: 'yck' }
observe(data)
// 模擬解析到 `{{name}}` 觸發(fā)的操作
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy' 

接下來,對(duì) defineReactive 函數(shù)進(jìn)行改造

function defineReactive(obj, key, val) {
  // 遞歸子屬性
  observe(val)
  let dp = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      // 將 Watcher 添加到訂閱
      if (Dep.target) {
        dp.addSub(Dep.target)
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
      // 執(zhí)行 watcher 的 update 方法
      dp.notify()
    }
  })
}

以上實(shí)現(xiàn)了一個(gè)簡(jiǎn)易的雙向綁定,核心思路就是手動(dòng)觸發(fā)一次屬性的 getter 來實(shí)現(xiàn)發(fā)布訂閱的添加拄查。

Proxy 與 Object.defineProperty 對(duì)比

Object.defineProperty 雖然已經(jīng)能夠?qū)崿F(xiàn)雙向綁定了吁津,但是他還是有缺陷的。

  1. 只能對(duì)屬性進(jìn)行數(shù)據(jù)劫持,所以需要深度遍歷整個(gè)對(duì)象
  2. 對(duì)于數(shù)組不能監(jiān)聽到數(shù)據(jù)的變化

雖然 Vue 中確實(shí)能檢測(cè)到數(shù)組數(shù)據(jù)的變化碍脏,但是其實(shí)是使用了 hack 的辦法梭依,并且也是有缺陷的。

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// hack 以下幾個(gè)函數(shù)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // 獲得原生函數(shù)
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    // 調(diào)用原生函數(shù)
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 觸發(fā)更新
    ob.dep.notify()
    return result
  })
})

反觀 Proxy 就沒以上的問題典尾,原生支持監(jiān)聽數(shù)組變化役拴,并且可以直接對(duì)整個(gè)對(duì)象進(jìn)行攔截,所以 Vue 也將在下個(gè)大版本中使用 Proxy 替換 Object.defineProperty

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      setBind(value);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
  value = v
}, (target, property) => {
  console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2

路由原理

前端路由實(shí)現(xiàn)起來其實(shí)很簡(jiǎn)單钾埂,本質(zhì)就是監(jiān)聽 URL 的變化河闰,然后匹配路由規(guī)則,顯示相應(yīng)的頁(yè)面褥紫,并且無須刷新姜性。目前單頁(yè)面使用的路由就只有兩種實(shí)現(xiàn)方式

  • hash 模式
  • history 模式

www.test.com/#/ 就是 Hash URL,當(dāng) # 后面的哈希值發(fā)生變化時(shí)髓考,不會(huì)向服務(wù)器請(qǐng)求數(shù)據(jù)部念,可以通過 hashchange 事件來監(jiān)聽到 URL 的變化,從而進(jìn)行跳轉(zhuǎn)頁(yè)面氨菇。

image

History 模式是 HTML5 新推出的功能印机,比之 Hash URL 更加美觀

image

Virtual Dom

代碼地址

為什么需要 Virtual Dom

眾所周知,操作 DOM 是很耗費(fèi)性能的一件事情门驾,既然如此射赛,我們可以考慮通過 JS 對(duì)象來模擬 DOM 對(duì)象,畢竟操作 JS 對(duì)象比操作 DOM 省時(shí)的多奶是。

舉個(gè)例子

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

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

如果以上操作對(duì)應(yīng)到 DOM 中,那么就是以下代碼

// 刪除第三個(gè) li
ul.childNodes[2].remove()
// 將第四個(gè) li 和第五個(gè)交換位置
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)

當(dāng)然在實(shí)際操作中及汉,我們還需要給每個(gè)節(jié)點(diǎn)一個(gè)標(biāo)識(shí)沮趣,作為判斷是同一個(gè)節(jié)點(diǎn)的依據(jù)。所以這也是 Vue 和 React 中官方推薦列表里的節(jié)點(diǎn)使用唯一的 key 來保證性能坷随。

那么既然 DOM 對(duì)象可以通過 JS 對(duì)象來模擬房铭,反之也可以通過 JS 對(duì)象來渲染出對(duì)應(yīng)的 DOM

以下是一個(gè) JS 對(duì)象模擬 DOM 對(duì)象的簡(jiǎn)單實(shí)現(xiàn)

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 = tag
    this.props = props
    if (Array.isArray(children)) {
      this.children = children
    } else if (isString(children)) {
      this.key = children
      this.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)
  }
  // 創(chuàng)建節(jié)點(diǎn)
  _createElement(tag, props, child, key) {
    // 通過 tag 創(chuàng)建節(jié)點(diǎn)
    let el = document.createElement(tag)
    // 設(shè)置節(jié)點(diǎn)屬性
    for (const key in props) {
      if (props.hasOwnProperty(key)) {
        const value = props[key]
        el.setAttribute(key, value)
      }
    }
    if (key) {
      el.setAttribute('key', key)
    }
    // 遞歸添加子節(jié)點(diǎn)
    if (child) {
      child.forEach(element => {
        let child
        if (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 算法簡(jiǎn)述

既然我們已經(jīng)通過 JS 來模擬實(shí)現(xiàn)了 DOM,那么接下來的難點(diǎn)就在于如何判斷舊的對(duì)象和新的對(duì)象之間的差異温眉。

DOM 是多叉樹的結(jié)構(gòu)缸匪,如果需要完整的對(duì)比兩顆樹的差異,那么需要的時(shí)間復(fù)雜度會(huì)是 O(n ^ 3)类溢,這個(gè)復(fù)雜度肯定是不能接受的凌蔬。于是 React 團(tuán)隊(duì)優(yōu)化了算法,實(shí)現(xiàn)了 O(n) 的復(fù)雜度來對(duì)比差異。

實(shí)現(xiàn) O(n) 復(fù)雜度的關(guān)鍵就是只對(duì)比同層的節(jié)點(diǎn)砂心,而不是跨層對(duì)比懈词,這也是考慮到在實(shí)際業(yè)務(wù)中很少會(huì)去跨層的移動(dòng) DOM 元素。

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

  • 首先從上至下辩诞,從左往右遍歷對(duì)象钦睡,也就是樹的深度遍歷,這一步中會(huì)給每個(gè)節(jié)點(diǎn)添加索引躁倒,便于最后渲染差異
  • 一旦節(jié)點(diǎn)有子元素,就去判斷子元素是否有不同

Virtual Dom 算法實(shí)現(xiàn)

樹的遞歸

首先我們來實(shí)現(xiàn)樹的遞歸算法洒琢,在實(shí)現(xiàn)該算法前秧秉,先來考慮下兩個(gè)節(jié)點(diǎn)對(duì)比會(huì)有幾種情況

  1. 新的節(jié)點(diǎn)的 tagName 或者 key 和舊的不同,這種情況代表需要替換舊的節(jié)點(diǎn)衰抑,并且也不再需要遍歷新舊節(jié)點(diǎn)的子元素了象迎,因?yàn)檎麄€(gè)舊節(jié)點(diǎn)都被刪掉了
  2. 新的節(jié)點(diǎn)的 tagNamekey(可能都沒有)和舊的相同,開始遍歷子樹
  3. 沒有新的節(jié)點(diǎn)呛踊,那么什么都不用做
import { StateEnums, isString, move } from './util'
import Element from './element'

export default function diff(oldDomTree, newDomTree) {
  // 用于記錄差異
  let pathchs = {}
  // 一開始的索引為 0
  dfs(oldDomTree, newDomTree, 0, pathchs)
  return pathchs
}

function dfs(oldNode, newNode, index, patches) {
  // 用于保存子樹的更改
  let curPatches = []
  // 需要判斷三種情況
  // 1.沒有新的節(jié)點(diǎn)砾淌,那么什么都不用做
  // 2.新的節(jié)點(diǎn)的 tagName 和 `key` 和舊的不同,就替換
  // 3.新的節(jié)點(diǎn)的 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 {
    // 節(jié)點(diǎn)不同汪厨,需要替換
    curPatches.push({ type: StateEnums.Replace, node: newNode })
  }

  if (curPatches.length) {
    if (patches[index]) {
      patches[index] = patches[index].concat(curPatches)
    } else {
      patches[index] = curPatches
    }
  }
}

判斷屬性的更改

判斷屬性的更改也分三個(gè)步驟

  1. 遍歷舊的屬性列表,查看每個(gè)屬性是否還存在于新的屬性列表中
  2. 遍歷新的屬性列表愉择,判斷兩個(gè)列表中都存在的屬性的值是否有變化
  3. 在第二步中同時(shí)查看是否有屬性不存在與舊的屬性列列表中
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
}

判斷列表差異算法實(shí)現(xiàn)

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

  1. 遍歷舊的節(jié)點(diǎn)列表衷戈,查看每個(gè)節(jié)點(diǎn)是否還存在于新的節(jié)點(diǎn)列表中
  2. 遍歷新的節(jié)點(diǎn)列表,判斷是否有新的節(jié)點(diǎn)
  3. 在第二步中同時(shí)判斷節(jié)點(diǎn)是否有移動(dòng)

PS:該算法只對(duì)有 key 的節(jié)點(diǎn)做處理

function listDiff(oldList, newList, index, patches) {
  // 為了遍歷方便层坠,先取出兩個(gè) list 的所有 keys
  let oldKeys = getKeys(oldList)
  let newKeys = getKeys(newList)
  let changes = []

  // 用于保存變更后的節(jié)點(diǎn)數(shù)據(jù)
  // 使用該數(shù)組保存有以下好處
  // 1.可以正確獲得被刪除節(jié)點(diǎn)索引
  // 2.交換節(jié)點(diǎn)位置只需要操作一遍 DOM
  // 3.用于 `diffChildren` 函數(shù)中的判斷殖妇,只需要遍歷
  // 兩個(gè)樹中都存在的節(jié)點(diǎn),而對(duì)于新增或者刪除的節(jié)點(diǎn)來說破花,完全沒必要
  // 再去判斷一遍
  let list = []
  oldList &&
    oldList.forEach(item => {
      let key = item.key
      if (isString(item)) {
        key = item
      }
      // 尋找新的 children 中是否含有當(dāng)前節(jié)點(diǎn)
      // 沒有的話需要?jiǎng)h除
      let index = newKeys.indexOf(key)
      if (index === -1) {
        list.push(null)
      } else list.push(key)
    })
  // 遍歷變更后的數(shù)組
  let length = list.length
  // 因?yàn)閯h除數(shù)組元素是會(huì)更改索引的
  // 所有從后往前刪可以保證索引不變
  for (let i = length - 1; i >= 0; i--) {
    // 判斷當(dāng)前元素是否為空谦趣,為空表示需要?jiǎng)h除
    if (!list[i]) {
      list.splice(i, 1)
      changes.push({
        type: StateEnums.Remove,
        index: i
      })
    }
  }
  // 遍歷新的 list,判斷是否有節(jié)點(diǎn)新增或移動(dòng)
  // 同時(shí)也對(duì) `list` 做節(jié)點(diǎn)新增和移動(dòng)節(jié)點(diǎn)的操作
  newList &&
    newList.forEach((item, i) => {
      let key = item.key
      if (isString(item)) {
        key = item
      }
      // 尋找舊的 children 中是否含有當(dāng)前節(jié)點(diǎn)
      let index = list.indexOf(key)
      // 沒找到代表新節(jié)點(diǎn)座每,需要插入
      if (index === -1 || key == null) {
        changes.push({
          type: StateEnums.Insert,
          node: item,
          index: i
        })
        list.splice(i, 0, key)
      } else {
        // 找到了蔚润,需要判斷是否需要移動(dòng)
        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 text
  list &&
    list.forEach(item => {
      let key
      if (isString(item)) {
        key = [item]
      } else if (item instanceof Element) {
        key = item.key
      }
      keys.push(key)
    })
  return keys
}

遍歷子元素打標(biāo)識(shí)

對(duì)于這個(gè)函數(shù)來說,主要功能就兩個(gè)

  1. 判斷兩個(gè)列表差異
  2. 給節(jié)點(diǎn)打上標(biāo)記

總體來說尺栖,該函數(shù)實(shí)現(xiàn)的功能很簡(jiǎn)單

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
    }
  }
  // 記錄上一個(gè)遍歷過的節(jié)點(diǎn)
  let last = null
  oldChild &&
    oldChild.forEach((item, i) => {
      let child = item && item.children
      if (child) {
        index =
          last && last.children ? index + last.children.length + 1 : index + 1
        let keyIndex = list.indexOf(item.key)
        let node = newChild[keyIndex]
        // 只遍歷新舊中都存在的節(jié)點(diǎn)嫡纠,其他新增或者刪除的沒必要遍歷
        if (node) {
          dfs(item, node, index, patches)
        }
      } else index += 1
      last = item
    })
}

渲染差異

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

這個(gè)函數(shù)主要兩個(gè)功能

  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 += 1
  if (changes && changes.length && patchs[index]) {
    changeDom(node, changes)
  }
  let last = null
  if (childNodes && childNodes.length) {
    childNodes.forEach((item, i) => {
      index =
        last && last.children ? index + last.children.length + 1 : index + 1
      patch(item, patchs)
      last = item
    })
  }
}

function changeDom(node, changes, noChild) {
  changes &&
    changes.forEach(change => {
      let { type } = change
      switch (type) {
        case StateEnums.ChangeProps:
          let { props } = change
          props.forEach(item => {
            if (item.value) {
              node.setAttribute(item.prop, item.value)
            } else {
              node.removeAttribute(item.prop)
            }
          })
          break
        case StateEnums.Remove:
          node.childNodes[change.index].remove()
          break
        case StateEnums.Insert:
          let dom
          if (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])
          break
        case StateEnums.Replace:
          node.parentNode.replaceChild(change.node.create(), node)
          break
        case 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)
          break
        default:
          break
      }
    })
}

最后

Virtual Dom 算法的實(shí)現(xiàn)也就是以下三步

  1. 通過 JS 來模擬創(chuàng)建 DOM 對(duì)象
  2. 判斷兩個(gè)對(duì)象的差異
  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('結(jié)束更新')
}, 1000)

當(dāng)然目前的實(shí)現(xiàn)還略顯粗糙者蠕,但是對(duì)于理解 Virtual Dom 算法來說已經(jīng)是完全足夠的了窃祝。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市踱侣,隨后出現(xiàn)的幾起案子粪小,更是在濱河造成了極大的恐慌,老刑警劉巖抡句,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件探膊,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡待榔,警方通過查閱死者的電腦和手機(jī)逞壁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來锐锣,“玉大人腌闯,你說我怎么就攤上這事〉胥荆” “怎么了姿骏?”我有些...
    開封第一講書人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)斤彼。 經(jīng)常有香客問我工腋,道長(zhǎng),這世上最難降的妖魔是什么畅卓? 我笑而不...
    開封第一講書人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任擅腰,我火速辦了婚禮,結(jié)果婚禮上翁潘,老公的妹妹穿的比我還像新娘趁冈。我一直安慰自己,他們只是感情好拜马,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開白布渗勘。 她就那樣靜靜地躺著,像睡著了一般俩莽。 火紅的嫁衣襯著肌膚如雪旺坠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,713評(píng)論 1 312
  • 那天扮超,我揣著相機(jī)與錄音取刃,去河邊找鬼蹋肮。 笑死,一個(gè)胖子當(dāng)著我的面吹牛璧疗,可吹牛的內(nèi)容都是我干的坯辩。 我是一名探鬼主播,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼崩侠,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼漆魔!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起却音,我...
    開封第一講書人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤改抡,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后系瓢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體阿纤,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年八拱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涯塔。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡肌稻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出匕荸,到底是詐尸還是另有隱情爹谭,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布榛搔,位于F島的核電站诺凡,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏践惑。R本人自食惡果不足惜腹泌,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望尔觉。 院中可真熱鬧凉袱,春花似錦、人聲如沸侦铜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)钉稍。三九已至涤躲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間贡未,已是汗流浹背种樱。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工蒙袍, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缸托。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓左敌,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親俐镐。 傳聞我的和親對(duì)象是個(gè)殘疾皇子矫限,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 1、JS的數(shù)據(jù)類型只有浮點(diǎn)型佩抹,沒有整型叼风。null,underfined,boolean,number,string...
    6e5e50574d74閱讀 2,217評(píng)論 2 1
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,113評(píng)論 1 32
  • 堅(jiān)持,耐心 走過每一天 今天棍苹,和昨天一樣 今天无宿,和明天一樣 那些不同的每一天 需要我們勇敢去追逐 靜心去改變
    帆船夢(mèng)閱讀 172評(píng)論 0 3