vue2.0源碼分析之理解響應式架構

分享前囉嗦

我以前介紹過vue1.0如何實現observerwatcher。本想繼續寫下去,但是vue2.0橫空出世..因此
直接看vue2.0吧。這篇文章在公司分享過,終於寫出來了。咱們採用用最精簡的代碼,還原vue2.0響應式架構實現
之前寫的那篇 vue 源碼分析之如何實現 observer 和 watcher能夠做爲本次分享的參考。
不過不看也不要緊,可是最好了解下Object.definePropertyjavascript

本文分享什麼

理解vue2.0的響應式架構,就是下面這張圖
vuedeptrace.jpghtml

順帶介紹他比react快的其中一個緣由vue

本分實現什麼

const demo = new Vue({
  data: {
    text: "before",
  },
  //對應的template 爲 <div><span>{{text}}</span></div>
  render(h){
    return h('div', {}, [
      h('span', {}, [this.__toString__(this.text)])
    ])
  }
})
 setTimeout(function(){
   demo.text = "after"
 }, 3000)

對應的虛擬dom會從
<div><span>before</span></div> 變爲 <div><span>after</span></div>
好,開始吧!!!java

第一步, 講data 下面全部屬性變爲observable

來來來先看代碼吧node

class Vue {
      constructor(options) {
        this.$options = options
        this._data = options.data
        observer(options.data, this._update)
        this._update()
      }
      _update(){
        this.$options.render()
      }
    }


    function observer(value, cb){
      Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
    }

    function defineReactive(obj, key, val, cb) {
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=>{},
        set:newVal=> {
          cb()
        }
      })
    }

    var demo = new Vue({
      el: '#demo',
      data: {
        text: 123,
      },
      render(){
        console.log("我要render了")
      }
    })

     setTimeout(function(){
       demo._data.text = 444
     }, 3000)

爲了好演示咱們只考慮最簡單的狀況,若是看了vue 源碼分析之如何實現 observer 和 watcher可能就會很好理解,不過不要緊,咱們三言兩語再說說,這段代碼要實現的功能就是將react

var demo = new Vue({
      el: '#demo',
      data: {
        text: 123,
      },
      render(){
        console.log("我要render了")
      }
    })

data 裏面全部的屬性置於 observer,而後data裏面的屬性,好比 text 以改變,就引發_update()函數調用進而從新渲染,是怎樣作到的呢,咱們知道其實就是賦值的時候就要改變對吧,當我給data下面的text 賦值的時候 set 函數就會觸發,這個時候 調用 _update 就ok了,可是git

setTimeout(function(){
       demo._data.text = 444
     }, 3000)

demo._data.text沒有demo.text用着爽,不要緊,咱們加一個代理github

_proxy(key) {
        const self = this
        Object.defineProperty(self, key, {
          configurable: true,
          enumerable: true,
          get: function proxyGetter () {
            return self._data[key]
          },
          set: function proxySetter (val) {
            self._data[key] = val
          }
        })
      }

而後在Vueconstructor加上下面這句segmentfault

Object.keys(options.data).forEach(key => this._proxy(key))

第一步先說到這裏,咱們會發現一個問題,data中任何一個屬性的值改變,都會引發
_update的觸發進而從新渲染,屬性這顯然不夠精準啊數組

第二步,詳細闡述第一步爲何不夠精準

好比考慮下面代碼

new Vue({
      template: `
        <div>
          <section>
            <span>name:</span> {{name}}
          </section>
          <section>
            <span>age:</span> {{age}}
          </section>
        <div>`,
      data: {
        name: 'js',
        age: 24,
        height: 180
      }
    })

    setTimeout(function(){
      demo.height = 181
    }, 3000)

template裏面只用到了data上的兩個屬性nameage,可是當我改變height的時候,用第一步的代碼,會不會觸發從新渲染?會!,但其實不須要觸發從新渲染,這就是問題所在!!

第三步,上述問題怎麼解決

