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

首發(fā)于SirM2z的博客

配合ppt食用更佳

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

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

思路

  • 當(dā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)式串慰,需要在它開始對屬性進(jìn)行響應(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這樣寫來的方便,所以就需要加一層代理進(jìn)行轉(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ù)的準(zhǔn)確度問題你画!

第二步,解決準(zhǔn)確度問題桃漾,引出虛擬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屬性坏匪,但是當(dāng)修改age屬性的時候,會不會觸發(fā)渲染呢撬统?答案是:會适滓。但實際是不需要觸發(fā)渲染機(jī)制的

解決這個問題,先要簡單說下虛擬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)換成同一種表達(dá)方式,即如下:

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標(biāo)簽名
    data: data, // 包含諸如 class 和 style 這些標(biāo)簽上的屬性
    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ù)

第三步漆弄,依賴收集,準(zhǔn)確渲染

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

其實不僅是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í)行過程就是:

  • 當(dāng)執(zhí)行render函數(shù)的時候滓玖,依賴到的變量的get就會被執(zhí)行,然后就把這個 render函數(shù)加到subs里面去质蕉。
  • 當(dāng)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的渲染機(jī)制一定是比react更加準(zhǔn)確的,為了驗證這一說法比规,我用兩個框架同時寫了兩個相同的簡單項目進(jìn)行對比若厚。

沒有對比就沒有傷害:

通過對比發(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)系作者
  • 序言:七十年代末霎冯,一起剝皮案震驚了整個濱河市铃拇,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沈撞,老刑警劉巖慷荔,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異缠俺,居然都是意外死亡显晶,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門壹士,熙熙樓的掌柜王于貴愁眉苦臉地迎上來磷雇,“玉大人,你說我怎么就攤上這事躏救∥希” “怎么了?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵落剪,是天一觀的道長睁本。 經(jīng)常有香客問我,道長忠怖,這世上最難降的妖魔是什么呢堰? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮凡泣,結(jié)果婚禮上枉疼,老公的妹妹穿的比我還像新娘。我一直安慰自己鞋拟,他們只是感情好骂维,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著贺纲,像睡著了一般航闺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上猴誊,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天潦刃,我揣著相機(jī)與錄音,去河邊找鬼懈叹。 笑死乖杠,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的澄成。 我是一名探鬼主播胧洒,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼畏吓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了卫漫?” 一聲冷哼從身側(cè)響起菲饼,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎汛兜,沒想到半個月后巴粪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡粥谬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年肛根,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片漏策。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡派哲,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出掺喻,到底是詐尸還是另有隱情芭届,我是刑警寧澤,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布感耙,位于F島的核電站褂乍,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏即硼。R本人自食惡果不足惜逃片,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望只酥。 院中可真熱鬧褥实,春花似錦、人聲如沸裂允。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绝编。三九已至僻澎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間十饥,已是汗流浹背怎棱。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留绷跑,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓凡资,卻偏偏與公主長得像砸捏,于是被迫代替她去往敵國和親谬运。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容

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