前言
Vue的数据双向绑定,响应式原理,其实就是通过Object.defineProperty()结合发布者订阅者模式来实现的。
我们可以先试着拆分一下Vue的核心模块。
- Vue构造函数,集中以下模块实现MVVM。
- Observer 通过Object.definePropty进行数据劫持
- Dep 发布订阅者,添加观察者者以及在数据发生改变的时候通知观察者
- Watcher 观察者,对数据进行观察以及保存数据修改需要触发的回调
- Compiler 模板编译器,对HTML模板进行编译,提取其中的变量并转化为数据。
对于整个Vue响应式的简单实现来说,在文中并不能做过多的介绍,只能依靠读者自己去试,按照注释来进行理解。推荐的学习方式就是通过本文的实现代码一步一步的自己实现一下,然后再自己实现的基础上自己编写注释。
正文
先看一下我们最终实现的效果吧
HTML
哈哈
{{msg}}
{{a.b}}
JavaScript
new Vue({
data() {
return {
msg: '呃呃呃呃呃',
name: '郝晨光',
a: {
b: 'bbbbb'
}
}
},
methods: {
setName() {
this.msg = '哈哈';
}
},
created() {
this.msg = '郝晨光哈哈';
console.log('实例初始化完成')
},
mounted() {
console.log('DOM挂载完成')
}
}).$mount('#app'); // 此处通过el属性绑定也是没有任何问题的
Vue构造函数
// Vue构造函数
function Vue(options) {
// 如果当前Vue不是通过new 关键字调用,就进行报错
if(!(this instanceof arguments.callee)) {
error('Vue是一个构造函数,必须通过new关键字调用!');
}
// 如果是的话,就接着执行_init方法
this._init(options);
}
// 实例化Vue的方法
Vue.prototype._init = function(options) {
// 先将options保存在Vue的this.$options上
this.$options = options;
// 再拿到对应的data中的值,没有默认为空对象
this.$data = initData(this.$options) || {};
// 拿到对应的方法,没有默认为空对象
this.$methods = this.$options.methods || {};
// 进行数据劫持
new Observer(this.$data);
// 对数据和方法进行代理
proxyData(this, this.$data);
proxyData(this, this.$methods);
// 生命周期created函数
this.$options.created.apply(this);
// 如果有el属性的话,自动调用$mount方法,挂载到DOM节点中
if(this.$options.el) {
this.$mount(this.$options.el);
}
};
// $mount方法,将Vue实例挂载到DOM节点上
Vue.prototype.$mount = function(el) {
// 拿到对应的DOM节点
let $el = typeof el === 'string'
?
document.querySelector(el)
: el.nodeType === 1
?
el
:
error('el必须是一个选择器或者是一个DOM节点!');
// 将DOM保存在$el属性上
this.$el = $el;
// 通过Compiler编译器进行编译
new Compiler(this.$el, this);
// 调用mounted生命周期钩子函数
this.$options.mounted.apply(this);
// 返回当前的Vue实例,保证外部能够拿到正确的Vue实例
return this;
};
// 初始化Vue实例的data
function initData(options) {
// 拿到data的数据类型
const type = typeof options.data;
// 如果是function的话,调用函数拿到对象,否则直接返回对象
return type === 'function' ? options.data() : options.data;
}
// 对data内的数据进行代理
function proxyData(target, proxy) {
// 拿到对象上的所有key值组成的数组,并进行遍历
Object.keys(proxy).forEach(key => {
// 通过Object.defineProperty方法对数据进行代理
Object.defineProperty(target, key, {
get() {
return proxy[key];
},
set(newValue) {
proxy[key] = newValue;
}
})
});
}
// 错误信息
function error(info) {
throw new Error(info);
}
Observer数据劫持
// 数据劫持
function Observer(data) {
// Observer必须是一个构造函数,如果不是通过new关键字调用的话,
// 在内部使用new关键字。
if(!(this instanceof arguments.callee)) {
return new arguments.callee(data);
}
// 如果data不是一个对象的话,提示错误,
// 因为只有对象才能调用Object.defineProperty
if(!data || typeof data !== 'object') {
error('代理的data必须是一个对象')
}
// 调用observe方法
this.observe(data);
}
Observer.prototype.observe = function(data) {
if(!data || typeof data !== 'object') {
return;
}
// 获取对象上的键值数组并对它进行遍历
Object.keys(data).forEach(key => {
// 调用数据劫持方法
this.defineReactive(data, key, data[key]);
// 判断如果当前的值还是对象的话,递归劫持
if(typeof data[key] === 'object') {
this.observe(data[key]); // 递归劫持所有的值
}
})
};
Observer.prototype.defineReactive = function(data, key, value) {
// 保存this
const _this = this;
// 添加观察者
let dep = new Dep();
// 数据劫持
Object.defineProperty(data, key, {
enumerable: true, // 可枚举的
configurable: true, // 可删除的
// 代理get
get() {
// 当前Dep.target是指的Watcher(订阅者)实例,
// 向dep实例中添加Watcher实例
Dep.target && dep.addSub(Dep.target);
return value;
},
// 代理set
set(newValue) {
// 如果新的值和旧的值不相等的情况下
if(newValue !== value) {
// 重新调用observe劫持数据
_this.observe(newValue);
// 设置新的值
value = newValue;
// dep实例通知订阅者进行修改
dep.notify();
}
}
})
};
Dep发布者
// Dep发布者将要执行的函数统一存储在一个数组中管理,
// 当达到某个执行条件时,循环这个数组并执行每一个成员。
function Dep() {
this.subs = [];
}
// 在发布者Dep实例上添加订阅者
Dep.prototype.addSub = function(watcher) {
this.subs.push(watcher);
};
// 通知订阅者进行修改
Dep.prototype.notify = function() {
// 遍历所有的订阅者,调用订阅者上的update方法进行修改。
this.subs.forEach(watcher => watcher.update());
};
Watcher订阅者
// 订阅者
function Watcher(vm, variable, callback) {
// 保存vm实例
this.vm = vm;
// 保存需要修改的属性
this.variable = variable;
// 保存属性修改时需要触发的回调
this.callback = callback;
// 保存属性的初始值,并将当前订阅者添加到发布者上
this.value = this.get();
}
Watcher.prototype.get = function() {
// 将当前的 watcher 添加到Dep发布者的静态属性上
Dep.target = this;
// 获取到当前的属性值
let value = CompilerUtil.getValue(this.vm, this.variable);
// 在Dep发布者的静态属性上清除当前 watcher
Dep.target = null;
// 返回拿到的值
return value;
};
Watcher.prototype.update = function() {
// 发生修改的时候,重新获取值
let newValue = CompilerUtil.getValue(this.vm, this.variable);
// 先获取旧的值
let oldValue = this.value;
// 如果两个值不等的话,调用修改DOM的回调函数
if(newValue !== oldValue) {
this.callback(newValue);
}
};
Compiler模板编译器
// Compiler模板编译器
function Compiler(el, vm) {
// 先拿到需要编译的DOM节点
this.el = el.nodeType === 1 ? el : document.querySelector(el);
// 拿到当前的vm实例
this.vm = vm;
// 如果当前的el存在,就开始编译
if(this.el) {
// 将真实的DOM转换为文档碎片
let fragment = this.vNodeFragment(this.el);
// 调用compile方法进行编译
this.compile(fragment);
// 编译完成之后再添加到真实DOM中
this.el.appendChild(fragment);
}
}
// DOM文档片段
Compiler.prototype.vNodeFragment = function(el) {
// 创建文档片段
let fragment = document.createDocumentFragment();
let firstChild;
// 遍历当前所有的DOM子节点
while (firstChild = el.firstChild) {
// 将真实DOM节点添加到文档片段中
fragment.appendChild(firstChild);
}
// 返回虚拟文档片段
return fragment;
};
// 进行编译
Compiler.prototype.compile = function(fragment) {
// 拿到文档片段的所有子节点
// 必须通过childNodes拿,因为childNodes不会忽略文本节点。
let children = fragment.childNodes;
// 转换为真实数组并进行遍历
Array.prototype.slice.call(children).forEach(node => {
// 如果当前是元素节点的话,继续递归遍历,并编译元素节点
if(node.nodeType === 1) {
this.compile(node); // 对当前节点内的子节点进行递归遍历
this.compileElement(node); // 编译元素节点
}else {
// 否则是文本节点,就开始编译文本
this.compileText(node);
}
})
};
// 编译元素节点
Compiler.prototype.compileElement = function (node) {
// 获取到元素所有的属性
let attrs = node.attributes;
// 转换为真实数组并进行遍历
Array.prototype.slice.call(attrs).forEach(attr => {
// 获取到当前的属性名
let attrName = attr.name;
// 判断当前的属性是否是指令
if(attrName.includes('v-')) {
// 如果是指令的话,拿到当前的属性值
let value = attr.value;
// 拿到当前的指令名
let [,type] = attrName.split('-');
// 对当前指令执行编译
CompilerUtil[type](node, this.vm, value);
// 判断当前属性是否是事件
}else if(attrName.includes('@')) {
// 拿到事件名称
let event = attrName.slice(1);
// 拿到事件需要触发的方法名称
let method = attr.value;
// 对当前元素添加DOM事件
CompilerUtil.addEvent(node, event, method, this.vm);
}
})
};
// 编译文本节点
Compiler.prototype.compileText = function (node) {
let content = node.textContent; // 获取文本节点的内容
let reg = /\{\{(.+?)\}\}/g; // 匹配模板编译器的内容
// 如果能匹配到模板编译器
if(reg.test(content)) {
// 编译文本节点
CompilerUtil.text(node, this.vm, content);
}
};
模板编译工具
// 模板编译工具对象
const CompilerUtil = {
// 文本编译的回调函数
textUpdater(node, value) {
node.textContent = value;
},
// input编译的回调函数
modelUpdater(node, value) {
node.value = value;
},
// 获取vm实例中对应的值
getValue(vm, variable) {
// 获取对象的属性
variable = variable.split('.');
// 通过reduce方法递归遍历vm.$data,拿到最终在vm实例中的属性值
return variable.reduce((prev, next) => prev[next], vm.$data);
},
// 获取文本中变量对应的内容
getTextValue(vm, variable) {
// 通过正则匹配,拿到属性名
let reg = /\{\{([^}]+)\}\}/g;
return variable.replace(reg, ($0, $1) => {
// 通过属性名,调用getValue方法,获取属性值
return this.getValue(vm, $1);
})
},
// 设置Value
setValue(vm, variable, newValue) {
// 获取对象的属性名
variable = variable.split('.');
// 通过reduce方法遍历
return variable.reduce((prev, next, index) => {
// 如果当前是匹配的属性名的话
if(index === variable.length - 1) {
// 给当前的属性设置值
return prev[next] = newValue;
}
// 如果不是就返回继续计算
return prev[next];
}, vm.$data);
},
// 双向数据绑定 v-model的简单实现
model(node, vm, variable) {
// 获取到双向数据绑定的修改方法
let updateFn = this.modelUpdater;
// 获取到对应的值
let value = this.getValue(vm, variable);
// 添加订阅者, 给订阅者添加回调
new Watcher(vm, variable, newValue => {
// 当数据发生修改的时候,就触发当前回调,修改元素节点的值
updateFn && updateFn(node, newValue);
});
// 将v-model属性从DOM节点上删除
node.removeAttribute('v-model');
// 给当前元素节点添加input事件
node.addEventListener('input', e => {
// 拿到对应的值
let value = e.target.value;
// 设置值
this.setValue(vm, variable, value);
});
// 初次渲染的时候,也要设置一次值
updateFn && updateFn(node, value);
},
// 添加事件
addEvent(node, event, method, vm) {
// 给元素删除事件符
node.removeAttribute('@'+event);
// 给元素添加事件
node.addEventListener(event, (...args) => {
// 调用vm上的方法,并传入参数
vm[method].apply(vm, args);
})
},
// 编译文本节点的变量
text(node, vm, variable) {
// 文本节点的修改函数
let updateFn = this.textUpdater;
// 获取到文本节点变量的值
let value = this.getTextValue(vm, variable);
// 定义正则
let reg = /\{\{(.+?)\}\}/g;
// 通过正则匹配变量,给变量添加观察者
variable.replace(reg, ($0, $1) => {
// 当解析模板遇到变量的时候,应该使用观察者监听这个变量
new Watcher(vm, $1, newValue => {
// 观察者的回调函数,当数据发生改变就触发该回调
updateFn && updateFn(node, newValue);
})
});
// 第一次设置值
updateFn && updateFn(node, value);
}
};
结束
参考文章链接:
一起学习、手写MVVM框架
前端 实现一个简易版的vue,了解vue的运行机制
JS实现一个简易版的vue
如果本文对您有帮助,可以看看本人的其他文章:
前端常见面试题(十六)@郝晨光
前端常见面试题(十五)@郝晨光
前端常见面试题(十四)@郝晨光