mini-vue渲染的簡易實現

前言

目前的主流框架Vue、React 都是通過 Virtual Dom(虛擬Dom)來實現的,通過Virtual Dom技術提高頁面的渲染效率。Vue中我們通過在 template 模板中編寫html代碼,React中我們通過在內部的一個 render 函數裡編寫html代碼,這個函數通過 jsx 編譯後,實際會輸出一個h函數,也就是我們的 Virtual Dom(虛擬Dom),下面簡單來實現一個虛擬dom渲染真實dom,以及更新的方法。

目標

主要實現以下三個功能:

  • 通過h函數返回Vnodes;
  • 通過 mount 函數將 虛擬dom 掛載到真實節點上;
  • 通過 patch 函數通過 newVnodes 與 oldVnodes比較來實現dom的更新;

第一步:

在body標簽內創建一個id為app的節點,後面會將虛擬節點掛載到這個節點上,而renderer.js用來實現上面三個功能。

<body>
  <div id="app"></div>
  <script src="./renderer.js"></script>
</body>

 第二步:

編寫h函數,用來返回tag(標簽元素)、props(屬性對象)、children(子節點),簡單來說虛擬Dom就是一個普通javaScript對象。

// renderer.js
 
const h = (tag, props, children) => {
  return {
    tag,
    props,
    children
  }
}

那麼通過這個h函數,我們簡單來看看下面一段代碼會輸出什麼?

const vdom = h("div", {class: "header"}, [
  h("h2", null, "Hello World"),
  h("h2", {id: 0}, h("span", null, "啦啦啦啦")) // 當props沒有值,必須傳一個null,不能不傳
]);
console.log(vdom);

由下圖可以看出,通過h函數,給我們返回瞭一個javaScript對象,這個便是 Virtual Dom(虛擬Dom),形成樹狀節點。

 那麼我們拿到這個vnodes,如何掛載到真實節點上呢?下面我們來看看第三步

第三步:

我們先創建一個 mount 函數,需要傳入兩個參數,第一個是我們剛剛通過h函數返回的vnodes,第二個參數是我們需要將這些vnode掛載到哪個節點上,接下來看代碼:

const mount = (vnodes, app) => {
  // 通過vnodes裡面的tag值,比如("div", "h2"),創建一個節點
  // 同樣在vnodes對象裡保存一份真實dom,方便以後進行更新,新增等操作
  const el = vnodes.el = document.createElement(vnodes.tag); 
  // 拿到這個節點後,我們通過判斷props值,進行添加屬性
  if (vnodes.props) {
    for (let key in vnodes.props) {
      // 這兒通過拿到props中key值後,在做判斷
      let value = vnodes.props[key];
      if (key.startsWith('on')) {
        // 比如用戶寫瞭一個onClick="changeData",處理為監聽函數的事件
        el.addEventListener(key.slice(2).toLowerCase(), value)
      } else {
        el.setAttribute(key, value)
      }
      // 這下面還有些判斷,比如指令啊v-if等等,進行邊界化處理
    }
  }
  // 處理完props後,最後是Children節點
  if (vnodes.children) {
    if (typeof vnodes.children === 'string') {
      // 如果這兒是個字符串類型,那麼就可以直接添加到節點中去
      el.textContent = vnodes.children
    } else {
      // 這種情況為數組類型,含有子節點,通過遍歷,再生成子節點
      vnodes.children.forEach(vnode => {
        // 通過遞歸,再次將子節點掛載到當前節點上去
        mount(vnode, el)
      })
    }
  }
  // 最終將這個真實節點掛載到我們傳入的app節點中去
  app.appendChild(el);
}
const app = document.querySelector("#app")
mount(vdom, app)

我們來看看通過mount函數掛載的實際效果:

 那麼到這裡,我們就已經實現瞭通過虛擬dom來創建真實dom瞭,那在vue當中是如何對這些dom進行更新操作呢,接下來我們再創建一個 patch函數(更新):

第四步:

