Vue 響應式實現原理

bojue發表於2020-05-23

Vue響應式原理

Vue是資料驅動檢視實現雙向繫結的一種前端框架,採用的是非入侵性的響應式系統,不需要採用新的語法(擴充套件語法或者新的資料結構)實現物件(model)和檢視(view)的自動更新,資料層(Model)僅僅是普通的Javascript物件,當Modle更新後view層自動完成更新,同理view層修改會導致model層資料更新。

雙向繫結實現機制

Vue的雙向繫結實現機制核心:

  • 依賴於Object.defineProperty()實現資料劫持
  • 訂閱模式

Object.defineProperty()

MDN: Object.defineProperty() 方法會直接在一個物件上定義一個新屬性,或者修改一個物件的現有屬性,並返回此物件

我們使用Object.defineProperty()進行簡單的資料劫持操作:

var Book = {
  name:"jsBook"
};

Object.defineProperty(Book, 'name', {
  enumerable: true,
  configurable: false,
  set: function (value) {
    this._name = `${value || "JavaScript程式設計思想"} `;
  },
  get:function() {
    return `《${this._name}》`;
  },

});
Book.name = null;
console.log(Book.name) // 《JavaScript程式設計思想》

這只是對於一個屬性的設定,當我們需要劫持物件的所有的屬性的時候,可以封裝 Object.defineProperty()方法並借用Object.keys()進行物件可列舉屬性的遍歷:

var Person = {
  name:"smith",
  skill:"熟練使用Java"
};

let myReactive = function(obj, key , val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: false,
    set: function (value) {
      val = value;
    },
    get:function() {
      return val;
    },
  });
}

Object.keys(Person).forEach(key => {
  myReactive(Person, key, Person[key])
})

Person.skill = "熟練使用JavaScript";
console.log(Person.name + Person.skill); // smith熟練使用JavaScript

通過簡單的例子我們可以瞭解Object.defineProperty()的基本資料劫持操作,這也是Vue的響應式實現的基本原理,Vue在初始化物件的之前將資料定義在data物件中,初始化例項時對屬性執行 getter/setter 轉化過程,所以只有定義在data物件上的屬性才能被劫持(被轉化),同時因為JavaScript的限制Vue不能檢測物件屬性的新增和刪除。

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

class Vue {
    constructor(options) {
        this._data = options.data;
        observe(this._data, options.render)
    }
}
let app = new Vue({
    el: '#app',
    data: {
        text: 'text',
        text2: 'text2'
    },
    render(){
        console.log("render");
    }
})

Vue原始碼分析

  1. 初始化Vue例項
/*initMixin就做了一件事情,在Vue的原型上增加_init方法,
* 構造Vue例項的時候會呼叫這個_init方法來初始化Vue例項
*/
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) { 
    const vm: Component = this;
    vm._self = vm
    /*初始化生命週期*/
    initLifecycle(vm)
    /*初始化事件*/
    initEvents(vm)
    /*初始化render*/
    initRender(vm)
    /*呼叫beforeCreate鉤子函式並且觸發beforeCreate鉤子事件*/
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    /*初始化props、methods、data、computed與watch*/
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    /*呼叫created鉤子函式並且觸發created鉤子事件*/
    callHook(vm, 'created')
  }
}
  1. 初始化狀態

我們可以瞭解initState(vm)方法用來初始化Vue我們配置的方法,資料等狀態,所以我們重點研究一下initState()方法:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  /*初始化props*/
  if (opts.props) initProps(vm, opts.props)
  /*初始化方法*/
  if (opts.methods) initMethods(vm, opts.methods)
  /*初始化data*/
  if (opts.data) {
    initData(vm)
  } else {
    /*該元件沒有data的時候繫結一個空物件*/
    observe(vm._data = {}, true /* asRootData */)
  }
  /*初始化computed*/
  if (opts.computed) initComputed(vm, opts.computed)
  /*初始化watchers*/
  if (opts.watch) initWatch(vm, opts.watch)
}
  1. 初始化資料

在初始化資料的時候,我們需要判斷data中的key 不能與props定義過的key重複,如果衝突將會以props定義的key優先,並且告警提示衝突。

/*初始化data*/
function initData (vm: Component) {

/*得到data資料*/
let data = vm.$options.data

/*遍歷data物件*/
const keys = Object.keys(data)
const props = vm.$options.props
let i = keys.length

//遍歷data中的資料
while (i--) {
  if (props && hasOwn(props, keys[i])) {
    process.env.NODE_ENV !== 'production' && warn(
      `The data property "${keys[i]}" is already declared as a prop. ` +
      `Use prop default value instead.`,
      vm
    )
  }
}
// observe data
/*通過observe例項化Observe物件,開始對資料進行繫結
* asRootData用來根資料,用來計算例項化根資料的個數
* 下面會進行遞迴observe進行對深層物件的繫結。則asRootData為非true
*/
observe(data, true /* asRootData */)
}
  1. 觀察物件
export class Observer {
constructor (value: any) {
  if (!Array.isArray(value))  {
    /*如果是物件則直接walk進行繫結*/
    this.walk(value)
  }
}

walk (obj: Object) {
  const keys = Object.keys(obj)
  /*walk方法會遍歷物件的每一個屬性進行defineReactive繫結*/
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i], obj[keys[i]])
  }
}

訂閱釋出

  1. 建立Wahtcher

Vue物件init後會進入mount階段,執行mountComponent函式:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 如果沒有!vm.$options.render方法,就建立一個空的VNODE,不是生產環境啥的報錯
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    // 報錯的程式碼
  }
  // 呼叫一下回撥函式
  callHook(vm, 'beforeMount')
  // 定義一個updateComponent方法
  let updateComponent

  // 如果啥啥啥條件,那麼updateComponent定義成如下方式,否則直接呼叫_update方法
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      // 在這裡的核心呼叫來一下_render方法建立來一個vnode
      const vnode = vm._render()
      vm._update(vnode, hydrating)
    }
  } else {
      // 這裡是定義的updateComponent是直接呼叫_update方法
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined

  // 例項化一個渲染watcher,用處是初始化的時候會執行回撥函式,
  // 另一個是當 vm 例項中的監測的資料發生變化的時候執行回撥函式
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // 函式最後判斷為根節點的時候設定 vm._isMounted 為 true, 表示這個例項已經掛載了,
  // 同時執行 mounted 鉤子函式
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
  1. Wather建構函式

在mountComponent函式內部,通過new Wather()建立監聽器 ,Vue Component都會經過一次mount階段並建立一個Wather與之對應。

Wather的建構函式new Watcher(vm, updateComponent)

  • vm :與Wather對應的Vue Component例項,這種對應關係通過Wather去管理
  • updateComponent:可以理解成Vue Component的更新函式,呼叫例項render和update兩個方法,render作用是將Vue物件渲染成虛擬DOM,update是通過虛擬DOM建立或者更新真實DOM

總結

  1. Vue Component都有一個對應的Wather例項
  2. Vue Component例項初始化的時候通過data繫結物件,data上的屬性通過getter/setter轉化
  3. Vue Component執行render方法的時候,data定義的資料物件會被讀取執行getter方法,Vue Component 會記錄自己依賴的data
  4. 當data資料被修改的時候,通過setter方法更新資料,Wather會通知所有依賴此data的元件去呼叫Vue Component 的render函式更新檢視。

參考

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章