实现双向绑定
参考自 https://juejin.im/post/5acc17cb51882555745a03f8
原理
Vue的双向绑定通过 Object对象的defineProperty属性,重写data的set和get函数来实现。
简单实现
html
<div id="app">
<form>
<input type="text" v-model="number">
<button type="button" v-click="increment">增加</button>
</form>
<h3 v-bind="number"></h3>
</div>- 一个input,使用v-model指令
- 一个button,使用v-click指令
- 一个h3,使用v-bind指令。
通过类似于vue的方式来使用我们的双向数据绑定,结合我们的数据结构添加注释
js
var app = new MyVue({
el: '#app',
data: {
number: 0
},
methods: {
increment() {
this.number++
}
}
})首先定义一个MyVue构造函数
js
function MyVue(options) {}为了初始化这个构造函数,给它添加一 个_init属性
js
function MyVue(options) {
this._init(options)
}
MyVue.prototype._init = function(options) {
this.$options = options // options为上面使用时传入的结构体,包括el data methods
this.$el = document.querySelector(options.el) // this.$el 即 id为app的 dom元素
this.$data = options.data // this.$data = {number: 0}
this.$methods = options.methods // { increment() {this.number++} }
}接下来实现_observe函数,对data进行处理,重写data的set和get函数,并改造 _init函数
js
MyVue.prototype._observe = function(obj) { // 将要实现监听的对象 obj = {number: 0}
var value
for (key in obj) {
if (obj.hasOwnProperty(key)) {
value = obj[key]
if (typeof value === 'object') { // 若值还是 object类型,则继续遍历
this._observe(value)
}
Object.defineProperty(this.$data, key, { // 关键
enumerable: true,
configurable: true,
get: function() {
console.log(`获取${value}`)
return value
},
set: function(newVal) {
console.log(`更新${newVal}`)
if (value !== newValue) {
value = newVal
}
}
})
}
}
}
MyVue.prototype._init = function(options) {
this.$options = options
this.$el = document.querySelector(options.el)
this.$data = options.data
this.$methods = options.methods
this._observe(this.$data)
}接下来我们写一个指令类 Watcher 用来绑定更新函数,实现对DOM元素的更新
js
function Watcher(name, el, vm, exp, attr) {
this.name = name // 指令名称,例如文件节点,该值设为 text
this.el = el // 指令对应的DOM元素
this.vm = vm // 指令所属的MyVue实例
this.exp = exp // 指令对应的值,本例为 number
this.attr = attr // 绑定的属性值,本例为 innerHTML
this._update()
}
Watcher.prototype._update = function() {
this.el[this.attr] = this.vm.$data[this.exp]
// 比如 h3.innerHTML = this.data.number
// 当number改变时,会触发_update函数,保证对应的DOM进行更新
}更新_init函数以及observe函数
js
MyVue.ptototype._init = function(options) {
/* 省略 */
this._binding = {}
// _binding保存着model与view的映射关系, 也就是我们前面定义的Watcher的实例
// 当model改变时,我们会触发其中的指令类更新,保证view也能实时更新
/* ... */
}
MyVue.prototype._observe = function(obj) {
/* 省略 */
if (obj.hasOwnProperty(key)) {
this._binding[key] = {
_directives: []
}
// ...
var binding = this._binding[key]
Object.defineProperty(this.$data, key, {
// ...
set: function(newVal) {
console.log(`更新${newVal}`)
if (value !== newVal) {
value = newVal
binding._directives.forEach(function(item) {
// 当number改变时,触发_binding[number]._directives 中绑定的Watcher类的更新
item._update()
})
}
}
})
}
/* ... */
}那么如何将 view 与 model进行绑定呢? 接下来我们定义一个_compiler函数,用来解析我们的指令(v-bind, v-model, v-click)等,并在这个过程中与view与model进行绑定
js
MyVue.prototype._init = function(options) {
// 省略
this._compile(this.$el)
}
MyVue.prototype._compile = function(root) { // root为 id为app的element,即根元素
var _this = this
var nodes = root.children
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i]
if (node.chilren.length) { // 若存在子元素则进行递归处理
this._compile(node)
}
// 如果有v-click属性,我们监听它的onclick事件,触发increment事件,即number++
if (node.hasAttribute('v-click')) {
node.onclick = (function() {
var attrVal = nodes[i].getAttribute('v-click')
return _this.$methods[attrVal].bind(_this.$data)
// bind是使data的作用域与method函数的作用域保持一致
})()
}
// 如果有v-model属性,且元素是input或textarea,就监听它的input事件
if (node.hasAttribute('v-model') && (node.tagName === 'input' || node.target === 'textarea')) {
node.addEventListener('input', (function(key) {
var attrVal = node.getAttribute('v-model')
// _this._binding[number]._directives = [一个Watcher的实例]
// 其中Watcher.prototype.update = functoin() {
// node['value'] = _this.$data['number']
// 这就将node的值保持与number一致了
// }
_this._bniding[attrVal]._directives.push(new Watcher(
'input', // 结合上面的 Watcher构造函数来看
node, // 分别传参 指令名, dom
_this, // Vue绑定的实例
attrVal, // 指令对应值
'value' // 绑定的对应属性
))
return function() {
_this.$data[attrVal] = nodes[key].value
// 使number的值与node的value保持一致,这就实现了双向绑定
}
})(i))
}
// 如果有v-bind属性,我们只要使node的值及时更新为data中的number值即可
if (node.hasAttribute('v-bind')) {
var attrVal = node.getAttribute('v-bind')
_this._binding[attrVal]._directives.push(new Watcher(
'text',
node,
_this,
attrVal,
'innerHTML'
))
}
}
}完整实现
最后来梳理一下逻辑:
- 写一个构造函数 MyVue 可传入一个options参数,实例化时执行其 _init方法
- _init 其核心是将 options的各种属性 放到其实例上,并执行 _observe 和 _compile 方法
- _observe 作为一个代理方法,监听传入的options.data属性的改变,(这里会将data里的每个key放入 _bingding 对象中,然后用Object.defineProperty对每一个key进行监听, 若属性值改变了,就实时改变)
- 上面说的实时改变就是 update 方法,那观察的对象从哪来? 指令上对应的值? 这一过程其实就是 _compile做的. 让 MyVue能知道 el 上的指令代表什么,要做怎样的处理
附上完整代码:
html
<div id="app">
<form>
<input type="text" v-model="number">
<button type="button" v-click="increment">增加</button>
</form>
<h3 v-bind="number"></h3>
</div>
<script>
function MyVue(options) {
this._init(options)
}
MyVue.prototype._init = function(options) {
this.$options = options
this.$el = document.querySelector(options.el)
this.$data = options.data
this.$methods = options.methods
this._binding = {}
this._observe(this.$data)
this._compile(this.$el)
}
MyVue.prototype._observe = function(obj) {
var value
for (key in obj) {
if (obj.hasOwnproperty(key)) {
this._binding[key] = {
_directives: []
}
value = obj[key]
if (typeof value === 'object') {
this._observe(value)
}
var binding = this._binding[key]
Object.defineProperty(this.$data, key, {
enumerable: true,
configurable: true,
get() {
console.log(`获取${value}`)
return value
},
set(newVal) {
console.log(`更新${newVal}`)
if (value !== newVal) {
value = newVal
binding._directives.forEach(function(item) {
item.update()
})
}
}
})
}
}
}
MyVue.prototype._compile = function(root) {
var _this = this
var nodes = root.children
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i]
if (node.children.length) {
this._compile(node)
}
if (node.hasAttribute('v-click')) {
node.onclick = (function() {
var attrVal = nodes[i].getAttribute('v-click')
return _this.$methods[attrVal].bind(_this.$data)
})()
}
if (node.hasAttribute('v-model') && (node.tagName === 'input' || node.tagName === 'textarea')) {
node.addEventListener('input', (function(key) {
var attrVal = node.getAttribute('v-model')
_this._binding[attrVal]._directives.push(new Watcher(
'input',
node,
_this,
attrVal,
'value'
))
return function() {
_this.$data[attrVal] = nodes[key].value
}
})(i))
}
if (node.hasAttribute('v-bind')) {
var attrVal = node.getAttribute('v-bind')
_this._binding[attrVal]._directives.push(new Watcher(
'text',
node,
_this,
attrVal,
'innerHTML'
))
}
}
}
function Watcher(name, el, vm, exp, attr) {
this.name = name //指令名称,例如文本节点,该值设为"text"
this.el = el //指令对应的DOM元素
this.vm = vm //指令所属myVue实例
this.exp = exp //指令对应的值,本例如"number"
this.attr = attr //绑定的属性值,本例为"innerHTML"
this.update()
}
Wathcer.prototype.update = function() {
this.el[this.attr] = this.vm.$data[this.exp]
}
window.onload = function() {
var app = new MyVue({
el: '#app',
data: {
number: 0
},
methods: {
increment: function() {
this.number++
}
}
})
}
</script>以上就实现了文本与input的双向绑定
