一、知识准备
Object.defineProperty( )方法可以直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回这个对象。
Object.defineProperty(obj,prop,descriptor),重点是第三个参数,对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。
数据描述符是一个拥有可写或不可写值的属性,存取描述符是由一对getter-setter函数功能来描述的属性。描述符必须二选一,不能同时是两者。
数据描述符和存取描述符均具有以下可选键值:
configurable:当且仅当该属性的 configurable 为 true 时,该属性才能够被改变,也能够被删除。默认为 false。 enumerable:当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。
数据描述符同时具有以下可选键值:
value:该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。 writable:当且仅当仅当该属性的writable为 true 时,该属性才能被赋值运算符改变。默认为 false。
存取描述符同时具有以下可选键值:
get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为undefined。 set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为undefined。
二、监听对象变动
Vue监听数据变化的机制是把一个普通JavaScript对象传给Vue实例的data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setter。
// 观察者构造函数 function Observer (value) { this.value = value this.walk(value) } // 递归调用,为对象绑定getter/setter Observer.prototype.walk = function (obj) { var keys = Object.keys(obj) for (var i = 0, l = keys.length; i < l; i++) { this.convert(keys[i], obj[keys[i]]) } } // 将属性转换为getter/setter Observer.prototype.convert = function (key, val) { defineReactive(this.value, key, val) } // 创建数据观察者实例 function observe (value) { // 当值不存在或者不是对象类型时,不需要继续深入监听 if (!value || typeof value !== 'object') { return } return new Observer(value) } // 定义对象属性的getter/setter function defineReactive (obj, key, val) { var property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // 保存对象属性预先定义的getter/setter var getter = property && property.get var setter = property && property.set var childOb = observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var value = getter ? getter.call(obj) : val console.log("访问:"+key) return value }, set: function reactiveSetter (newVal) { var value = getter ? getter.call(obj) : val if (newVal === value) { return } if (setter) { setter.call(obj, newVal) } else { val = newVal } // 对新值进行监听 childOb = observe(newVal) console.log('更新:' + key + ' = ' + newVal) } }) }
【测试】定义一个对象作为数据模型,并监听这个对象。
let data = { user: { name: 'camille', age: '94' }, address: { city: 'shagnhai' } } observe(data) console.log(data.user.name) // 访问:user // 访问:name data.user.name = 'Camille Hou' // 访问:user // 更新:name = Camille Hou
三、监听数组变动
数组对象无法通过Object.defineProperty实现监听,Vue包含观察数组的变异方法,来触发视图更新。
const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto) function def(obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) } // 数组的变异方法 ;[ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] .forEach(function (method) { // 缓存数组原始方法 var original = arrayProto[method] def(arrayMethods, method, function mutator () { var i = arguments.length var args = new Array(i) while (i--) { args[i] = arguments[i] } console.log('数组变动') return original.apply(this, args) }) })
【测试】定义一个数组作为数据模型,并对这个数组调用变异的七个方法实现监听。
let skills = ['JavaScript', 'Node.js', 'html5'] // 原型指针指向具有变异方法的数组对象 skills.__proto__ = arrayMethods skills.push('java') // 数组变动 skills.pop() // 数组变动
四、数组监听优化
我们可以在上面Observer观察者构造函数中添加对数组的监听。
const hasProto = '__proto__' in {} const arrayKeys = Object.getOwnPropertyNames(arrayMethods) // 观察者构造函数 function Observer (value) { this.value = value if (Array.isArray(value)) { var augment = hasProto ? protoAugment : copyAugment augment(value, arrayMethods, arrayKeys) this.observeArray(value) } else { this.walk(value) } } // 观察数组的每一项 Observer.prototype.observeArray = function (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]) } } // 将目标对象/数组的原型指针__proto__指向src function protoAugment (target, src) { target.__proto__ = src } // 将具有变异方法挂在需要追踪的对象上 function copyAugment (target, src, keys) { for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i] def(target, key, src[key]) } }
五、发布订阅模式
Vue的Watcher订阅者作为Observer和Compile之间通信的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图。
/** * 观察者对象 */ function Watcher(vm, expOrFn, cb) { this.vm = vm this.cb = cb this.depIds = {} if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = this.parseExpression(expOrFn) } this.value = this.get() } /** * 收集依赖 */ Watcher.prototype.get = function () { // 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者 Dep.target = this // 触发getter,将自身添加到dep中 const value = this.getter.call(this.vm, this.vm) // 依赖收集完成,置空,用于下一个Watcher使用 Dep.target = null return value } Watcher.prototype.addDep = function (dep) { if (!this.depIds.hasOwnProperty(dep.id)) { dep.addSub(this) this.depIds[dep.id] = dep } } /** * 依赖变动更新 * * @param {Boolean} shallow */ Watcher.prototype.update = function () { this.run() } Watcher.prototype.run = function () { var value = this.get() if (value !== this.value) { var oldValue = this.value this.value = value // 将newVal, oldVal挂载到MVVM实例上 this.cb.call(this.vm, value, oldValue) } } Watcher.prototype.parseExpression = function (exp) { if (/[^\w.$]/.test(exp)) { return } var exps = exp.split('.') return function(obj) { for (var i = 0, len = exps.length; i < len; i++) { if (!obj) return obj = obj[exps[i]] } return obj } }
Dep是一个数据结构,其本质是维护了一个watcher队列,负责添加watcher,更新watcher,移除 watcher,通知watcher更新。
let uid = 0 function Dep() { this.id = uid++ this.subs = [] } Dep.target = null /** * 添加一个订阅者 * * @param {Directive} sub */ Dep.prototype.addSub = function (sub) { this.subs.push(sub) } /** * 移除一个订阅者 * * @param {Directive} sub */ Dep.prototype.removeSub = function (sub) { let index = this.subs.indexOf(sub); if (index !== -1) { this.subs.splice(index, 1); } } /** * 将自身作为依赖添加到目标watcher */ Dep.prototype.depend = function () { Dep.target.addDep(this) } /** * 通知数据变更 */ Dep.prototype.notify = function () { var subs = toArray(this.subs) // stablize the subscriber list first for (var i = 0, l = subs.length; i < l; i++) { // 执行订阅者的update更新函数 subs[i].update() } }
六、模板编译
compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
function Compile(el, value) { this.$vm = value this.$el = this.isElementNode(el) ? el : document.querySelector(el) if (this.$el) { this.compileElement(this.$el) } } Compile.prototype.compileElement = function (el) { let self = this let childNodes = el.childNodes ;[].slice.call(childNodes).forEach(node => { let text = node.textContent let reg = /\{\{((?:.|\n)+?)\}\}/ // 处理element节点 if (self.isElementNode(node)) { self.compile(node) } else if (self.isTextNode(node) && reg.test(text)) { // 处理text节点 self.compileText(node, RegExp.$1.trim()) } // 解析子节点包含的指令 if (node.childNodes && node.childNodes.length) { self.compileElement(node) } }) } Compile.prototype.compile = function (node) { let nodeAttrs = node.attributes let self = this ;[].slice.call(nodeAttrs).forEach(attr => { var attrName = attr.name if (self.isDirective(attrName)) { let exp = attr.value let dir = attrName.substring(2) if (self.isEventDirective(dir)) { compileUtil.eventHandler(node, self.$vm, exp, dir) } else { compileUtil[dir] && compileUtil[dir](node, self.$vm, exp) } node.removeAttribute(attrName) } }); } Compile.prototype.compileText = function (node, exp) { compileUtil.text(node, this.$vm, exp); } Compile.prototype.isDirective = function (attr) { return attr.indexOf('v-') === 0 } Compile.prototype.isEventDirective = function (dir) { return dir.indexOf('on') === 0; } Compile.prototype.isElementNode = function (node) { return node.nodeType === 1 } Compile.prototype.isTextNode = function (node) { return node.nodeType === 3 } // 指令处理集合 var compileUtil = { text: function (node, vm, exp) { this.bind(node, vm, exp, 'text') }, html: function (node, vm, exp) { this.bind(node, vm, exp, 'html') }, model: function (node, vm, exp) { this.bind(node, vm, exp, 'model') let self = this, val = this._getVMVal(vm, exp) node.addEventListener('input', function (e) { var newValue = e.target.value if (val === newValue) { return } self._setVMVal(vm, exp, newValue) val = newValue }); }, bind: function (node, vm, exp, dir) { var updaterFn = updater[dir + 'Updater'] updaterFn && updaterFn(node, this._getVMVal(vm, exp)) new Watcher(vm, exp, function (value, oldValue) { updaterFn && updaterFn(node, value, oldValue) }) }, eventHandler: function (node, vm, exp, dir) { var eventType = dir.split(':')[1], fn = vm.$options.methods && vm.$options.methods[exp]; if (eventType && fn) { node.addEventListener(eventType, fn.bind(vm), false); } }, _getVMVal: function (vm, exp) { var val = vm exp = exp.split('.') exp.forEach(function (k) { val = val[k] }) return val }, _setVMVal: function (vm, exp, value) { var val = vm; exp = exp.split('.') exp.forEach(function (k, i) { // 非最后一个key,更新val的值 if (i < exp.length - 1) { val = val[k] } else { val[k] = value } }) } } var updater = { textUpdater: function (node, value) { node.textContent = typeof value == 'undefined' ? '' : value }, htmlUpdater: function (node, value) { node.innerHTML = typeof value == 'undefined' ? '' : value }, modelUpdater: function (node, value, oldValue) { node.value = typeof value == 'undefined' ? '' : value } }
七、MVVM实例
/** * @class 双向绑定类 MVVM * @param {[type]} options [description] */ function MVVM(options) { this.$options = options || {} // 简化了对data的处理 let data = this._data = this.$options.data // 监听数据 observe(data) new Compile(options.el || document.body, this) } MVVM.prototype.$watch = function (expOrFn, cb) { new Watcher(this, expOrFn, cb) }
为了能够直接通过实例化对象操作数据模型,我们需要为 MVVM 实例添加一个数据模型代理的方法。
MVVM.prototype._proxy = function (key) { Object.defineProperty(this, key, { configurable: true, enumerable: true, get: () => this._data[key], set: (val) => { this._data[key] = val } }) }
八、举个例子
<div id="J_box"> <h3>{{user.name}}</h3> <input type="text" v-model="modelValue"> <p>{{modelValue}}</p> </div> <script> let vm = new MVVM({ el: '#J_box', data: { modelValue: '', user: { name: 'camille', age: '94' }, address: { city: 'shanghai' }, skills: ['JavaScript', 'Node.js', 'html5'] } }) vm.$watch('modelValue', val => console.log(`watch modelValue :${val}`)) </script>