逛掘金看到篇讲Vue双向绑定的文章 ,很不错,就拿过来了,里面的代码抄了一遍,当然不是初抄啦,加入我自己的理解和总结。虽然看了几次这类文章,但真不嫌多,温故而知新嘛。
原理 Vue的双向绑定通过 Object对象的defineProperty属性,重写data的set和get函数来实现。
简单实现
1 2 3 4 5 6 7 <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的方式来使用我们的双向数据绑定,结合我们的数据结构添加注释
1 2 3 4 5 6 7 8 9 10 11 var app = new MyVue ({ el : '#app' , data : { number : 0 }, methods : { increment ( ) { this .number ++ } } })
首先定义一个MyVue构造函数
1 function MyVue (options ) {}
为了初始化这个构造函数,给它添加一 个_init属性
1 2 3 4 5 6 7 8 9 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 }
接下来实现_observe函数,对data进行处理,重写data的set和get函数,并改造 _init函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 MyVue .prototype ._observe = function (obj ) { var value for (key in obj) { if (obj.hasOwnProperty (key)) { value = obj[key] if (typeof value === '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元素的更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function Watcher (name, el, vm, exp, attr ) { this .name = name this .el = el this .vm = vm this .exp = exp this .attr = attr this ._update () } Watcher .prototype ._update = function ( ) { this .el [this .attr ] = this .vm .$data [this .exp ] }
更新_init函数以及observe函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 MyVue .ptototype ._init = function (options ) { this ._binding = {} } 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 ) { item._update () }) } } }) } }
那么如何将 view 与 model进行绑定呢? 接下来我们定义一个_compiler函数,用来解析我们的指令(v-bind, v-model, v-click)等,并在这个过程中与view与model进行绑定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 MyVue .prototype ._init = function (options ) { this ._compile (this .$el ) } 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.chilren .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.target === 'textarea' )) { node.addEventListener ('input' , (function (key ) { var attrVal = node.getAttribute ('v-model' ) _this._bniding [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' )) } } }
附上完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 <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 this .el = el this .vm = vm this .exp = exp this .attr = attr 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 >
总结 最后来梳理一下逻辑:
写一个构造函数 MyVue 可传入一个options参数,实例化时执行其 _init方法
_init 其核心是将 options的各种属性 放到其实例上,并执行 _observe 和 _compile 方法
_observe 作为一个代理方法,监听传入的options.data属性的改变,(这里会将data里的每个key放入 _bingding 对象中,然后用Object.defineProperty对每一个key进行监听, 若属性值改变了,就实时改变)
上面说的实时改变就是 update 方法,那观察的对象从哪来? 指令上对应的值? 这一过程其实就是 _compile做的. 让 MyVue能知道 el 上的指令代表什么
_compile让我们知道了指令是干嘛的,具体触发改变就是由Watcher来做的了,我们传入name(指令名), el(对应dom), vm(MyVue实例), exp(指令对应值), attr(绑定的属性),然后调用其原型上的update方法。 触发了 _observe其最终是执行 update方法来更改值
说得好像很啰嗦,但这就是双向绑定的过程了,知根知底,这样又往前进了一小步咯。以上就实现了文本与input的双向绑定
4.17更新~ 添加完整可运行的 es6版
理下思路:
MyVue实例时将传入的options的给实例,完成初始化
_binding 进行依赖收集,每次设置会触发 Watcher 实例的update
_observe 监听data数据,实现数据响应式化
_compile 将模版编译为抽象语法树AST
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 class MyVue { constructor (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 ) } _observe (obj ) { for (let key in obj) { if (obj.hasOwnProperty (key)) { this ._binding [key] = { _directives : [] } console .log ('this._binding[key]' , this ._binding [key]) let value = obj[key] if (typeof value === 'object' ) { this ._observe (value) } let binding = this ._binding [key] Object .defineProperty (this .$data , key, { enumerable : true , configurable : true , get ( ) { console .log (`${key} 获取${value} ` ) return value }, set (newVal ) { console .log (`${key} 设置${newVal} ` ) if (value !== newVal) { value = newVal binding._directives .forEach (item => item.update ()) } } }) } } } _compile (root ) { let _this = this let nodes = root.children for (let i = 0 ; i < nodes.length ; i++) { let node = nodes[i] if (node.children .length ) { this ._compile (node) } if (node.hasAttribute ('v-click' )) { node.onclick = (function ( ) { let 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 ) { let attrVal = nodes[i].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' )) { let attrVal = nodes[i].getAttribute ('v-bind' ) _this._binding [attrVal]._directives .push (new Watcher ( 'text' , node, _this, attrVal, 'innerHTML' )) } } } } class Watcher { constructor (name, el, vm, exp, attr ) { this .name = name this .el = el this .vm = vm this .exp = exp this .attr = attr this .update () } update ( ) { this .el [this .attr ] = this .vm .$data [this .exp ] } }