您的位置:首页 > Web前端 > Vue.js

vue实现数据双向绑定的原理

2018-07-03 15:07 1056 查看

一、知识准备
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>
阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: