實現一個簡單版本的vue及原始碼解析(二)

San_Alex發表於2018-10-20

響應式系統及實現

響應式原理

Vue.js的響應式原理依賴於Object.defineProperty,尤大大在Vue.js文件中就已經提到過,這也是Vue.js不支援IE8 以及更低版本瀏覽器的原因。Vue通過設定物件屬性的 setter/getter 方法來監聽資料的變化,通過getter進行依賴收集,而每個setter方法就是一個觀察者,在資料變更的時候通知訂閱者更新檢視。

Let data to observable

首先假定一種最簡單的情況,不去考慮其他情況。在initData中會呼叫observe這個函式將Vue的資料設定成observable的。當_data資料發生改變的時候就會觸發set,對訂閱者進行回撥(在這裡是render)。

function observe(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: ()=>{
            /*....依賴收集等....*/
            /*Github:https://github.com/answershuto*/
            return val
        },
        set:newVal=> {
            val = newVal;
            cb();/*訂閱者收到訊息的回撥*/
        }
    })
}
複製程式碼

為了操作方便,我們需要將_data上的資料代理到vm例項上.

function proxy (data) {
    const that = this;
    Object.keys(data).forEach(key => {
        Object.defineProperty(that, key, {
            configurable: true,
            enumerable: true,
            get: function proxyGetter () {
                return that._data[key];
            },
            set: function proxySetter (val) {
                that._data[key] = val;
            }
        })
    });
}
複製程式碼

依賴收集

依賴收集的原因

按照上面的方法進行繫結會出現一個問題——實際模板中未使用的資料被更改後也會進行重新渲染,而這樣無疑會消耗效能,因此需要依賴收集來保證只渲染實際模板中使用到的資料。

Dep

當對data上的物件進行修改值的時候會觸發它的setter,那麼取值的時候自然就會觸發getter事件,所以我們只要在最開始進行一次render,那麼所有被渲染所依賴的data中的資料就會被getter收集到Dep的subs中去。在對data中的資料進行修改的時候setter只會觸發Dep的subs的函式.

Dep.prototype.depend方法是將觀察者Watcher例項賦值給全域性的Dep.target,然後觸發render操作只有被Dep.target標記過的才會進行依賴收集。有Dep.target的物件會將Watcher的例項push到subs中,在物件被修改觸發setter操作的時候dep會呼叫subs中的Watcher例項的update方法來重新獲取資料生成虛擬節點,再由服務端將虛擬節點渲染成真實DOM。

src/oberver/dep.js

var uid = 0;

//dep建構函式
export default function Dep(argument) {
	this.id = uid++
	this.subs = []
}
//新增一個觀察者物件
Dep.prototype.addSub = function(sub) {
	this.subs.push(sub)
}
//移除一個觀察者物件
Dep.prototype.removeSub = function(sub) {
	remove(this.subs, sub)
}
//依賴收集
Dep.prototype.depend = function() {
	if(Dep.target) {
		Dep.target.addDep(this)
	}
}
//通知所有訂閱者
Dep.prototype.notify = function() {
	var subs = this.subs.slice()
	for(var i = 0, l = subs.length; i < l; i++){
		subs[i].update()
	}
}

Dep.target = null

function remove (arr, item) {
    if (arr.length) {
        const index = arr.indexOf(item)
        if (index > -1) {
            return arr.splice(index, 1)
    }
}
複製程式碼

實現

上面講述響應式原理和依賴收集的原因,接下來就來簡單實現一下。開始正式程式設計前,照常先寫測試用例。

test/observer/observer.spec.js

import {
  Observer,
  observe
} from "../../src/observer/index"
import Dep from '../../src/observer/dep'

describe('Observer test', function() {
  it('observing object prop change', function() {
  	const obj = { a:1, b:{a:1}, c:NaN}
    observe(obj)
    // mock a watcher!
    const watcher = {
      deps: [],
      addDep (dep) {
        this.deps.push(dep)
        dep.addSub(this)
      },
      update: jasmine.createSpy()
    }
    // observing primitive value
    Dep.target = watcher
    obj.a
    Dep.target = null
    expect(watcher.deps.length).toBe(1) // obj.a
  });

});
複製程式碼

接下來正式實現資料繫結,其中observe的作用是返回一個observer例項,而observer則負責實現資料繫結.

src/observer/index.js

import {
  def, //new
  hasOwn,
  isObject
}
from '../util/index'

export function Observer(value) {
  this.value = value
  this.dep = new Dep()
  this.walk(value)
  def(value, '__ob__', this)
}

export function observe (value){
  if (!isObject(value)) {
    return
  }
  var ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

Observer.prototype.walk = function(obj) {
  var keys = Object.keys(obj)
  for (var i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
  }
}

export function defineReactive (obj, key, val) {
  var dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = val
      if (Dep.target) {
        dep.depend()
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value =  val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
	   val = newVal
      dep.notify()
    }
  })
}
複製程式碼

在上面的程式碼中我們用到了一些工具函式,下面我們就把這些工具函式在單獨的檔案中實現,方便之後其他元件的呼叫。

src/util/index.js

const hasOwnProperty = Object.prototype.hasOwnProperty
//必須對傳入的引數進行判斷,不然obj為null時會報錯
export function hasOwn(obj, key) {
  if (!isObject(obj) && !Array.isArray(obj)) {
    return
  }
  return hasOwnProperty.call(obj, key)
}

export function isObject(obj) {
	return obj !== null && typeof obj === 'object'
}
//給要觀察的物件的_ob_屬性存放Observer物件,標記已觀察
export function def(obj, key, val, enumerable) {
	Object.defineProperty(obj, key, {
		value: val,
		enumerable: !!enumerable,
		writable: true,
		configurable: true
	})
}
複製程式碼

上面已經簡單實現了資料繫結,接下來不妨使用npm run test命令來測試下專案吧。

相關文章