最近由于公司知识体系变动,需要学习vue
来适应新的开发平台,我选择了《深入浅出vue.js》
来进行学习,之前有了解过一些vue
知识,这次打算深入学习一下,尤其是第一篇变化侦测,学习过程中对javascript
也增加了不少理解,我把代码都手打了出来,代码中有很多按照自己理解打上的注释,这里分享给大家。
这里我不给大家解释他是什么,书中总结的很好了,我直接把代码贴出来。下面有两点值得注意:
require
以及 module.exports
这个是因为node不支持es6
特性@ 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
,这就实现了变化侦测。