vue源码分析
分析vue作为一个MVVM框架的基本实现原理
一. 数据代理
- 数据代理: 通过一个对象代理对另一个对象(在前一个对象内部)中属性的操作(读/写)
- vue 数据代理: 通过 vm 对象来代理 data 对象中所有属性的操作
// 相当于vue的构造函数
function MVVM(options) {
//将配置对象保存到vm
this.$options = options;
//将data对象保存到vm和变量data中
var data = (this._data = this.$options.data);
//保存vm到变量me中
var me = this;
//遍历data中所有的属性
Object.keys(data).forEach(function (key) {
//key是data的某个属性名:name
//对指定的属性实现代理
me._proxy(key);
});
}
MVVM.prototype = {
//这个方法是在原型上面的,来实现数据代理
//实现指定属性代理的方法
_proxy: function (key) {
//保存vm
var me = this;
//给vm添加指定属性名的属性
Object.defineProperty(me, key, {
configurable: false, //不能重新定义
enumerable: true, //可以枚举遍历
//当通过vm.xxx读取属性值时调用,从data中获取对应的属性值返回 代理读操作
get: function proxyGetter() {
return me._data[key];
},
//当通过vm.xxx=value时,value被保存到data中对应的属性上
set: function proxySetter(newVal) {
me._data[key] = newVal;
},
});
},
};
第一个:尽量将当前断点执行掉,当碰到其他断点暂停
第二个:当步执行,执行下一条语句,一句句执行
第三个:进入函数内部执行
二. 模板解析
1.模板解析的基本流程
- 将 el 的所有子节点取出, 添加到一个新建的文档 fragment 对象中
- 对 fragment 中的所有层次子节点递归进行编译解析处理
对大括号表达式文本节点进行解析
对元素节点的指令属性进行解析
事件指令解析 * 一般指令解析 - 将解析后的 fragment 添加到 el 中显示
2.模板解析(三种):
2.1大括号表达式解析
(1) 根据正则对象得到匹配出的表达式字符串: 子匹配/RegExp.$1 name (取出name)
(2) 从 data 中取出表达式对应的属性值 (value)
(3) 将属性值设置为文本节点的 textContent(textContent)
2.2事件指令解析:
(1) 从指令名中取出事件名
(2) 根据指令的值(表达式)从 methods 中得到对应的事件处理函数对象
(3) 给当前元素节点绑定指定事件名和回调函数的 dom 事件监听
(4) 指令解析完后, 移除此指令属性
2.3一般指令解析:
(1) 得到指令名和指令值(表达式) text/html/class msg/myClass
(2) 从 data 中根据表达式得到对应的值
(3) 根据指令名确定需要操作元素节点的什么属性
- v-text—textContent 属性
- v-html—innerHTML 属性
- v-class–className 属性
(4) 将得到的表达式的值设置到对应的属性上
(5) 移除元素的指令属性
compile.js
// 相当于vue的构造函数
function MVVM(options) {
//创建了一个编译对象,用来编译解析模板
this.$compile = new Compile(options.el || document.body, this);
}
function Compile(el, vm) {
// 保存vm到compile对象
this.$vm = vm;
//将el对应的元素对象保存到compile对象中
// 保存el元素,是一个dom元素,通过isElementNode寻找是否是一个元素节点,不是的话再去寻找
this.$el = this.isElementNode(el) ? el : document.querySelector(el);
// 如果el元素存在
if (this.$el) {
//编译模板的整体三步
//1. 取出el元素中所有子节点保存到一个fragment对象中(转移)
this.$fragment = this.node2Fragment(this.$el);
//2. 编译fragment中所有层次的节点(编译)
this.init();
//3. 将编译好的fragment添加到页面的el元素中(添加回去)
this.$el.appendChild(this.$fragment);
}
}
Compile.prototype = {
//将节点转化为Fragment
node2Fragment: function (el) {
//1. 创建空的fragment
var fragment = document.createDocumentFragment(),
child;
//2. 把页面el里面所有的子节点都转移到fragment中
while ((child = el.firstChild)) {
fragment.appendChild(child);
}
//3. 返回fragment
return fragment;
},
init: function () {
// 编译指定元素(所有层次的子节点)
this.compileElement(this.$fragment);
},
//编译el里面的所有子节点
compileElement: function (el) {
// 取出最外层所有子节点
var childNodes = el.childNodes,
// 保存compile对象(this是compile实例)
me = this;
//遍历所有子节点(text/element),[].slice.call:将伪数组转化成数组
[].slice.call(childNodes).forEach(function (node) {
// 得到节点的文本内容
var text = node.textContent;
// 正则对象,来匹配大括号表达式
var reg = /\{\{(.*)\}\}/; // {{name}}
// 判断节点是否为元素节点
if (me.isElementNode(node)) {
// 编译元素节点(解析指令,查看是哪个指令的)
me.compile(node);
}
// 判断节点是否为大括号表达式的文本节点
else if (me.isTextNode(node) && reg.test(text)) {
//编译大括号表达式文本节点,RegExp.$1是取出来的匹配的表达式name
me.compileText(node, RegExp.$1); // RegExp.$1: 表达式 name
}
// 如果当前节点还有子节点,通过递归调用实现所有层次节点的编译
if (node.childNodes && node.childNodes.length) {
// 递归调用实现所有层次节点的编译
me.compileElement(node);
}
});
},
compile: function (node) {
// 得到标签的所有属性节点
var nodeAttrs = node.attributes,
me = this;
// 遍历所有属性
[].slice.call(nodeAttrs).forEach(function (attr) {
// 得到属性名: v-on:click
var attrName = attr.name;
// 判断是否是指令属性
if (me.isDirective(attrName)) {
// 得到表达式(属性值): test
var exp = attr.value;
// 从属性名中得到指令名: on:click
var dir = attrName.substring(2);
// 是否是事件指令
if (me.isEventDirective(dir)) {
// 解析处理事件指令
compileUtil.eventHandler(node, me.$vm, exp, dir);
// 普通指令
} else {
// 解析普通指令
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
}
// 移除指令属性
node.removeAttribute(attrName);
}
});
},
compileText: function (node, exp) {
// 调用编译工具对象解析
compileUtil.text(node, this.$vm, exp);
},
isDirective: function (attr) {
return attr.indexOf("v-") == 0;
},
isEventDirective: function (dir) {
return dir.indexOf("on") === 0;
},
isElementNode: function (node) {
return node.nodeType == 1;
},
isTextNode: function (node) {
return node.nodeType == 3;
},
};
// 包含多个解析指令的方法的工具对象
var compileUtil = {
// 解析: v-text/也包括{{}}
text: function (node, vm, exp) {
this.bind(node, vm, exp, "text");
},
// 解析: v-html
html: function (node, vm, exp) {
this.bind(node, vm, exp, "html");
},
// 解析: v-model
model: function (node, vm, exp) {
this.bind(node, vm, exp, "model");
var me = this,
val = this._getVMVal(vm, exp);
node.addEventListener("input", function (e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
me._setVMVal(vm, exp, newValue);
val = newValue;
});
},
// 解析: v-class
class: function (node, vm, exp) {
this.bind(node, vm, exp, "class");
},
// 真正用于解析指令的方法
bind: function (node, vm, exp, dir) {
/*实现初始化显示*/
// 根据指令名(text)得到对应的更新节点函数(得到更新节点的函数,dir是指令名)
var updaterFn = updater[dir + "Updater"];
// 调用函数来更新节点
updaterFn && updaterFn(node, this._getVMVal(vm, exp));
// 创建表达式对应的watcher对象
new Watcher(vm, exp, function (value, oldValue) {
/*更新界面*/ // 当对应的属性值发生了变化时, 自动调用, 更新对应的节点
updaterFn && updaterFn(node, value, oldValue);
});
},
// 事件处理
eventHandler: function (node, vm, exp, dir) {
// 得到事件名/类型: click
var eventType = dir.split(":")[1],
//从methods中得到表达式所对应的函数(事件回调函数)
// 根据表达式得到事件处理函数(从methods中): test(){}
fn = vm.$options.methods && vm.$options.methods[exp];
// 如果都存在
if (eventType && fn) {
//给节点绑定指定事件名和回调函数(强制绑定this为vm)的DOM事件监听
// 绑定指定事件名和回调函数的DOM事件监听, 将回调函数中的this强制绑定为vm
node.addEventListener(eventType, fn.bind(vm), false);
}
},
// 从vm中得到表达式所对应的值
_getVMVal: function (vm, exp) {
var val = vm._data;
exp = exp.split(".");
exp.forEach(function (k) {
val = val[k];
});
return val;
},
_setVMVal: function (vm, exp, value) {
var val = vm._data;
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属性值
textUpdater: function (node, value) {
node.textContent = typeof value == "undefined" ? "" : value;
},
// 更新节点的innerHTML属性值
htmlUpdater: function (node, value) {
node.innerHTML = typeof value == "undefined" ? "" : value;
},
// 更新节点的className属性值
classUpdater: function (node, value, oldValue) {
//静态class属性值
var className = node.className;
//将静态class属性值与动态class值进行合并后设置为新的className的属性值
node.className = className + (className ? " " : "") + value;
},
// 更新节点的value属性值
modelUpdater: function (node, value, oldValue) {
node.value = typeof value == "undefined" ? "" : value;
},
};
三. 数据绑定
1.数据绑定
一旦更新了 data 中的某个属性数据, 所有界面上直接使用或间接使用了此属性的节点都会更新
2.数据劫持
(1) 数据劫持是 vue 中用来实现数据绑定的一种技术
(2) 基本思想: 通过 defineProperty()来监视 data 中所有属性(任意层次)数据的变化, 一旦变化就去更新界面
在上面数据代理中是给vm的xxx添加get和set的方法,而这里是给data中的属性绑定get和set的方法,当我们使用this.xxx=999,去更改xxx的值,此时vm中set会去改变data中xxx的值
总结
MVVM分为两个部分:初始化和更新
初始化 会创建observer和complie,observer是给data中所有层次的属性都使用数据劫持的方法使用(object.definedProperty)添加get和set方法。同时给每一个属性创建一个Dep;
Compile中去解析指令或大括号表达式之后调用updater方法实现初始化视图。同时也给每一个表达式创建一个watcher,watcher会与dep建立关系,把watcher保存到dep的中。
更新阶段: 会触发observer中对应属性的set方法调用,之后会去通知dep,此时里面中有对应的watcher,再去通知所有相关的watcher,watcher调用updater方法去更新视图。
v-model,双向数据绑定
- 双向数据绑定是建立在单向数据绑定(model==>View)的基础之上的
- 双向数据绑定的实现流程:
a. 在解析 v-model 指令时, 给当前元素添加 input 监听
b. 当 input 的 value 发生改变时, 将最新的值赋值给当前表达式所对应的 data 属性
在input标签中输入内容,下面视图也会变化
要怎么做才能把input里面数据同步到data中?(实际上就是监听,要明白它是怎么去解析v-model的)
以上(2.a、2.b)
<input type="text" v-model="msg" />
//相当于(vue写法)
<input type="text" :value="msg" @input="handleInput" />
handleInput(e) {
this.msg = e.target.value;
console.log(e.target.value);
},