时下三大框架当道,应该在国内主要是react和vue,互相借鉴,各有特点,相较之下本人可能更喜欢vue,因为确实更加简洁,尤其喜欢双向数据绑定和计算属性等语法。所以想手动实现一下其中基本原理,便于理解。
本文主要借鉴sf上的一篇文章,原文写的非常清晰,可以看看
本文同时发布在csdn博客上,代码也放在了github上。在正式敲代码之前先来做些准备
Object.defineProperty()
首先来看vue其中的一个核心语法Object.defineProperty(),在双向数据绑定和计算属性等都有用到
var obj = {
firstname: 'a',
lastname: 'b'
};
//定义一个新属性
Object.defineProperty(obj, 'fullname', {
//属性描述符
//数据描述符
// configurable: false, //该属性描述符能否更改,属性能否修改,默认为false
//enumerable: false, //可否枚举(列举,遍历),默认为false
// value: 'b-c', //该属性值,默认为undefined
// writable: false, //value能否被改变,默认为false
//访问描述符
get: function(){ //读取属性值
return this.firstname + '-' + this.lastname;
},
set: function(value){ //监视属性值的变化
this.firstname = value.split('-')[0];
this.lastname = value.split('-')[1];
}
})
obj.firstname = 'b';
obj.fullname = 'x-y';
//输出 x y x-y
console.log(obj.firstname, obj.lastname, obj.fullname);
//输出 ['firstname', 'lastname'],没有'fullname',因为enumerable默认为false
console.log(Object.keys(obj));
描述符同时存在的情况
参考文章
根据字符串获取对象中的值
简单理解,当前有对象 data = {name: 'sam', age: [18, 22], obj: {a: 1, b: 2}} ,如果要得到name属性值,可以用data['name'],但data里面嵌套的obj对象的属性值却不能使用data['obj.a']得到,所以需要封装函数来实现
字符串中含有 '.'
function getValue(keyStr, data){ var val = data; var keys = keyStr.split('.'); keys.forEach(function(key){ val = val[key]; }); return val; }
以上方法只适用字符串中只用到点语法的,像data['age[0]']则无法获取,那么
使用eval()或new Function()
//eval() console.log(eval('data.age[0]')) //18 console.log(eval('data.obj.b')) //2 //new Function() function getValue (data, key) { return new Function('x', 'return x.' + key)(data) } console.log(getValue(data, 'age[0]')) //18 console.log(getValue(data, 'obj.b')) //2
注意:eval()总是不被推荐使用,原因自行了解
正式代码开始,先来看一张流程图
先来实现一个mvvm构造函数
function MVVM(options){
this._options = options;
this._data = options.data || {}; //配置选项中的data
var vm = this;
//数据代理,将data对象中的属性添加到vm实例上
Object.keys(this._data).forEach(function(key){
vm._proxy(vm._data, key);
});
//如果配置选项中有methods则将methods里的函数添加到vm实例上
if(typeof options.methods === 'object'){
Object.keys(options.methods).forEach(function(key){
vm._proxy(options.methods, key);
})
}
//数据劫持,观察者监视数据变化
observe(this._data);
//编译模板,指令与双大括号等
new Compile(options.el ? options.el : document.body, this);
}
//代理数据
MVVM.prototype._proxy = function(obj, key){
Object.defineProperty(this, key, {
configurable: false,
enumerable: true,
get: function(){
return obj[key];
},
set: function(value){
obj[key] = value;
}
});
}
该mvvm函数原型上只写了一个 _proxy函数,主要作用是将配置选项中的data数据添加到vm实例上,便于操作。
数据劫持,观察者监视数据变化
observer.js文件
function observe(data){
if(!data || typeof data !== 'object'){ //如果没有数据或者数据不是对象则不用递归
return;
}
Object.keys(data).forEach(function(key){
hijackData(data, key, data[key]);
})
}
//数据劫持
function hijackData(data, key, val){
var dep = new Dep(); //管理订阅者
observe(data[key]); //递归观察嵌套对象
Object.defineProperty(data, key, {
configurable: false,
enumerable: true,
get: function(){
Dep.target && dep.addSub(Dep.target);//如果Dep.target有值则证明当前是订阅者在取值,这时添加订阅者
return val;
},
set: function(value){
val = value; //这里value不能直接赋值给data[key],否则会报错
dep.notify(); //当值改变时通知订阅者
}
})
}
//依赖(管理订阅者)
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
上面代码主要实现,将vm实例上的 _data对象属性重新定义,注意内部嵌套对象(像 data: {obj: {a:1}})的属性也要重新定义,然后在定义属性的get()中添加订阅者,在set()通知订阅者。这里要注意的是,要添加一个标识(这里用到Dep.target)来判断当前是订阅者在取值才添加订阅者
订阅者
watcher.js文件
function Watcher(vm, exp, cb){
this.$vm = vm; //vm实例
this.exp = exp; //取值表达式
this.cb = cb; //回调
this.value = this.getValue();
}
Watcher.prototype.getValue = function(){
Dep.target = this; //作为是订阅者取值的标识
var value = this.getVMValue(this.exp, this.$vm);
Dep.target = null; //添加订阅者之后移除标识
return value;
}
Watcher.prototype.update = function(){
var newValue = this.getVMValue(this.exp, this.$vm);
//如果新旧值不相等则执行回调函数重新渲染
if(newValue !== this.value){
this.value = newValue;
this.cb();
}
}
//获取vm实例上的数据,对象嵌套取值
Watcher.prototype.getVMValue = function(keyStr, vm){
return new Function('vm', 'return vm.' + keyStr)(vm);
}
订阅者第一次取值的时候将该订阅者实例(watcher)赋值给Dep.target,然后观察者将该watcher添加到subs数组中,当监视的数据变化时dep实例会通知watcher调用update方法,然后判断值是否改变再执行更新dom的回调函数。
模板编译,指令与双大括号
compile.js
function Compile(el, vm){
this.$vm = vm; //需要vm实例
this.$el = this.isElementNode(el) ? el : document.querySelector(el); //根节点
this.$fragment = this.nodeToFragment(this.$el); //文档碎片
this.init(this.$fragment); //编译模板
this.$el.appendChild(this.$fragment); //挂载到dom元素上
}
//创建文档碎片
Compile.prototype.nodeToFragment = function(el){
var fragment = document.createDocumentFragment();
while(el.firstChild){
fragment.appendChild(el.firstChild);
}
return fragment;
}
//编译模板
Compile.prototype.init = function(node){
var nodes = node.childNodes;
if(nodes.length){ //如果有子节点
for(var i = 0; i < nodes.length; i++){
if(this.isElementNode(nodes[i])){ //如果是元素节点
this.compileElementNode(nodes[i]); //编译元素节点
this.init(nodes[i]); //递归
}else if(this.isTextNode(nodes[i])){ //如果是文本节点
this.compileTextNode(nodes[i]); //编译文本节点
}
}
}
}
//编译文本节点
Compile.prototype.compileTextNode = function(node){
var text = node.textContent;
var reg = /\{\{.+\}\}/; //双大括号{{}}的正则
if(reg.test(text)){ //如果存在{{}}
updater.braces(node, text, this.$vm);
var me = this; //回调函数内部this指向问题
text.replace(/\{\{(.+?)\}\}/g, function(){
var keyStr = arguments[1].trim(); //正则子匹配的值
new Watcher(me.$vm, keyStr, function(){
updater.braces(node, text, me.$vm);
})
return me.getVMValue(keyStr, me.$vm);
})
}
}
//编译元素节点
Compile.prototype.compileElementNode = function(node){
var attrs = node.attributes; //标签属性对象集合
var me = this;
Array.prototype.slice.call(attrs).forEach(function(attr){
var attrName = attr.name;
if(attrName.indexOf('v-') === 0){ //如果元素标签属性函数'v-'即为指令
var attrValue = node.getAttribute(attrName);
if(attrName.indexOf('on') > 0){ //指令属性键名含有'on'即为事件指令
me.eventDirective(node, attrName, attrValue, me.$vm); //事件指令
}else{ //否则为一般指令
var directive = attrName.slice(2); //指令名
if(directiveUtil[directive]){ //如果存在该指令才执行
directiveUtil[directive](node, me.$vm, attrName, attrValue);
}
}
}
})
}
//是否元素节点
Compile.prototype.isElementNode = function(node){
return node.nodeType === 1;
}
//是否文本节点
Compile.prototype.isTextNode = function(node){
return node.nodeType === 3;
}
//获取vm实例上的数据,对象嵌套取值
Compile.prototype.getVMValue = function(keyStr, vm){
return new Function('vm', 'return vm.' + keyStr)(vm);
}
//事件指令
Compile.prototype.eventDirective = function(node, attrName, attrValue, vm){
var eventName = attrName.split(':')[1];
var fn = this.getVMValue(attrValue, vm);
if(fn){
node.addEventListener(eventName, fn.bind(vm), false); //元素监听事件,回调函数内部指向vm实例
node.removeAttribute(attrName);
}
}
//指令工具
var directiveUtil = {
//v-text
text: function(node, vm, attrName, attrValue){
this.bind(node, vm, attrName, attrValue, 'text');
},
//v-html
html: function(node, vm, attrName, attrValue){
this.bind(node, vm, attrName, attrValue, 'html');
},
//v-class
class: function(node, vm, attrName, attrValue){
this.bind(node, vm, attrName, attrValue, 'class');
},
//v-model
model: function(node, vm, attrName, attrValue){
this.bind(node, vm, attrName, attrValue, 'model');
node.addEventListener('input', function(){
var value = node.value;
// 用new Function()来执行表达式字符串
new Function('vm', 'value', 'console.log(vm.'+attrValue+'= value)')(vm, value);
}, false);
},
//用于给指令添加订阅者
bind: function(node, vm, attrName, attrValue, funName){
//初始化界面
updater[funName] && updater[funName](node, vm, attrValue); //如果存在则执行
var exp = attrValue;
if(attrValue.indexOf('?') > 0 && attrValue.indexOf(':') > 0){ //如果标签属性值是三元表达式
exp = attrValue.slice(0, attrValue.indexOf('?')).trim(); //表达式?前的变量
}
//添加订阅者
new Watcher(vm, exp, function(){
updater[funName] && updater[funName](node, vm, attrValue); //如果监视的值变了才执行
});
//移除html指令属性
node.removeAttribute(attrName);
}
}
//更新工具,数据变化时会调用的函数
var updater = {
//双大括号
braces: function(node, text, vm){
node.textContent = text.replace(/\{\{(.+?)\}\}/g, function(){
var keyStr = arguments[1].trim(); //正则子匹配的值
return Compile.prototype.getVMValue(keyStr, vm);
});
},
//v-text
text: function(node, vm, attrValue){
var value = Compile.prototype.getVMValue(attrValue, vm);
node.textContent = value;
},
//v-html
html: function(node, vm, attrValue){
var value = Compile.prototype.getVMValue(attrValue, vm);
node.innerHTML = value;
},
//v-class
class: function(node, vm, attrValue){
var newClass = '';
if(attrValue.indexOf('?') > 0 && attrValue.indexOf(':') > 0){ //如果是三元表达式
var variable = attrValue.slice(0, attrValue.indexOf('?')).trim();//表达式?前的变量
var val = Compile.prototype.getVMValue(variable, vm); //表达式?前的变量的值
var expression = attrValue.replace(/.+\?/, val+' ?'); //新的三元表达式
newClass = new Function("return "+ expression)(); //得到新的类名
expression = attrValue.replace(/.+\?/, !val+' ?'); //旧的三元表达式
oldClass = new Function("return "+ expression)(); //得到旧的类名
}else{
newClass = Compile.prototype.getVMValue(attrValue, vm); //否则类名在vm实例中找
}
var classNameStr = node.className;
if(classNameStr){ //如果元素原来有类名
var classNames = node.className.split(' ');
if(classNames.indexOf(oldClass) >= 0){ //如果旧类名存在
classNames.splice(classNames.indexOf(oldClass), 1); //去除旧类名
classNameStr = classNames.join(' ');
}
node.className = classNameStr + ' ' + newClass;
}else{
node.className = newClass;
}
},
//v-model
model: function(node, vm, attrValue){
node.value = Compile.prototype.getVMValue(attrValue, vm);
}
}
模板编译也是关键的一步,这里代码有点多,虽然只是简单地实现了v-text,v-html,v-model,v-class和v-on:的事件指令以及 {{}},其中只有v-class可以使用简单的三元表达式,其他的都只能是data的属性。顺便一提的根据表达式字符串取值vue源码里面有很完善的方法实现,而我这里为了简便就直接使用new Function()。好了,以上代码就可以简单实现vue中的像数据劫持、双向数据绑定的一些核心原理。
最后再加上html文件
Document
{{msg}}---{{hello}}---{{arr[2]}}
哈哈哈
哈哈哈