//先看一例子 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)
click here 可运行的 example
(以下都是简化的代码,实际的源码复杂许多)首先是让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