天天看點

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

分享前啰嗦

我之前介紹過vue1.0如何實作observer和watcher。本想繼續寫下去,可是vue2.0橫空出世..是以

直接看vue2.0吧。這篇文章在公司分享過,終于寫出來了。我們采用用最精簡的代碼,還原vue2.0響應式架構實作

以前寫的那篇 vue 源碼分析之如何實作 observer 和 watcher可以作為本次分享的參考。

不過不看也沒關系,但是最好了解下object.defineproperty

本文分享什麼

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

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

順帶介紹他比react快的其中一個原因

本分實作什麼

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>

好,開始吧!!!

第一步, 講data 下面所有屬性變為observable

來來來先看代碼吧

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可能就會很好了解,不過沒關系,我們三言兩語再說說,這段代碼要實作的功能就是将

var demo = new vue({ 

     el: '#demo', 

     data: { 

       text: 123, 

     }, 

     render(){ 

       console.log("我要render了") 

     } 

   })  

中data 裡面所有的屬性置于 observer,然後data裡面的屬性,比如 text

以改變,就引起_update()函數調用進而重新渲染,是怎樣做到的呢,我們知道其實就是指派的時候就要改變對吧,當我給data下面的text

指派的時候 set 函數就會觸發,這個時候 調用_update 就ok了,但是

settimeout(function(){ 

      demo._data.text = 444 

    }, 3000)  

demo._data.text沒有demo.text用着爽,沒關系,我們加一個代理

_proxy(key) { 

      const self = this 

      object.defineproperty(self, key, { 

        get: function proxygetter () { 

          return self._data[key] 

        }, 

        set: function proxysetter (val) { 

          self._data[key] = val 

    }  

然後在vue的constructor加上下面這句

object.keys(options.data).foreach(key => this._proxy(key)) 

第一步先說到這裡,我們會發現一個問題,data中任何一個屬性的值改變,都會引起

_update的觸發進而重新渲染,屬性這顯然不夠精準啊

第二步,詳細闡述第一步為什麼不夠精準

比如考慮下面代碼

new vue({ 

     template: ` 

       <div> 

         <section> 

           <span>name:</span> {{name}} 

         </section> 

           <span>age:</span> {{age}} 

       <div>`, 

       name: 'js', 

       age: 24, 

       height: 180 

   }) 

   settimeout(function(){ 

     demo.height = 181 

   }, 3000)  

template裡面隻用到了data上的兩個屬性name和age,但是當我改變height的時候,用第一步的代碼,會不會觸發重新渲染?會!,但其實不需要觸發重新渲染,這就是問題所在!!

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

簡單說說虛拟 dom

首先,template最後都是編譯成render函數的(具體怎麼做,就不展開說了,以後我會說的),然後render 函數執行完就會得到一個虛拟dom,為了好了解我們寫寫最簡單的虛拟dom

function vnode(tag, data, children, text) { 

      return { 

        tag: tag, 

        data: data, 

        children: children, 

        text: text 

    class vue { 

        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); 

        text: "before", 

        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,tag是html 标簽名,data 是包含諸如 class 和 style 這些标簽上的屬性,childen就是子節點,關于虛拟dom就不展開說了。

回到開始的問題,也就是說,我得知道,render 函數裡面依賴了vue執行個體裡面哪些變量(隻考慮render 就可以,因為template 也會是幫你編譯成render)。叙述有點拗口,還是看代碼吧

        name: "123", 

        age: 23 

就像這段代碼,render 函數裡其實隻依賴text,并沒有依賴 name和 age,是以,我們隻要text改變的時候

我們自動觸發 render 函數 讓它生成一個虛拟dom就ok了(剩下的就是這個虛拟dom和上個虛拟dom做比對,然後操作真實dom,隻能以後再說了),那麼我們正式考慮一下怎麼做

第三步,'touch' 拿到依賴

回到最上面那張圖,我們知道data上的屬性設定definereactive後,修改data 上的值會觸發 set。

那麼我們取data上值是會觸發 get了。

對,我們可以在上面做做手腳,我們先執行一下render,我們看看data上哪些屬性觸發了get,我們豈不是就可以知道 render 會依賴data 上哪些變量了。

然後我麼把這些變量做些手腳,每次這些變量變的時候,我們就觸發render。

上面這些步驟簡單用四個子概括就是 計算依賴。

(其實不僅是render,任何一個變量的改别,是因為别的變量改變引起,都可以用上述方法,也就是computed 和 watch 的原理,也是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()) 

      const dep = new dep() 

        // 省略 

然後,當執行render 函數去'touch'依賴的時候,依賴到的變量get就會被執行,然後我們就可以把這個 render 函數加到 subs 裡面去了。

當我們,set 的時候 我們就執行 notify 将所有的subs數組裡的函數執行,其中就包含render 的執行。

至此就完成了整個圖,好我們将所有的代碼展示出來

     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({ 

       text: "before", 

       return this.__h__('div', {}, [ 

         this.__h__('span', {}, [this.__tostring__(this.text)]) 

       ]) 

    settimeout(function(){ 

      demo.text = "after" 

我們看一下運作結果

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

好我們解釋一下 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 要實作性能最大化,要做大量工作,比如我以前分享的

<a href="https://segmentfault.com/a/1190000004290333">react如何性能達到最大化(前傳),暨react為啥非得使用immutable.js</a>

<a href="https://segmentfault.com/a/1190000004295639">react 實作pure render的時候,bind(this)隐患。</a>

而 vue2.0 天然幫你做到了最優,而且對于像萬年不變的 如标簽上靜态的class屬性,

vue2.0 在重新渲染後做diff 的時候是不比較的,vue2.0比 達到性能最大化的react 還要快的一個原因

然後源碼在此,喜歡的記得給個 star 哦

後續,我會簡單聊聊,vue2.0的diff。

作者:楊川寶

來源:51cto

繼續閱讀