vue 源碼 -- 2.0.0原理

一、響應式原理揭祕
(1)頁面部分html

<body>
    <div id="app">
        <p>{{name}}</p>
        <p k-text="name"></p>
        <p>{{age}}</p>
        <p>
            {{doubleAge}}
        </p>
        <input type="text" k-model="name">
        <button @click="changeName">呵呵</button>
        <div k-html="html"></div>
    </div>
    <script src='./watcher.js'></script>
    <script src='./compile.js'></script>
    <script src='./kvue.js'></script>
    <script src='./dep.js'></script>

    <script>
      let kaikeba = new KVue({
        el:'#app',
        data: {
            name: "I am test.",
            age:12,
            html:'<button>這是一個按鈕</button>'
        },
        created(){
            console.log('開始啦')
            setTimeout(()=>{
                this.name='我是蝸牛'
            }, 15000) 
        },
        methods:{
          changeName(){
            this.name = '哈嘍,開課吧'
            this.age=1
            this.id = 'xx'
            console.log(1,this)
          }
    }
  })
    </script>
  </body>

(2)響應式實現代碼:
響應式原理口訣:
Vue定義每一個響應式數據時,都建立一個Dep依賴收集容器。
Compile編譯每一個綁定數據的節點時,都建立一個Watcher監聽數據變化。
數據-->視圖 綁定依靠Watcher。
視圖-->數據 綁定依靠addEventListener('input/change...')。vue

/**
 * 一、將數據丁意思成響應式
 * 二、用this.key 代理 this.$data.key的訪問
 */
class KVue {
  constructor(options) {
  this.$data = options.data
  this.$options = options
  this.observer(this.$data)

  if(options.created){
      options.created.call(this)
    }
    this.$compile = new Compile(options.el, this)
  }
  observer(value) {
    if (!value || (typeof value !== 'object')) {
      return
    }
    Object.keys(value).forEach((key) => {
      this.proxyData(key) // 用this.key 代理 this.$data.key的訪問
      this.defineReactive(value, key, value[key]) // 將數據丁意思成響應式
    })
  }
  defineReactive(obj, key, val) {
    debugger
    const dep = new Dep()
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 將Dep.target(即當前的Watcher對象存入Dep的deps中)
        Dep.target && dep.addDep(Dep.target)
        console.log(dep);
        return val
      },
      set(newVal) {
        debugger
        if (newVal === val) return
        val = newVal
        // 在set的時候觸發dep的notify來通知全部的Watcher對象更新視圖
        dep.notify()
      }
    })
  }
  proxyData(key) {
    Object.defineProperty(this, key, {
      configurable: false,
      enumerable: true,
      get() {
        return this.$data[key]
      },
      set(newVal) {
        this.$data[key] = newVal
      }
    })
  }
}


/**
 * 每一個數據定義成響應式時,都建立一個依賴收集容器
 * 在Compiler編譯器編譯文檔時,讀取數據(觸發數據get方法)時
 */
class Dep {
  constructor() {
    // 存數全部的依賴
    this.deps = []
  }
    // 在deps中添加一個監聽器對象 
  addDep(dep) {
    this.deps.push(dep)
  }
  depend() { // 貌似該方法沒用過
    Dep.target.addDep(this)
  }
    // 通知全部監聽器去更新視圖 
  notify() {
    this.deps.forEach((dep) => {
      dep.update()
    }) 
  }
}

/**
 * 編譯文檔,深度優先遍歷全部HTML節點,完成vm命名空間下 「節點初始賦值」 和 「節點數據雙向監聽綁定」
 * 數據 --> 視圖節點綁定: 爲監聽數據的節點建立Watcher(vm, key, cb),cb(node, value)更新視圖
 * 視圖節點 --> 數據綁定:爲輸入控件例如<input> 添加 node.addEventListener('input', cb), cb(vm, key)更新數據
*/
class Compile {
    constructor(el,vm) {
      this.$vm = vm
      this.$el = document.querySelector(el)
      if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el)
        this.compileElement(this.$fragment)
        this.$el.appendChild(this.$fragment)
      }
    }
    node2Fragment(el) {
        // 建立文檔片斷 DocumentFragment
        let fragment = document.createDocumentFragment()
        let child
        // 將原生節點拷貝到文檔片斷
        while (child = el.firstChild) {
            //  appendChild() 方法向節點添加最後一個子節點
            //  當子節點來源於原生節點時,appendChild() 方法從一個元素向另外一個元素中移動元素
            fragment.appendChild(child)
        }
        return fragment
    }

    compileElement(el) {
        let childNodes = el.childNodes
        Array.from(childNodes).forEach((node) => {
            let text = node.textContent
            // 表達式文本
            // 就是識別{{}}中的數據
            let reg = /\{\{(.*)\}\}/
            // 按元素節點方式編譯
            if (this.isElementNode(node)) {
                this.compile(node)
            } else if (this.isTextNode(node) && reg.test(text)) {
                // 文本 而且有{{}}
                this.compileText(node, RegExp.$1)
            }
            // 遍歷編譯子節點
            if (node.childNodes && node.childNodes.length) {
                this.compileElement(node)
            }
        })
    }

    compile(node) {
        let nodeAttrs = node.attributes
        Array.from(nodeAttrs).forEach( (attr)=>{
            // 規定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令爲 v-text
            let attrName = attr.name // v-text
            let exp = attr.value // content
            if (this.isDirective(attrName)) {
                let dir = attrName.substring(2) // text
                // 普通指令
                this[dir] && this[dir](node, this.$vm, exp)
            }
            if(this.isEventDirective(attrName)){
                let dir = attrName.substring(1) // text
                this.eventHandler(node, this.$vm, exp, dir)
            }
        })
    }
    compileText(node, exp) {
        this.text(node, this.$vm, exp)
    }
    isDirective(attr) {
        return attr.indexOf('k-') == 0
    }
    isEventDirective(dir) {
        return dir.indexOf('@') === 0
    }
    isElementNode(node) {
        return node.nodeType == 1
    }
    isTextNode(node) {
        return node.nodeType == 3
    }
    text(node, vm, exp) {
        this.update(node, vm, exp, 'text')
    }
    html(node, vm, exp) {
        this.update(node, vm, exp, 'html')
    }
    model(node, vm, exp) {
        this.update(node, vm, exp, 'model')
        node.addEventListener('input', (e)=>{
            let newValue = e.target.value
            vm[exp] = newValue
    }) }
    // 編譯發現節點有數據綁定時,執行對應updater(node, value)賦值
    // 並在vm命名空間下建立Watcher(vm, key, cb)監聽vm.key改動,再次執行更新函數
    update(node, vm, exp, dir) {
        let updaterFn = this[dir + 'Updater']
        updaterFn && updaterFn(node, vm[exp])
        new Watcher(vm, exp, function(value) {
            updaterFn && updaterFn(node, value)
        })
    }
    // 編譯發現節點有事件綁定時,在命名空間vm中找對應方法,綁定到節點事件上
    eventHandler(node, vm, exp, dir) {
        let fn = vm.$options.methods && vm.$options.methods[exp]
        if (dir && fn) {
            node.addEventListener(dir, fn.bind(vm), false)
        }
    }
    textUpdater(node, value) {
        node.textContent = value
    }
    htmlUpdater(node, value) {
        node.innerHTML = value
    }
    modelUpdater(node, value) {
        node.value = value
    }
}


/**
 * 編譯文檔時,每一個綁定數據的節點,都建立一個Watcher監聽數據變化
 * Watcher初始化時訪問所監聽響應式數據,並把自身賦值給Dep.target,這時響應式數據執行get方法,給本身的Dep收集到Watcher後,清空Dep.target.
 */
class Watcher {
  /**
   *
   * @param {Vue} vm Vue實例,做用空間
   * @param {String} key 監聽屬性
   * @param {Function} cb 更新視圖的方法
   */
  constructor(vm, key, cb) {
    // 在new一個監聽器對象時將該對象賦值給Dep.target,在get中會用到
    // 將 Dep.target 指向本身
    // 而後觸發屬性的 getter 添加監聽
    // 最後將 Dep.target 置空
    this.cb = cb
    this.vm = vm
    this.key = key
    this.value = this.get()
    }
    get() {
        Dep.target = this
        let value = this.vm[this.key] // 監聽屬性的值
        return value
    }
    // 更新視圖的方法
    update() {
        this.value = this.get() // 有嚴重缺陷,反覆調用響應式數據的get方法會重複建立Watcher並添加到數據的Dep之中,執行幾回後瀏覽器便會卡死。
        this.cb.call(this.vm, this.value)
    }
  }

二、diff&patch過程及算法node

三、keep-alive原理算法

巴拉巴拉

應用場景:npm

  • 從分類頁面進入詳情頁再返回到列表頁時,咱們不但願刷新,但願列表頁可以緩存已加載的數據和自動保存用戶上次瀏覽的位置。

四、CSS的scoped私有做用域和深度選擇器原理瀏覽器

編譯前:
<div class="example">測試文本</div>
<style scoped>
.example {
  color: red;
}

編譯後:
<div data-v-4fb7e322 class="example">測試文本</div>
<style scoped>
.example[data-v-4fb7e322] {
  color: red;
}

原理:給Vue組件的全部標籤添加一個相同的屬性,私有樣式選擇器都加上該屬性。可是第三方組件(如UI庫)內部的標籤並無編譯爲附帶[data-v-xxx]這個屬性,因此局部樣式對第三方組件不生效。
注意:儘可能別用標籤選擇器,特別是與特性選擇器組合使用時會慢不少。取而代之能夠給標籤起個class名或者id。
若是但願scoped樣式中的一個選擇器可以做用的更深,例如影響子組件,可使用 >>> 操做符。而對於less或者sass等預編譯,是不支持 >>> 操做符的,可使用 /deep/ 來替換。緩存

五、nextTick原理:
使用Vue.js的global API的$nextTick方法,便可在回調中獲取已經更新好的DOM實例了。sass

nextTick的實現比較簡單,執行的目的是在microtask或者task中推入一個function,在當前棧執行完畢(也許還會有一些排在前面的須要執行的任務)之後執行nextTick傳入的function,看一下源碼:網絡

/**
 * Defer a task to execute it asynchronously.
 */
 /*
    延遲一個任務使其異步執行,在下一個tick時執行,一個當即執行函數,返回一個function
    這個函數的做用是在task或者microtask中推入一個timerFunc,在當前調用棧執行完之後以此執行直到執行到timerFunc
    目的是延遲到當前調用棧執行完之後執行
*/
export const nextTick = (function () {
  /*存放異步執行的回調*/
  const callbacks = []
  /*一個標記位,若是已經有timerFunc被推送到任務隊列中去則不須要重複推送*/
  let pending = false
  /*一個函數指針,指向函數將被推送到任務隊列中,等到主線程任務執行完時,任務隊列中的timerFunc被調用*/
  let timerFunc

  /*下一個tick時的回調*/
  function nextTickHandler () {
    /*一個標記位,標記等待狀態(即函數已經被推入任務隊列或者主線程,已經在等待當前棧執行完畢去執行),這樣就不須要在push多個回調到callbacks時將timerFunc屢次推入任務隊列或者主線程*/
    pending = false
    /*執行全部callback*/
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
      copies[i]()
    }
  }
  ......
}

看了源碼發現timerFunc會檢測當前環境而不一樣實現,其實就是按照Promise,MutationObserver,setTimeout優先級,哪一個存在使用哪一個,最不濟的環境下使用setTimeout。
這裏解釋一下,一共有Promise、MutationObserver以及setTimeout三種嘗試獲得timerFunc的方法。
優先使用Promise,在Promise不存在的狀況下使用MutationObserver,這兩個方法的回調函數都會在microtask中執行,它們會比setTimeout更早執行,因此優先使用。
(1)首先是Promise,Promise.resolve().then()能夠在microtask中加入它的回調,
(2)MutationObserver新建一個textNode的DOM對象,用MutationObserver綁定該DOM並指定回調函數,在DOM變化的時候則會觸發回調,該回調會進入microtask,即textNode.data = String(counter)時便會加入該回調。
(3)setTimeout是最後的一種備選方案,它會將回調函數加入task中,等到執行。
綜上,nextTick的目的就是產生一個回調函數加入task或者microtask中,當前棧執行完之後(可能中間還有別的排在前面的函數)調用該回調函數,起到了異步觸發(即下一個tick時觸發)的目的。app

如今有這樣的一種狀況,mounted的時候test的值會被++循環執行1000次。每次++時,都會根據響應式觸發setter->Dep->Watcher->update->patch。
因此Vue.js實現了一個queue隊列,在下一個tick的時候會統一執行queue中Watcher的run。同時,擁有相同id的Watcher不會被重複加入到該queue中去,因此不會執行1000次Watcher的run。最終更新視圖只會直接將test對應的DOM的0變成1000。保證更新視圖操做DOM的動做是在當前棧執行完之後下一個tick的時候調用,大大優化了性能。

100、Hiper:一款使人愉悅的性能分析工具

安裝:
npm install hiper -g
使用:
#請求頁面,報告DNS查詢耗時、TCP鏈接耗時、TTFB...
hiper baidu.com?a=1&b=2 //省略協議頭時,默認添加 https://
#加載指定頁面100次
hiper -n 100 "baidu.com?a=1&b=2" --no-cache
#使用指定useragent加載網頁100次
hiper -n 100 "baidu.com?a=1&b=2" -u "Mozilla/5.0(Macintosh;Intel Mac OS X 10_13_14) AppleWebkit/537.36(KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36"

平時咱們查看性能的方式,是在performance和network中查看數據,記錄下幾個關鍵的性能指標,而後刷新幾回再看這些性能指標。有時候咱們發現,因爲採樣太少,受當前【網絡】、【CPU】、【內存】的繁忙成都的影響很重,有時優化後的項目反而比優化前更慢。 若是有一個工具,一次性的請求N次網頁,而後把各個性能指標取出來求平均值,咱們就能很是準確的知道這個優化是【正優化】仍是【負優化】。 hiper就是解決這個痛點的。

相關文章
相關標籤/搜索