vue2.x響應(yīng)式原理,vue與react響應(yīng)式簡單對比

首發(fā)于SirM2z的博客

配合ppt食用更佳

實現(xiàn)的最終目標

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

對應(yīng)的虛擬DOM會從

<div><span>before</span></div> 

變成

 <div><span>after</span></div>

第一步,監(jiān)聽data下邊的所有屬性,轉(zhuǎn)換為響應(yīng)式

思路

  • 當data下的某個屬性變化時,如何觸發(fā)相應(yīng)的函數(shù)?

方案:ES5中新添加了一個方法:Object.defineProperty,通過這個方法,可以自定義gettersetter函數(shù),那么在獲取對象屬性或者設(shè)置對象屬性時就能夠執(zhí)行相應(yīng)的回調(diào)函數(shù)

Object.defineProperty MDN

代碼如下:

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

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

function defineReactive(obj, key, val, cb) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: () => {
      console.log('你訪問了' + key)
      return val
    },
    set: newVal => {
      if (newVal === val)
        return
      console.log('你設(shè)置了' + key)
      console.log('新的' + key + ' = ' + newVal)
      val = newVal
      cb()
    }
  })
}

var demo1 = new Vue({
  el: '#demo',
  data: {
    text: "before"
  },
  render(){
    console.log("我要render了")
  }
})
  • 引發(fā)了第二個問題,如果data中的屬性是一個對象還能觸發(fā)我們的回掉函數(shù)么?比如說下邊的demo
var demo2 = new Vue({
  el: '#demo',
  data: {
    text: "before",
    o: {
      text: "o-before"
    }
  },
  render(){
    console.log("我要render了")
  }
})

方案:用遞歸完善上邊的響應(yīng)式,需要在它開始對屬性進行響應(yīng)式轉(zhuǎn)換的時候,前邊加個判斷,即如下

function observer(obj) {
  Object.keys(obj).forEach((key) => {
    if (typeof obj[key] === 'object') {
      new observer(obj[key], cb)
    }
    defineReactive(obj, key, obj[key])
  })
}
  • 實際寫的過程中發(fā)現(xiàn)調(diào)用data的屬性時需要這樣寫demo._data.text,肯定是沒有demo.text這樣寫來的方便,所以就需要加一層代理進行轉(zhuǎn)換

代碼如下:

  _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
      }
    })
  }

然后在構(gòu)造函數(shù)中加上這么一句話

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

到此,我們的data屬性已經(jīng)變?yōu)轫憫?yīng)式的了,只要data的屬性發(fā)生變化,那么就會觸發(fā)render函數(shù)。這也是為什么只有vue組件中的data屬性才是響應(yīng)式的,其他地方聲明的值均不是響應(yīng)式的原因。但是這里有個問題,即觸發(fā)render函數(shù)的準確度問題!

第二步,解決準確度問題,引出虛擬dom

比如下邊的demo

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

setTimeout(function(){
  demo.age = 25
}, 3000)

template中只用到了data中的name屬性,但是當修改age屬性的時候,會不會觸發(fā)渲染呢?答案是:會。但實際是不需要觸發(fā)渲染機制的

解決這個問題,先要簡單說下虛擬dom。vue有兩種寫法:

// template模板寫法(最常用的)
new Vue({
  data: {
    text: "before",
  },
  template: `
    <div>
      <span>text:</span> {{text}}
    </div>`
})

// render函數(shù)寫法,類似react的jsx寫法
new Vue({
  data: {
    text: "before",
  },
  render (h) {
    return (
      <div>
        <span>text:</span> {{text}}
      </div>
    )
  }
})

由于vue2.x引入了虛擬dom的原因,這兩種寫法最終都會被解析成虛擬dom,但在這之前,他們會先被解析函數(shù)轉(zhuǎn)換成同一種表達方式,即如下:

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

透過上邊的render函數(shù)中的this.__h__方法,可以簡單了解下虛擬dom

function VNode(tag, data, children, text) {
  return {
    tag: tag, // html標簽名
    data: data, // 包含諸如 class 和 style 這些標簽上的屬性
    children: children, // 子節(jié)點
    text: text // 文本節(jié)點
  }
}

寫一個簡單的虛擬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)])
    ])
  }
})

回頭看問題,也就是說,我需要知道render函數(shù)中依賴了data中的哪些屬性,只有這些屬性變化,才需要去觸發(fā)render函數(shù)

第三步,依賴收集,準確渲染

思路:在這之前,我們已經(jīng)把data中的屬性改成響應(yīng)式了,當去獲取或者修改這些變量時便能夠觸發(fā)相應(yīng)函數(shù)。那這里就可以利用這個相應(yīng)的函數(shù)做些手腳了。當聲明一個vue對象時,在執(zhí)行render函數(shù)獲取虛擬dom的這個過程中,已經(jīng)對render中依賴的data屬性進行了一次獲取操作,這次獲取操作便可以拿到所有依賴。

其實不僅是render,任何一個變量的改別,是因為別的變量改變引起,都可以用上述方法,也就是computedwatch的原理

首先需要寫一個依賴收集的類,每一個data中的屬性都有可能被依賴,因此每個屬性在響應(yīng)式轉(zhuǎn)化(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, {
    // 省略
  })
}

那么執(zhí)行過程就是:

  • 當執(zhí)行render函數(shù)的時候,依賴到的變量的get就會被執(zhí)行,然后就把這個 render函數(shù)加到subs里面去。
  • set的時候,就執(zhí)行notify,將所有的subs數(shù)組里的函數(shù)執(zhí)行,其中就包含render的執(zhí)行。

注:代碼中有一個Dep.target值,這個值時用來區(qū)分是普通的get還是收集依賴時的get

最后完整代碼如下:

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[key] = 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(obj) {
  Object.keys(obj).forEach((key) => {
    if (typeof obj[key] === 'object') {
      new observer(obj[key])
    }
    defineReactive(obj, key, obj[key])
  })
}

function defineReactive(obj, key, val) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: () => {
      if (Dep.target) {
        dep.add(Dep.target)
        Dep.target = null
      }
      console.log('你訪問了' + key)
      return val
    },
    set: newVal => {
      if (newVal === val)
        return
      console.log('你設(shè)置了' + key)
      console.log('新的' + key + ' = ' + newVal)
      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",
    test: {
      a: '1'
    },
    t: 1
  },
  render() {
    return this.__h__('div', {}, [
      this.__h__('span', {}, [this.__toString__(this.text)]),
      this.__h__('span', {}, [this.__toString__(this.test.a)])
    ])
  }
})

vue react響應(yīng)式簡單對比

綜上發(fā)現(xiàn),利用Object.defineProperty這個特性可以精確的寫出訂閱發(fā)布模式,從這點來說,vue是優(yōu)于react的,在沒經(jīng)過優(yōu)化之前,vue的渲染機制一定是比react更加準確的,為了驗證這一說法,我用兩個框架同時寫了兩個相同的簡單項目進行對比。

沒有對比就沒有傷害:

通過對比發(fā)現(xiàn),react在正常使用的過程中產(chǎn)生了多余的渲染,在移動端或者組件嵌套非常深的情況下會產(chǎn)生非常大的性能消耗,因此在使用react的過程中,寫好react生命周期中的shouldComponentUpdate是非常重要的!

參考

理解vue2.0響應(yīng)式架構(gòu)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 這篇筆記主要包含 Vue 2 不同于 Vue 1 或者特有的內(nèi)容,還有我對于 Vue 1.0 印象不深的內(nèi)容。關(guān)于...
    云之外閱讀 5,173評論 0 29
  • 原教程內(nèi)容詳見精益 React 學習指南,這只是我在學習過程中的一些閱讀筆記,個人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,931評論 1 18
  • Vue也已經(jīng)升級到2.0版本了,到現(xiàn)在為止(2016/11/19)比較流行的MVVM框架有AngularJS(也有...
    彬_仔閱讀 27,386評論 12 114
  • 夜里的暗,格外的寂寞,蟬也不叫了,只聽到蚊子嗡嗡的聲音。 昏暗的路燈照射在那座公寓的門,就是那里,剛才父親進去的地...
    蒂姆閱讀 773評論 0 50
  • 正面的心理暗示、積極的心態(tài),會引導事情正向發(fā)展,人生變得越來越美好! (一)發(fā)燒 涵寶感冒發(fā)燒了,她說:為什么我這...
    pan02閱讀 251評論 0 0

友情鏈接更多精彩內(nèi)容