function Vue(){
this.init(); //初始化
this.compile(); //编译
}
在函数里面,我们竟然看到了this,这个在《JavaScript百炼成仙》的函数七重关有讲过,这说明我们如果要使用这个函数,就得把它new出来。
init方法和compile方法是这个函数的两个实例方法。
关于创建函数的实例方法,这边我们介绍一个新的方式,那就是用prototype。
prototype很迷惑,你可以这样理解它:prototype是函数的一个公共未来对象!
只要这个函数将来在什么地方被new了,prototype有的,那个实例一定有!
所以,我们这样写
Vue.prototype.init = function(){
console.log('所有的Model数据,都被我劫持了!');
}
Vue.prototype.compile = function(){
console.log('页面所有的DOM,都被我窃听了!');
}
怎么用?
这么用:
var vm = new Vue({
/** 需要被vue控制的根元素ID * */
el: 'app' ,
/** Model数据层 * */
data: {
username: 'jack',
password: '123456'
}
});
效果:
我在init方法和compile方法中都打印了可爱的语句,现在你不妨猜一猜这两个阶段分别干了什么,对应下面这张图的哪两个部分?
大家开动脑筋,猜一猜呢。
3.初始化阶段
========
init:所有的Model数据,都被我劫持了!
init函数劫持了Model中所有的数据,当Model中数据发生了set操作,就自动去更新页面。所有,init起到的作用就是 M -> VM -> V
好了,我要劫持数据,那么请问,数据从哪来?
答:Vue函数的参数传进来,哈哈。
function Vue(options){
/** Model层 * */
this.$data = options.data;
this.init();
this.compile();
}
$data是啥,不就是这个:
/** Model数据层 * */
data: {
username: 'jack',
password: '123456'
}
ok,怎么遍历呢?
答:还记得《JavaScript百炼成仙》中叶老教授叶小凡如何遍历对象的法术么?
看代码:
for(let key in this.$data){
}
这段代码是写在init函数里面的,因为是实例方法,它可以直接调用Vue中的实例对象$data。
怎么劫持?
答:用Object.defineProperty,这个方法的意思就是对某个对象的某个key进行劫持。
for(let key in this.$data){
Object.defineProperty(this.$data,key,{
get:function(){
return this.$data[key];
},
set:function(newVal){
this.$data[key] = newVal;
}
})
}
嗯,代码还未写完,毕竟如果就这么点东西,那还劫持个锤子!
我们希望看到的是,当data的某个key发生变化,就去更新DOM。怎么更新呢?再来看下这个图:
从图中可以看到,假如username改变了,页面上有两个地方要变。那我凭什么知道是这两个要变呢?
答:因为vue指令(其实就是元素的属性)
代码如下,我们去掉了id,毕竟不用jQuery那一套了,改成v-model和v-bind:
到这一步,我们发现一个username可能有2个指令(甚至更多),所以需要给每一个key配置一个指令集,用数组比较合适。
function Vue(options){
...
/** 用来给每一个Model属性配置指令集 * */
this.$bindings = {}
...
}
遍历Model属性的时候,初始化指令集
let _this = this;
for(let key in _this.$data){
_this.$bindings[key] = {
directions: []
}
}
最后,在set的时候,去遍历所有指令,更新DOM
let value = _this.$data[key];
Object.defineProperty(_this.$data,key,{
get:function(){
return value;
},
set:function(newVal){
value = newVal;
/** 更新DOM * */
_this.$bindings[key].directions.forEach(watcher => {
watcher.update();
});
}
})
这边我们用了forEach,watcher是数组中的某一个对象,它有一个update方法,目的在于更新DOM元素。上面的代码还有一个小技巧,就是要把**_this.$data[key]单独放到上面用value**存起来,不然会有循环调用的问题。
演示循环调用:
Object.defineProperty(_this.$data,key,{
get:function(){
return _this.$data[key];
},
...
})
因为_this.$data[key]会触发get函数,所以产生了循环调用,直接给你报错:
可是我们只要在外面用value锁住 _this.$data[key]就不会有这个问题了。(emmm......很眼熟是不是,这也算是闭包的一种,即用函数锁住变量)
然后是watcher,_this.$bindings[key].directions中我们会放置很多Watcher对象,这是一个专门用来更新DOM的对象。
Watcher代码如下:
/** 口诀:什么DOM的什么 = 某个Model属性的值* */
function Watcher(dom,expression,vm,dataKey){
this.dom = dom;
this.expression = expression;
this.vm = vm;
this.dataKey = dataKey;
this.update();
}
Watcher.prototype.update = function(){
this.dom[this.expression] = this.vm.$data[this.dataKey];
}
至此,init函数开发完毕,可能有的同学会问,这弄了半天,DOM的指令怎么和Model绑定呢?别急,这就是下一步【编译阶段】该做的事情。
4. 编译阶段
========
编译阶段要做的事情很简单,就是根据el(你传进来的根节点元素),遍历所有的子节点(为了简单我们不做递归),挨个检查vue支持的指令,如果找到了,就将这个元素与Model进行绑定。
function Vue(options){
/** 根节点 * */
this.$el = document.getElementById(options.el);
...
}
上面的代码是获取根节点的DOM对象,然后是具体的编译函数:
Vue.prototype.compile = function(){
let _this = this;
let nodes = this.$el.children;
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i];
if(node.hasAttribute('v-model')){
let dataKey = node.getAttribute('v-model');
/** 如果是v-model指令,就监听DOM的input事件 * */
node.addEventListener('input',function(){
/** 更新Model * */
_this.$data[dataKey] = node.value;
});
/** 添加watcher * */
_this.$bindings[dataKey].directions.push(new Watcher(
node,
'value',
_this,
dataKey
));
}else if(node.hasAttribute('v-bind')){
/** 如果是v-bind指令,只需要添加watcher即可 * */
let dataKey = node.getAttribute('v-bind');
_this.$bindings[dataKey].directions.push(new Watcher(
node,
'innerHTML',
_this,
dataKey
));
}
}
}
5.成果展示
======
刷新页面,会看到这个效果:
当我们随便修改input框的值,右边会产生联动效果,很有趣,建议自己把代码撸一遍尝试一下。
6. 一定要用let,不要用var
==================
用var最大的问题就是变量提升,比如init方法中,假如我们把这个地方改成var
你猜会发生什么?这个我在书里写过,可以根据抽象语法树去判断,当你运行这个函数时,var定义的变量会自动提升到顶端,其实是这样的。
这就会导致我们去使用的时候,value已经变成循环的最后一个了,效果如图:
当data的循环结束,value因为变量提升就自动变成了123456,导致data中所有属性get方法都受到了影响!
get:function(){
return value;
},
value就变成123456了,不管是哪个key。
这与我们预期的效果相悖,虽然可以用闭包来解决这个问题,但是代码会变得很复杂。所以,我们尽量要使用let,不要用var了。
7. 完整代码,可直接运行
==============
Document