Vue.js中的指令指的是v-model
、v-if
、v-on
等由v-
开头的属性,本文会结合前面介绍的数据响应性原理来实现v-model
指令。
Vue类的构建 在使用Vue的时候,我们是这样创建Vue实例的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var vm = new Vue ({ el : "#app" , data : { a : 1 , b : { c : 2 } }, watch : { a (newVal, oldVal ) { console .log (`a改变了,新值是${newVal} ,旧值是${oldVal} ` ) } } })
可以看到,Vue的构造器接收一个对象,对象里包含创建实例时的data
、methods
、元素、watch
等选项,然后把接收到的data
、watch
等变成实例自身的data
、watch
。同时,在前面介绍的数据响应性原理中的observe
函数在这里也会排上用场,在构造器中就会把data
变成响应性的:
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 import { observe } from "./observe" ;import Watcher from './Watcher.js' export default class Vue { constructor (options ) { this .$options = options || {}; this ._data = options.data || undefined ; observe (this ._data ); this ._initData (); this ._initWatch (options.watch ); } _initData ( ) { var self = this ; Object .keys (this ._data ).forEach (key => { Object .defineProperty (self, key, { get ( ) { return self._data [key]; }, set (value ) { self._data [key] = value } }) }) } _initWatch (watch ) { var self = this ; Object .keys (watch).forEach (v => { new Watcher (self, v, watch[v]) }) } }
除了处理data
、watch
,还要处理接收到的el
,我们可以新建一个Compile
类来处理el
。
1 2 3 4 5 6 7 8 9 constructor (options ) { this .$options = options || {}; this ._data = options.data || undefined ; observe (this ._data ); this ._initData (); this ._initWatch (options.watch ); this .el = new Compile (options.el , this ); }
Compile
类上面看到Compile
类的构造器接收一个选择器,还有当前的Vue实例。接收到选择器后,可以直接使用document.querySelector
来获取DOM上的节点,然后把节点添加进DocumentFragment
里,对DocumentFragment
进行编译后,再上树。
DocumentFragment可以理解成虚拟节点,如果把DOM上的节点添加到DocumentFragment里,该节点就会下树,如果要让它重新上树,那就要将DocumentFragment上树。
在构造器中我们使用node2Fragment函数将节点变成DocumentFragment。然后用compile函数对DocumentFragment进行处理,再使用appendChild
把DocumentFragment上树。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 export default class Compile { constructor (el, vue ) { this .$el = document .querySelector (el); this .$vue = vue; if (this .$el ) { var fragment = this .node2Fragment (this .$el ); this .compile (fragment); this .$el .appendChild (fragment) } } }
node2Fragment
在node2Fragment
函数中,对el
节点的子节点进行遍历,将子节点都添加到DocumentFragment中。
1 2 3 4 5 6 7 8 9 node2Fragment (el ) { var fragment = document .createDocumentFragment (); var child; while (child = el.firstChild ) { fragment.appendChild (child); } return fragment; }
compile
compile
函数接收node2Fragment
生成的Fragment
,对它的子节点进行遍历,根据子节点的类型进行编译。如果子节点是一个元素,那就调用compileElement
进行编译;如果是一个带有Mustache语法的文字节点,那就调用compileTextNode
编译:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 compile (el ) { var childNodes = el.childNodes ; var textReg = /\{\{(.*)\}\}/ ; childNodes.forEach (node => { var text = node.textContent ; if (node.nodeType == 1 ) { this .compileElement (node) } else if (node.nodeType == 3 && textReg.test (text)) { let name = text.match (textReg); this .compileTextNode (node, name[1 ]) } }) }
compileElement
compileElement
函数用于编译普通元素,这里主要是要实现v-model
双向绑定指令,我们先要使用node.attributes
获取节点的属性列表,然后遍历属性列表,如果属性名是v-model
,就要获取到属性值,并创建一个Watcher
实例,并获取到Vue对象中性值对应的值,再对该节点添加一个事件监听器,监听元素值的修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 compileElement (node ) { var nodeAttrs = node.attributes ; Array .from (nodeAttrs).forEach (attr => { var name = attr.name ; var value = attr.value ; if (name.indexOf ("v-" ) == 0 ) { var dir = name.substring (2 ) if (dir == "model" ) { new Watcher (this .$vue , value, value => { node.value = value; }) var v = this .getVueVal (this .$vue , value); node.value = v; node.addEventListener ("input" , (e ) => { node.value = e.target .value this .setVueVal (this .$vue , value, e.target .value ) }) } } }) }
compileText
compileText
函数用于编译带有Mustache语法的文字节点,函数接收文字节点和Mustache中的内容,Mustache中的内容要变成Vue实例中对应的值。并且也要在这里新建一个Watcher
实例。在Vue中的值修改时,修改元素中的值。
1 2 3 4 5 6 compileTextNode (node, name ) { node.textContent = this .getVueVal (this .$vue , name) new Watcher (this .$vue , name, (value => { node.textContent = value })); }
getVueVal
、setVueVal
这两个函数用于获取和设置Vue实例中的data
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 getVueVal (vue, key ) { var val = vue; var exp = key.split ("." ); exp.forEach (k => { val = val[k] }) return val; } setVueVal (vue, key, value ) { var val = vue; var exp = key.split ("." ); exp.forEach ((k, i ) => { if (i < exp.length - 1 ) { val = val[k] } else { val[k] = value } }) }
效果 新建一个index.html,引入webpack
生成的bundle.js
,新建一个简单的模板,并新建一个我们自己写的Vue实例:
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 <body > <div id ="app" > {{a}} <br > <input type ="text" v-model ="a" /> <br > <input type ="button" value ="click me" onclick ="add()" > </div > <script src ="virtual/bundle.js" > </script > <script > var vm = new Vue ({ el : "#app" , data : { a : 1 , b : { c : 2 } }, watch : { a (newVal, oldVal ) { console .log (`a改变了,新值是${newVal} ,旧值是${oldVal} ` ) } } }) function add ( ) { vm.a ++; } </script > </body >
运行页面:
修改输入框或者点击按钮都可以修改data
中的a
的值,模板中的Mustache也会相应变化,watch
中的函数也成功运行了。