//先看一例子
import Vue from './instance/vue'
let v = new Vue({
data:{
a:1,
b:{
c:3
}
}
})
console.log(v.b.c);//3
v.$watch("b.c",(newVal,oldVal)=>console.log('newVal',newVal,'oldVal',oldVal,'\n')); //1秒后 newVal { d: [Getter/Setter] } oldVal 3
v.$watch("b.c.d",(newVal,oldVal)=>console.log('newVal',newVal,'oldVal',oldVal,'\n')) //2秒后 newVal 5 oldVal 4
setTimeout(()=>{
v.b.c = {d:4};
},1000)
setTimeout(()=>{
v.b.c.d = 5
},2000)
(以下都是简化的代码,实际的源码复杂许多)首先是让vue本身对data引用,以及添加$watch方法:
import Watcher from '../watcher'
import {observe} from "../observer"
export default class Vue {
constructor (options={}) {
this.$options=options
let data = this._data=this.$options.data
//shallow iterate through keys to give vue direct reference
Object.keys(data).forEach(key=>this._proxy(key))
//deep iterate through all keys to proxy all keys
observe(data,this)
}
$watch(expOrFn, cb, options){
new Watcher(this, expOrFn, cb)
}
_proxy(key) {
var 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
}
})
}
}
接着实现深度代理函数observe,递归代理所有属性,从而监测所有属性的变化:
import {def} from "../util"
import Dep from "./dep"
export default class Observer{
constructor(value) {
this.value = value
this.walk(value)
}
//deep observe each key
walk(value){
Object.keys(value).forEach(key=>this.convert(key,value[key]))
}
convert(key, val){
defineReactive(this.value, key, val)
}
}
export function defineReactive (obj, key, val) {
var dep = new Dep()
//recursive observe all keys
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=>{
// check dependency to subscribe
if(Dep.target){
dep.addSub(Dep.target)
}
return val
},
set:newVal=> {
var value = val
if (newVal === value) {
return
}
val = newVal
//check change to reobserve and publish all dependencies
childOb = observe(newVal)
dep.notify()
}
})
}
export function observe (value, vm) {
if (!value || typeof value !== 'object') {
return
}
return new Observer(value)
}
实现依赖处理:
// import { toArray } from '../util/index'
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*
* @constructor
*/
export default function Dep () {
this.id = uid++
this.subs = []
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
/**
* Add a directive subscriber.
*
* @param {Directive} sub
*/
Dep.prototype.addSub = function (sub) {
this.subs.push(sub)
}
/**
* Remove a directive subscriber.
*
* @param {Directive} sub
*/
Dep.prototype.removeSub = function (sub) {
// this.subs.$remove(sub)
}
/**
* Add self as a dependency to the target watcher.
*/
Dep.prototype.depend = function () {
Dep.target.addDep(this)
}
/**
* Notify all subscribers of a new value.
*/
Dep.prototype.notify = function () {
// stablize the subscriber list first
var subs = (this.subs)
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
实现watch:
import Dep from './observer/dep'
export default class Watcher {
constructor(vm, expOrFn, cb) {
this.cb = cb
this.vm = vm
this.expOrFn = expOrFn
this.value = this.get()
}
update(){
this.run()
}
run(){
const value = this.get()
if(value !==this.value){ //check isEqual if not then callback
this.cb.call(this.vm,value,this.value)
this.value = value
}
}
addDep(dep){
dep.addSub(this)
}
get(){
this.beforeGet();
console.log('\n','watch get');
//fn or expr
var res = this.vm._data,
key = [];
console.log('expOrFn',this.expOrFn)
//to watch instance like a.b.c
if(typeof this.expOrFn == 'string'){
this.expOrFn.split('.').forEach(key=> {
res = res[key] // each will invoke getter, since Dep.target is true ,this will surely add this into dep
})
}
this.afterGet();
return res
}
}
/**
* Prepare for dependency collection.
*/
Watcher.prototype.beforeGet = function () {
Dep.target = this;
};
/**
* Clean up for dependency collection.
*/
Watcher.prototype.afterGet = function () {
Dep.target = null;
};
以上示例是可以无依赖直接运行的。
接下来是vue的源码片段;
一般对象的处理可以直接代理defineProperty就可以了,不过对于Array的各种操作就不管用了,所以vue进行了基本数组方法代理:
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
function indexOf (arr, obj) {
var i = arr.length
while (i--) {
if (arr[i] === obj) return i
}
return -1
}
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
var original = arrayProto[method]
def(arrayMethods, method, function mutator () {
// avoid leaking arguments:
// http://jsperf.com/closure-with-arguments
var i = arguments.length
var args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
var result = original.apply(this, args)
var ob = this.__ob__
var inserted
switch (method) {
case 'push':
inserted = args
break
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
//same as object change
//check change to reobserve and publish all dependencies
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
//es5 defineProperty对于数组的某些操作和属性(如:length)变化代理有问题,所以需要使用定义的方法操作
具体原因看http://www.cnblogs.com/ziyunfei/archive/2012/11/30/2795744.html和
http://wiki.jikexueyuan.com/project/vue-js/practices.html
def(
arrayProto,
'$set',
function $set (index, val) {
if (index >= this.length) {
this.length = Number(index) + 1
}
return this.splice(index, 1, val)[0] // 在里面还是通过代理方法splice实现
}
)
def(
arrayProto,
'$remove',
function $remove (item) {
/* istanbul ignore if */
if (!this.length) return
var index = indexOf(this, item)
if (index > -1) {
return this.splice(index, 1)
}
}
)
看实际的observer干了什么:
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export function Observer (value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (isArray(value)) {
var augment = hasProto //hasProo = '__prop__' in {}; __prop__只在某些浏览器才暴漏出来
? protoAugment // 直接将 value.__proto__ = src
: copyAugment //直接改变本身方法
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
function copyAugment (target, src, keys) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i]
def(target, key, src[key])
}
}
function protoAugment (target, src) {
target.__proto__ = src
}
总的来说,vue是代理了原始数据的增删改查,从而进行事件订阅和发布等操作,从而控制数据流。
heavily inspired by:https://segmentfault.com/a/1190000004384515