簡單說說虛擬 DOM

首先,template最後都是編譯成render函數的(具體怎麼作,就不展開說了,之後我會說的),而後render 函數執行完就會獲得一個虛擬DOM,爲了好理解咱們寫寫最簡單的虛擬DOM

function VNode(tag, data, children, text) {
      return {
        tag: tag,
        data: data,
        children: children,
        text: text
      }
    }

    class Vue {
      constructor(options) {
        this.$options = options
        const vdom = this._update()
        console.log(vdom)
      }
      _update() {
        return this._render.call(this)
      }
      _render() {
        const vnode = this.$options.render.call(this)
        return vnode
      }
      __h__(tag, attr, children) {
        return VNode(tag, attr, children.map((child)=>{
          if(typeof child === 'string'){
            return VNode(undefined, undefined, undefined, child)
          }else{
            return child
          }
        }))
      }
      __toString__(val) {
        return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
      }
    }


    var demo = new Vue({
      el: '#demo',
      data: {
        text: "before",
      },
      render(){
        return this.__h__('div', {}, [
          this.__h__('span', {}, [this.__toString__(this.text)])
        ])
      }
    })

咱們運行一下,他會輸出

{
       tag: 'div',
       data: {},
       children:[
         {
           tag: 'span',
           data: {},
           children: [
             {
               children: undefined,
               data: undefined,
               tag: undefined,
               text: '' // 正常狀況爲 字符串 before,由於咱們爲了演示就不寫代理的代碼,因此這裏爲空
             }
           ]
         }
       ]
     }

這就是 虛擬最簡單虛擬DOM,taghtml 標籤名,data 是包含諸如 classstyle 這些標籤上的屬性,childen就是子節點,關於虛擬DOM就不展開說了。
回到開始的問題,也就是說,我得知道,render 函數裏面依賴了vue實例裏面哪些變量(只考慮render 就能夠,由於template 也會是幫你編譯成render)。敘述有點拗口,仍是看代碼吧

var demo = new Vue({
      el: '#demo',
      data: {
        text: "before",
        name: "123",
        age: 23
      },
      render(){
        return this.__h__('div', {}, [
          this.__h__('span', {}, [this.__toString__(this.text)])
        ])
      }
    })

就像這段代碼,render 函數裏其實只依賴text,並無依賴 nameage,因此,咱們只要text改變的時候
咱們自動觸發 render 函數 讓它生成一個虛擬DOM就ok了(剩下的就是這個虛擬DOM和上個虛擬DOM作比對,而後操做真實DOM,只能之後再說了),那麼咱們正式考慮一下怎麼作

第三步,'touch' 拿到依賴

回到最上面那張圖,咱們知道data上的屬性設置defineReactive後,修改data 上的值會觸發 set
那麼咱們取data上值是會觸發 get了。
對,咱們能夠在上面作作手腳,咱們先執行一下render,咱們看看data上哪些屬性觸發了get,咱們豈不是就能夠知道 render 會依賴data 上哪些變量了。
而後我麼把這些變量作些手腳,每次這些變量變的時候,咱們就觸發render
上面這些步驟簡單用四個子歸納就是 計算依賴。
(其實不只是render,任何一個變量的改別,是由於別的變量改變引發,均可以用上述方法,也就是computedwatch 的原理,也是mobx的核心)

第一步,

咱們寫一個依賴收集的類,每個data 上的對象都有可能被render函數依賴,因此每一個屬性在defineReactive
時候就初始化它,簡單來講就是這個樣子的

class Dep {
      constructor() {
        this.subs = []
      }
      add(cb) {
        this.subs.push(cb)
      }
      notify() {
        console.log(this.subs);
        this.subs.forEach((cb) => cb())
      }
    }
    function defineReactive(obj, key, val, cb) {
      const dep = new Dep()
      Object.defineProperty(obj, key, {
        // 省略
      })
    }