通過patch函數,我們需要傳入兩個參數(vnodes1、vnodes2),分別是新的虛擬dom和舊的虛擬dom,通過比較新舊虛擬dom,來指定更新哪些節點。(這兒不考慮key值,需要參考key值可以查看鏈接:https://www.jb51.net/article/219078.htm)

// patch函數 n1: 舊節點、 n2:新節點
// 在vue源碼中,舊vnode,新vnode分別用n1, n2表示
const patch = (n1, n2) => {
  // 在上面我們通過mount函數給n2添加瞭節點屬性el,綁定到n2上
  const el = n2.el = n1.el
  // 首先,還是從兩個中的tag入手
  if (n1.tag == n2.tag) {
    // n1、n2的tag相同,再對比props
    const n1Props = n1.props || {};
    const n2Props = n2.props || {};
    // 分別取到n1,n2中的props,進行比較
    for (let key in n2Props) {
      // 取出n2中所有key,判斷n2的key值和n1key值是否相同
      const n1Value = n1Props[key] || '';
      const n2Value = n2Props[key] || '';
      if (n1Value !== n2Value) {
        if (key.startsWith('on')) {
          // 比如用戶寫瞭一個onClick="changeData",處理為監聽函數的事件
          el.addEventListener(key.slice(2).toLowerCase(), n2Value)
        } else {
          el.setAttribute(key, n2Value)
        }
      }
      // 相同則不作處理
    }
    for (let key in n1Props) {
      const oldValue = n1Props[key];
      if (!(key in n2Props)) {
        if (key.startsWith('on')) {
          el.removeEventListener(key.slice(2).toLowerCase(), oldValue)
        } else {
          el.removeAttribute(key)
        }
      }
    }
  } else {
    // tag不同,拿到n1的父節點
    const n1Parent = n1.el.parentElement;
    // 通過removeChild將舊節點從父節點中移除,然後將n2掛載到父節點中
    n1Parent.removeChild(n1.el); //n1.el是通過mount函數往對象裡添加的真實dom節點
    mount(n2, n1Parent)
  }
  // 最後處理children,相對於來說復雜些
  // children可以為字符串也可以為數組,那麼先看字符串時怎麼處理
  const n1Children = n1.children || [];
  const n2Children = n2.children || [];
  if (typeof n2Children === "string") {
    // 如果新節點內容為字符串,直接使用innerhtml進行替換
    el.innerHtml = n2Children;
  } else {
    // 下面情況是n2.children為數組情況時
    if (typeof n1.children === "string") {
      // n1.children為字符串,n2.children為數組
      el.innerHtml = ''; // 先將節點內容情況,再講新的內容添加進去
      mount(n2.children, el)
    } else {
      // 兩種都為數組類型時,這兒不考慮key值
      const minLength = Math.min(n1Children.length, n2Children.length);
      for (let i = 0 ; i < minLength ; i++) {
        patch(n1Children[i], n2Children[i]);
      }
      if(n2Children.length > n1Children.length) {
        n2Children.slice(minLength).forEach(item => {
          mount(item, el)
        })
      }
      if(n2Children.length < n1Children.length) {
        n1Children.slice(minLength).forEach(item => {
          el.removeChild(item.el)
        })
      }
    }
  }
}

上面簡單的實現瞭patch的作用,其實就是我們說的diff算法(當然這兒沒有考慮key值的情況,隻能兩個依次比較),同一層級進行比較。現在模擬演示一下,看看是否能更新成功:

const vdom = h("div", {class: "header"}, [
  h("h2", null, "Hello World"),
  h("h2", {id: 0}, [h("span", null, "啦啦啦啦")]) // 當props沒有值,必須傳一個null,不能不傳
]);
const app = document.querySelector("#app")
mount(vdom, app)
setTimeout(()=> { // 3秒後向patch傳入新舊Vnodes
  const vdom1 = h("div", {class: "header"}, [
    h("h3", null, "Hello World"),
    h("span", null, "哈哈哈")
  ])
  patch(vdom, vdom1)
},3000)

 通過下圖,我們可以看到已經簡單的實現瞭虛擬dom更新節點。

總結

簡單的實現瞭下虛擬Dom生成真實節點,然後通過patch進行更新。再去看看源碼,就能更好的理解vue的渲染器是如何實現的瞭。

到此這篇關於mini-vue渲染的簡易實現的文章就介紹到這瞭,更多相關mini-vue渲染內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: