javascript --- > Vue初始化 && 模板渲染

不帶響應式的Vue縮減實現

模板

現有模板如下:

<div id ="app">
    <div class="c1">
        <div title='tt1' id="id">{{ name }}</div>
        <div title='tt2' >{{age}}</div>
        <div>hello3</div>
    </div>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
    </ul>
</div>
<script>
    let app = new Vue({
        el: '#app',
        data:{
            name: '張三',
            age: 19
        }
    })
</script>

Vue初始化流程

Vue的初始化,是從new Vue開始的,以下的圖中可以知道在new Vue後,會執行init,再$mount實現掛載,再到編譯compile,生成render函數,接下來是響應式依賴收集,通過pach實現異步更新。render function會被轉化爲Vnode節點,Virtual DOM是一棵以JavaScript對象(Vnode節點)爲基礎的樹。是對真實DOM的描述。通過patch()轉化爲真實DOM。在數據有變化時,會通過setter -> Watcher -> update來更新視圖。整個Vue的運行機制大致就是這樣

img

實現

  • 在這裏實現new Vue -> $mount -> compile -> render function -> Virtual DOM Tree -> patch() -> DOM,即除了響應式的部分.

  • 簡略版

【流程梳理】:

  • 首先要明確目的,我們需要將現有的HTML模板與數據結合,生成一個新的HTML結構,並渲染到頁面上.考慮到性能問題,我們首先將模板讀取到內存中(源代碼是進行HTML解析,生成一棵抽象AST).在這裏使用帶mustcache語法的HTML模板代替.

  • 首先是執行new Vue,在Vue函數中會將傳入的數據和模板保存起來,爲了後續的方便,會將模板及其父元素也保存起來,然後執行mount

function Vue(options){
    let elm = document.querySelector()
    this._data = options.data
    this._template = elm
    this._parent = elm.parentNode
    this.mount()
}
  • 然後是mount函數,在裏面做了2件事:
    • 第一件事是將HTML讀取爲AST保存在內存中,並返回一個根據AST 和 data 生成 虛擬DOM的render函數
    • 第二件事是調用mountComponent: 將render函數生成的VNode(虛擬DOM)轉換成真實的HTML節點渲染到頁面上

【先看第一件事】

Vue.prototype.mount = function(){
    this.render = this.createRenderFn()
}
Vue.prototype.createRenderFn = function(){
    let AST = getVNode(this._template)
    return function render(){
        let _tmp = combine(AST, this._data)
        return _tmp
    }
}

上面在mount中調用了createRenderFn,生成了一個render函數(AST + DATA -> VNode). 之所以寫出那種形式,

是因爲AST僅在一開始讀取DOM結構時候就固定不變了,採用上面的寫法可以提高性能.

getVNode函數根據模板,返回帶mustache語法的虛擬DOM.更多參考

class VNode {
    constructor(tag ,data, value, type){
        this.tag = tag && tag.toLowerCase()
        this.data = data
        this.value = value
        this.type = type
        this.children = []
    }
    appendChild(vnode){
        this.children.push(vnode)
    }
}
function getVNode(node){
    let nodeType = node.nodeType
    let _vnode = null
    if(nodeType == 1){
        // 元素節點
        let tag = node.nodeName
        ,attrs = node.attributes
        ,_data = {}
        for(let i = 0, len = attrs.length; i < len; i++){
            _data[attrs[i].nodeName] = attrs[i].nodeValue
        }
        _vnode = new VNode(tag, _data, undefined, nodeType)
        
        // 考慮子元素
        let childNodes = node.childNodes;
        for(let i = 0, len = childNodes.length; i< len; i++){
            _vnode.appendChild(getVNode(childNodes[i]))
        }
    } else if(nodeType == 3){
        // 文本節點
        _vnode = new VNode(undefined, undefined, node.nodeValue, nodeType)
    }
    return _vnode
}

此時得到的是一個對象,這個對象中的值類似{{name}}(模擬了AST),下面使用combine將該對象模板與數據結合生成一個新的對象(在Vue中是虛擬的DOM)。即將mustache語法用真實的數據替換

function combine(vnode ,data){
    let _type = vnode.type
    , _data = vnode.data
    , _tag = vnode.tag
    , _value = vnode.value
    , _children = vnode.children
    , _vnode = null
    
    if(_type == 3){
        // 文本節點
        _value = _value.replace(/\{\{(.+?)\}\}/g, function(_, g){
            return getValueByPath(data, g.trim())
        })
        _vnode = new VNode(_tag, _data, _value, _type)
    } else if(_type == 1){
        // 元素節點
        _vnode = new VNode(_tag, _data, _value, _type)
        _children.forEach(_subVNode => _vnode.appendChild(combine(_subVNode, data)))
    }
    return _vnode
}
// getValueByPath,深層次獲取對象的數據. 栗子: 獲取 a.name.age.salary
function getValueByPath(obj, path){
    let res=obj
    , currProp
    , props = path.join('.')
    while(currProp = props.shift()){
        res = res[props]
    }
    return res
}

【再看第二件事】

mountComponent中會使用第一件事中的render函數將AST和Data結合起來生成虛擬DOM,然後調用this.update方法將虛擬DOM渲染到頁面上

Vue.prototype.mountComponent = function(){
    let mount = () => {
        this.update(this.render())
    }
    mount.call(this)
}
// 之所以採用this.update,是因爲update後面會交付給watcher來調用的
Vue.prototype.update = function (vnode){
    let realDOM = parseVNode(vnode)
    this._parent.replaceChild(realDOM, this._template)
}
function parseVNode(vnode){
    let type = vnode.type
    , _node = null
    if(type ==3){
        return document.createTextNode(vnode.value)
    } else if (type == 1){
        _node = document.createElement(vnode.tag)
        let data = vnode.data
        Object.keys(data).forEach(key => {
            _node.setAttribute(key, data[key])
        })
        
        let children = vnode.children
        children.forEach(subvnode =>{
            _node.appendChild(parseNode(subvnode))
        })
    }
    return _node
}

整體代碼

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <div class="c1">
        <div title="tt1" id="id">{{ name }}</div>
        <div title="tt2">{{age}}</div>
        <div>hello3</div>
        <ul>
          <li>1</li>
          <li>2</li>
          <li>3</li>
        </ul>
      </div>
    </div>

    <script>
      /* 虛擬DOM 構造函數 */
      class VNode {
        constructor(tag, data, value, type) {
          this.tag = tag && tag.toLowerCase()
          this.data = data
          this.value = value
          this.type = type
          this.children = []
        }
        appendChild(vnode) {
          this.children.push(vnode)
        }
      }

      /* HTML DOM -> VNode(帶坑的Vnode): 將這個函數當做 compiler 函數  */
      /*
         Vue中會將真實的DOM結構當作字符串去解析得到一棵 AST
         此處使用帶有mustache語法的虛擬DOM來代替 AST
      */
      function getVNode(node) {
        let nodeType = node.nodeType
        let _vnode = null
        if (nodeType == 1) {
          // 元素
          let nodeName = node.nodeName
          let attrs = node.attributes
          let _attrObj = {}
          for (let i = 0; i < attrs.length; i++) {
            _attrObj[attrs[i].nodeName] = attrs[i].nodeValue
          }
          _vnode = new VNode(nodeName, _attrObj, undefined, nodeType)

          // 考慮node的子元素
          let childNodes = node.childNodes
          for (let i = 0; i < childNodes.length; i++) {
            _vnode.appendChild(getVNode(childNodes[i]))
          }
        } else if (nodeType == 3) {
          _vnode = new VNode(undefined, undefined, node.nodeValue, nodeType)
        }
        return _vnode
      }

      /* 將虛擬DOM轉換成真正的DOM */
      function parseVNode(vnode){
        // 創建真實的DOM
        let type = vnode.type;
        let _node = null;
        if( type == 3){
          return document.createTextNode(vnode.value)
        } else if(type == 1){
          _node = document.createElement(vnode.tag)

          // 屬性
          let data = vnode.data  // 現在這個data是鍵值對
          Object.keys(data).forEach((key)=>{
            let attrName = key
            let attrValue = data[key]
            _node.setAttribute(attrName, attrValue)
          })

          // 子元素
          let children = vnode.children;
          children.forEach(subvnode =>{
            _node.appendChild(parseVNode(subvnode))
          })
          return _node

        }
      }



      const mustache = /\{\{(.+?)\}\}/g // 匹配{{}}的正則表達式

      // 根據路徑訪問對象成員
      function getValueByPath(obj, path) {
        let res = obj,
          currProp,
          props = path.split('.')
        while ((currProp = props.shift())) {
          res = res[currProp]
        }
        return res
      }

      /*
        模擬 AST -> VNode 的過程
        將帶有坑(mustache語法)的VNode與數據data結合,得到填充數據的VNode:
      */
      function combine(vnode, data) {
        let _type = vnode.type
        let _data = vnode.data
        let _tag = vnode.tag
        let _value = vnode.value
        let _children = vnode.children

        let _vnode = null
        if (_type == 3) {
          // 文本節點
          // 對文本處理
          _value = _value.replace(mustache, function(_, g) {
            return getValueByPath(data, g.trim())
          })
          _vnode = new VNode(_tag, _data, _value, _type)
        } else if (_type == 1) {
          // 元素節點
          _vnode = new VNode(_tag, _data, _value, _type)
          _children.forEach(_subVNode => _vnode.appendChild(combine(_subVNode, data)))
        }
        return _vnode
      }

      function JGVue(options) {
        // this._options = options;
        this._data = options.data
        let elm = document.querySelector(options.el)
        this._template = elm
        this._parent = elm.parentNode
        this.mount() // 掛載
      }

      JGVue.prototype.mount = function() {
        // 需要提供一個render方法: 生成虛擬DOM
        // if(typeof this._options.render !== 'function'){

        // }

        this.render = this.createRenderFn() // 帶有緩存

        this.mountComponent()
      }

      JGVue.prototype.mountComponent = function() {
        // 執行mountComponent()
        let mount = () => {
          // update將虛擬DOM渲染到頁面上
          this.update(this.render())
        }
        mount.call(this) // 本質上應該交給 watcher 來調用

        // 爲什麼
        // this.update(this.render())  // 使用發佈訂閱模式,渲染和計算的行爲應該交給watcher來完成
      }

      /*
        在真正的Vue中,使用了二次提交的設計結構
        第一次提交是在內存中,在內存中確定沒有問題了在修改硬盤中的數據
        1. 在頁面中的DOM和虛擬DOM是一一對應的關係
      */

      // 這裏是生成render函數,目的是緩存抽象語法樹(我們使用虛擬DOM來模擬)
      JGVue.prototype.createRenderFn = function() {
        let AST = getVNode(this._template)
        // 將 AST + data => VNode
        // 我們: 帶坑的VNode + data => 含有數據的 VNode
        return function render() {
          // 將帶坑的VNode轉換爲真正帶數據的VNode
          let _tmp = combine(AST, this._data)
          return _tmp
        }
      }

      // 將虛擬DOM薰染到頁面中: diff算法就在這裏
      JGVue.prototype.update = function(vnode) {
        // 簡化,直接生成HTML DOM replaceChild 到頁面中
        // 父元素.replaceChild(新元素,舊元素)
        let realDOM = parseVNode(vnode)
        // debugger
        this._parent.replaceChild(realDOM, document.querySelector('#app'))
        // 這個算法是不負責任的
        // 每次都會將頁面中的DOM全部替換
      }

      let app = new  ({
        el: '#app',
        data: {
          name: '張三',
          age: 19
        }
      })
    </script>
  </body>
</html>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章