而後,當執行render 函數去'touch'依賴的時候,依賴到的變量get就會被執行,而後咱們就能夠把這個 render 函數加到 subs 裏面去了。
當咱們,set 的時候 咱們就執行 notify 將全部的subs數組裏的函數執行,其中就包含render 的執行。
至此就完成了整個圖,好咱們將全部的代碼展現出來

function VNode(tag, data, children, text) {
      return {
        tag: tag,
        data: data,
        children: children,
        text: text
      }
    }

    class Vue {
      constructor(options) {
        this.$options = options
        this._data = options.data
        Object.keys(options.data).forEach(key => this._proxy(key))
        observer(options.data)
        const vdom = watch(this, this._render.bind(this), this._update.bind(this))
        console.log(vdom)
      }
      _proxy(key) {
        const self = this
        Object.defineProperty(self, key, {
          configurable: true,
          enumerable: true,
          get: function proxyGetter () {
            return self._data[key]
          },
          set: function proxySetter (val) {
            self._data.text = val
          }
        })
      }
      _update() {
        console.log("我須要更新");
        const vdom = this._render.call(this)
        console.log(vdom);
      }
      _render() {
        return this.$options.render.call(this)
      }
      __h__(tag, attr, children) {
        return VNode(tag, attr, children.map((child)=>{
          if(typeof child === 'string'){
            return VNode(undefined, undefined, undefined, child)
          }else{
            return child
          }
        }))
      }
      __toString__(val) {
        return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
      }
    }

    function observer(value, cb){
      Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
    }

    function defineReactive(obj, key, val, cb) {
      const dep = new Dep()
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=>{
          if(Dep.target){
            dep.add(Dep.target)
          }
          return val
        },
        set: newVal => {
          if(newVal === val)
            return
          val = newVal
          dep.notify()
        }
      })
    }
    function watch(vm, exp, cb){
      Dep.target = cb
      return exp()
    }

    class Dep {
      constructor() {
        this.subs = []
      }
      add(cb) {
        this.subs.push(cb)
      }
      notify() {
        this.subs.forEach((cb) => cb())
      }
    }
    Dep.target = null


    var demo = new Vue({
      el: '#demo',
      data: {
        text: "before",
      },
      render(){
        return this.__h__('div', {}, [
          this.__h__('span', {}, [this.__toString__(this.text)])
        ])
      }
    })


     setTimeout(function(){
       demo.text = "after"
     }, 3000)

咱們看一下運行結果
D4A9A9B8-03CA-4A73-A12F-30409E08D99D.png

好咱們解釋一下 Dep.target 由於咱們得區分是,普通的get,仍是在查找依賴的時候的get
全部咱們在查找依賴時候,咱們將

function watch(vm, exp, cb){
      Dep.target = cb
      return exp()
    }

Dep.target 賦值,至關於 flag 一下,而後 get 的時候

get: () => {
          if (Dep.target) {
            dep.add(Dep.target)
          }
          return val
        },

判斷一下,就行了。
到如今爲止,咱們再看那張圖是否是就清楚不少了?

總結

我很是喜歡,vue2.0 以上代碼爲了好展現,都採用最簡單的方式呈現。
不過整個代碼執行過程,甚至是命名方式都和vue2.0同樣
對比react,vue2.0 自動幫你監測依賴,自動幫你從新渲染,而
react 要實現性能最大化,要作大量工做,好比我之前分享的
react如何性能達到最大化(前傳),暨react爲啥非得使用immutable.js
react 實現pure render的時候,bind(this)隱患
而 vue2.0 自然幫你作到了最優,並且對於像萬年不變的 如標籤上靜態的class屬性,
vue2.0 在從新渲染後作diff 的時候是不比較的,vue2.0比 達到性能最大化的react 還要快的一個緣由
而後源碼在此,喜歡的記得給個 star 哦? 後續,我會簡單聊聊,vue2.0的diff。 若是有疑問,能夠在評論區留言哈

相關文章
相關標籤/搜索