《深入淺出vue.js》中變化偵測的源碼以及解析
背景
最近由於公司知識體系變動,需要學習vue
來適應新的開發平臺,我選擇了《深入淺出vue.js》
來進行學習,之前有了解過一些vue
知識,這次打算深入學習一下,尤其是第一篇變化偵測,學習過程中對javascript
也增加了不少理解,我把代碼都手打了出來,代碼中有很多按照自己理解打上的註釋,這裏分享給大家。
開門見山
這裏我不給大家解釋他是什麼,書中總結的很好了,我直接把代碼貼出來。下面有兩點值得注意:
- 我使用了
require
以及module.exports
這個是因爲node不支持es6
特性 - 運行環境爲node,需要自行安裝
@ Deppro.js
依賴收集,哪裏引用了reactive的變量,就把他加入Dep
let uid = 0
class Dep {
constructor() {
this.id = uid++
this.subs = [];
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
const index = this.subs.indexOf(sub)
if (index > -1) {
this.subs.splice(index, 1)
}
}
depend() {
if(global.target) {
this.addSub(global.target)
}
}
notify (msg) {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i< l; i++) {
if (msg) {
subs[i].update(msg)
} else {
subs[i].update()
}
}
}
}
module.exports = Dep;
@ common.js
這裏我提取了一些公共方法,使用的時候直接引用即可
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
function parsePath(path) {
const bailRE = /[^\w.$]/
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) {
return // 如果.到當前沒有值,則選擇返回空
}
obj = obj[segments[i]] // 例如返回obj.a,下一次返回(obj.a).b
}
return obj
}
}
module.exports = {
def,
parsePath
}
@ arrayIntercept.js
const {def} = require('./common')
const ArrayProto = Array.prototype
/**
* 對數組默認的增刪方法進行攔截
*/
const ArrayMethods = Object.create(ArrayProto)
;['push','pop','shift','unshift','splice','sort','reverse'].forEach(method => {
// 緩存原始方法
const original = ArrayProto[method]
def(ArrayMethods, method, function mutator (...args) {
const ob = this.__ob__
let inserted
let msg
switch (method) {
case 'push':
msg = 'you just execute push opration'
break;
case 'unshift':
inserted = args
break;
case 'splice':
inserted = args.slice(2) // 獲得args第三個到第n個參數,即增加項
break;
default:
break;
}
if (inserted) ob.observeArray(inserted) // 添加對新增元素的變化偵測
ob.dep.notify(msg)
/**
* 改變this的指向,比如original是push函數,調用的時候肯定是[].push,此時push中的this是指向[]的
* 如果這裏不操作,就會導致this鏈斷裂
*/
return original.apply(this, args)
})
});
module.exports = ArrayMethods
@ Observer.js
這裏面我定義了 Observer 和 Watcher
const { def, parsePath } = require('./common')
const arrayMethods = require('./arrayIntercept')
const Dep = require('../DepPro')
// 是否支持proto
const hasProto = '__proto__' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
/**
* 不能對全局的Array.prototype 進行直接覆蓋,這樣會污染全局的Array
* 實現針對性數組攔截
*/
class Watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
this.deps = []
this.depIds = new Set()
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.cb = cb
this.value = this.get()
}
get() {
global.target = this
let value = this.getter.call(this.vm, this.vm)
global.target = undefined
return value
}
update(msg) {
if (msg) {
// this.cb.call(this.vm, msg)
this.cb(msg)
} else {
const oldValue = this.value
this.value = this.get() // 這裏又會執行一遍Object.definedProperty的get函數
this.cb.call(this.vm, this.value, oldValue)
}
}
addDep(dep) {
const id = dep.id
if (!this.depIds.has(id)) {
this.depIds.add(id)
this.deps.push(dep)
dep.addSub(this)
}
}
teardown() {
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
}
}
/**
* 數組增刪需要在攔截其中才能觸發依賴,setter中並不能監測到,需要同時滿足在getter中和攔截器中都能訪問到(一個push一個notify),所以放到Observe中
*/
class Observer {
constructor(value) {
this.value = value;
this.dep = new Dep() // 一個Observer擁有一個dep
// 將Ovserver實例賦給value.__ob__,第一,標記已響應,第二,通過__ob__拿到Observer實例,就可以在攔截器中調用__ob__上的dep了
def(value, '__ob__', this)
if (Array.isArray(value)) {
this.observeArray(value) // 如果被偵測的數據是數組,則將數組每一個元素都轉換成響應式並偵測變化
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
} else { // 如果是對象,則直接調用walk
this.walk(value)
}
}
observeArray(items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
let a = {
b: [1, 2, 3],
c: 'abk'
}
new Observer(a)
new Watcher(a, 'b', function (msg) {
console.log(this.c)
console.log(msg)
})
a.b.push(4)
console.log(a)
console.log(a.__ob__.value)
/**
* 進一步封裝Observe, 返回Observe實例,如果已經存在則直接返回,不存在返回新建數據
* 數組對象有了實例,代表已經是響應式數據,調用該方法,返回一個被攔截器修改的數組
* 總結一下,通過const a = observe(value) 1、value從此擁有攔截器 2、a.value 等於 value ,a 等於 value.__ob__
* 值得注意的是,value既可以是數組也可以是對象,是兼容的
* @param {數組對象} value
* @param {暫無介紹} asRootData
*/
function observe(value, asRootData) {
if (typeof value !== 'object') {
return null
}
let ob
if (value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
ob = value__ob__
} else {
ob = new Observer(value)
}
return ob
}
function protoAugment(target, src, keys) {
target.__proto__ = src
}
// 如果不支持,將攔截器中的值一個一個添加到數組對象
function copyAugment(target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) { // 只需要機算一次keys.length的值
const key = keys[i]
def(target, key, src[key])
}
}
function defineReactive(data, key, val) {
let childOb = observe(val) // 對對象或者數組返回Observe實例,其他返回空
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend() // 如果傳值爲數組,則此dep監測數組引用變化,下方監測數據增刪改等變化
if (childOb) {
childOb.dep.depend()
}
return val
},
set: function (newVal) {
if (val === newVal) {
return
}
val = newVal
dep.notify()
}
})
}
然後在控制檯環境中運行 node Observer.js
,便可以得到如果所示的運行結果
說到最後
最後想說的是自己的一些總結,變化偵測映射到我們平時使用vue
的時候,就好比在data中聲明瞭變量data,然後vue
會對這個變量使用Observer(data)
,然後當DOM
中有一個地方引用了這個變量,就相當於新建了一個watcher
,並將該watcher
放入data的dep
中,當引用改變,會觸發nofity
方法中從而調用watcher
的update
方法,更新dom
,這就實現了變化偵測。