300行代码手写简单vue.js,彻底弄懂MVVM底层原理
当我们对vue的用法较为熟练的时候,但有时候在排查bug的时候还是会有点迷惑。主要是因为对vue各种用法和各种api使用都是只知其然而不知其所以然。这时候我们想到可以去看看源码,但是源码太长,其实我们只要把大概实现流程实现一遍,很多开发中想不明白的地方就会豁然开朗。下面我们就来实现一个简单的vue.js
vue采取数据劫持,配合观察者模式,通过Object.defineProperty() 来劫持各个属性的setter和getter,在数据变动时,发布消息给依赖收集器dep,去通知观察者,做出对应的回调函数,去更新视图。(也就是在getter中收集依赖,在setter中通知依赖更新。)
其实vue主要就是整合Observer,compile和watcher三者,通过Observer来监听 model数据变化表,通过compile来解析编译模板指令,最终利用Watcher搭起observer 和compile的通信桥梁,达到数据变化=>视图变化,视图变化=>数据变化的双向绑定效果。
下面来一张图↓
这个流程图已经非常形象深刻的表达了vue的运行模式,当你理解了这个流程,再去看vue源码时就会容易很多了
声明一下,下面的代码只简单实现了vue里的
- v-model(数据的双向绑定)
- v-bind/v-on
- v-text/v-html
- 没有实现虚拟dom,采用文档碎片(createDocumentFragment)代替
- 数据只劫持了Object,数组Array没有做处理
代码大致结构如下,初步定义了6个类
代码如下,具体操作案例可以看==>GitHub
// 定义Vue类class Vue {
constructor(options) {
// 把数据对象挂载到实例上
this.$el = options.el;
this.$data = options.data;
this.$options = options;
// 如果有需要编译的模板
if (this.$el) {
// 数据劫持 就是把对象的所有属性 改成get和set方法
new Observer(this.$data);
// 用数据和元素进行编译
new Compiler(this.$el, this);
// 3. 通过数据代理实现 主要给methods里的方法this直接访问data
this.proxyData(this.$data);
}
}
//用vm代理vm.$data
proxyData(data){
for(let key in data){
Object.defineProperty(this,key,{
get(){
return data[key];
},
set(newVal){
data[key] = newVal;
}
})
}
}
}
// 编译html模板
class Compiler {
// vm就是vue对象
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if(this.el){ // 如果该元素能获取到,我们开始编译
// 1.把真实的dom放到内存中fragment文档碎片
let fragment = this.node2fragment(this.el);
// console.log(fragment);
// 2.编译 => 提取想要的元素节点 v-model和文本节点{{}}
this.compile(fragment);
// 3.把编译好的fragment再放到页面里
this.el.appendChild(fragment);
}
}
/* 一些辅助方法 */
isElementNode(node) {
return node.nodeType === 1;
}
isDirective(name) { // 判断是不是指令
return name.includes('v-');
}
isEventName(attrName){ // 判断是否@开头
return attrName.startsWith('@');
}
isBindName(attrName){ // 判断是否:开头
return attrName.startsWith(':');
}
/* 核心方法区 */
node2fragment(el){ // 需要将el中的内容全部放到内存中
// 文档碎片
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild){
fragment.appendChild(firstChild);
}
return fragment; // 内存中的节点
}
compile(fragment){
// 1.获取子节点
let childNodes = fragment.childNodes;
// 2.递归循环编译
[...childNodes].forEach(node=>{
if(this.isElementNode(node)){
this.compileElement(node); // 这里需要编译元素
this.compile(node); // 是元素节点,还需要继续深入的检查
}else{
// 文本节点
// 这里需要编译文本
this.compileText(node);
}
});
}
compileElement(node){ // 编译元素
// 带v-model v-html ...
let attrs = node.attributes; // 取出当前节点的属性
// attrs是类数组,因此需要先转数组
[...attrs].forEach(attr=>{
// console.log(attr); // type="text" v-model="content" v-on:click="handleclick" @click=""...
let attrName = attr.name; // type v-model v-on:click @click
if(this.isDirective(attrName)){ // 判断属性名字是不是包含v-
// 取到对应的值放到节点中
let expr = attr.value; // content/变量 handleclick/方法名
// console.log(expr)
let [, type] = attrName.split('-'); // model html on:click
let [compileKey, detailStr] = type.split(':'); // 处理 on: bind:
// node this.vm.$data expr
CompileUtil[compileKey](node, this.vm, expr, detailStr);
// 删除有指令的标签属性 v-text v-html等,普通的value等原生html标签不必删除
node.removeAttribute('v-' + type);
}else if(this.isEventName(attrName)){ // 如果是事件处理 @click='handleClick'
let [, detailStr] = attrName.split('@');
CompileUtil['on'](node, this.vm, attr.value, detailStr);
// 删除有指令的标签属性
node.removeAttribute('@' + detailStr);
}else if(this.isBindName(attrName)){ // 如果是:开头,动态绑定值
let [, detailStr] = attrName.split(':');
CompileUtil['bind'](node, this.vm, attr.value, detailStr);
// 删除有指令的标签属性
node.removeAttribute(':' + detailStr);
}
})
}
compileText(node){ // 编译文本
// 带{{}}
let expr = node.textContent; // 取文本中的内容
let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}}
if(reg.test(expr)){
// node this.$data
// console.log(expr); // {{content}}
CompileUtil['text'](node, this.vm, expr);
}
}
}
// 编译模版具体执行
const CompileUtil = {
getVal(vm, expr){ // 获取实例上对应的数据
expr = expr.split('.'); // [animal,dog]/[animal,cat]
return expr.reduce((prev, next)=>{ // vm.$data.
return prev[next];
}, vm.$data)
},
// 这里实现input输入值变化时 修改绑定的v-model对应的值
setVal(vm, expr, inputValue){ // [animal,dog]
let exprs = expr.split('.'), len = exprs.length;
exprs.reduce((data,currentVal, idx)=>{
if(idx===len-1){
data[currentVal] = inputValue;
}else{
return data[currentVal]
}
}, vm.$data)
},
getTextVal(vm, expr){ // 获取编译文本后的结果
return expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
// console.log(args); // ["{{title}}", "title", 0, "{{title}}"]
// ["{{ animal.dog }}", " animal.dog ", 0, "{{ animal.dog }}-vs-{{ animal.cat }}"]
return this.getVal(vm, args[1].trim());
});
},
text(node, vm, expr){ // 文本处理
let updateFn = this.updater['textUpdater'];
// {{content}} => "welcome to animal world"
let value;
if(expr.indexOf('{{')!==-1){ // dom里直接写{{}}的时候
value = this.getTextVal(vm, expr);
// {{a}} {{b}} 对多个值进行监控
expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
new Watcher(vm, args[1].trim(), ()=>{
// 如果数据变化了,文本节点需要重新获取依赖的属性更新文本中的内容
updateFn && updateFn(node, this.getTextVal(vm, expr));
})
});
}else{ // v-text 的时候
value = this.getVal(vm, expr);
new Watcher(vm, expr, (newVal)=>{
// 当值变化后会调用cb 将新值传递过来
updateFn && updateFn(node, newVal);
});
}
updateFn && updateFn(node, value);
},
html(node, vm, expr) { //
let updateFn = this.updater['htmlUpdater'];
updateFn && updateFn(node, this.getVal(vm, expr));
},
model(node, vm, expr){ // 输入框处理
let updateFn = this.updater['modelUpdater'];
// console.log(this.getVal(vm, expr)); // "welcome to animal world"
// 这里应该加一个监控 数据变化了 应该调用这个watch的callback
new Watcher(vm, expr, (newVal)=>{
// 当值变化后会调用cb 将新值传递过来
updateFn && updateFn(node, newVal);
});
// 视图 => 数据 => 视图
node.addEventListener('input', (e)=>{
this.setVal(vm, expr, e.target.value);
})
updateFn && updateFn(node, this.getVal(vm, expr));
},
on(node, vm, expr, detailStr) {
let fn = vm.$options.methods && vm.$options.methods[expr];
node.addEventListener(detailStr, fn.bind(vm), false);
},
bind(node, vm, expr, detailStr){
// v-bind:src='...' => href='...'
node.setAttribute(detailStr, expr);
},
updater:{
// 文本更新
textUpdater(node, value){
node.textContent = value;
},
// html更新
htmlUpdater(node, value){
node.innerHTML = value;
},
// 输入框更新
modelUpdater(node, value){
node.value = value;
}
}
}
// 观察者
class Observer{
constructor(data){
this.observe(data);
}
observe(data){
// 要对data数据原有属性改成set和get的形式
if(!data || typeof data !== 'object'){ // 不是对象就不劫持了
return
}
// 要劫持 先获取到data的key和value
Object.keys(data).forEach(key=>{
this.defineReactive(data, key, data[key]); // 劫持
this.observe(data[key]); // 深度递归劫持
})
}
// 定义响应式
defineReactive(obj, key, value){
let dep = new Dep();
// 在获取某个值的时候
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可修改
get(){ // 当取值的时候
// 订阅数据变化时,往Dev中添加观察者
Dep.target && dep.addSub(Dep.target);
return value;
},
// 采用箭头函数在定义时绑定this的定义域
set: (newVal)=>{ // 更改data里的属性值的时候
if(value === newVal) return;
this.observe(newVal); // 如果设置新值是对象,劫持
value = newVal;
// 通知watcher数据发生改变
dep.notify();
}
})
}
}
// 观察者的目的就是给需要变化的那个元素增加一个观察者,当数据变化后执行对应的方法
class Watcher{
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先获取一下老的值
this.oldVal = this.getOldVal();
}
// 获取实例上对应的老值
getOldVal(){
// 在利用getValue获取数据调用getter()方法时先把当前观察者挂载
Dep.target = this;
const oldVal = CompileUtil.getVal(this.vm, this.expr);
// 挂载完毕需要注销,防止重复挂载 (数据一更新就会挂载)
Dep.target = null;
return oldVal;
}
// 对外暴露的方法 通过回调函数更新数据
update(){
const newVal = CompileUtil.getVal(this.vm, this.expr);
if(newVal !== this.oldVal){
this.cb(newVal); // 对应watch的callback
}
}
}
// Dep类存储watcher对象,并在数据变化时通知watcher
class Dep{
constructor(arg) {
// 订阅的数组
this.subs = []
}
addSub(watcher){
this.subs.push(watcher);
}
notify(){ // 数据变化时通知watcher更新
this.subs.forEach(w=>w.update());
}
}
以上是 300行代码手写简单vue.js,彻底弄懂MVVM底层原理 的全部内容, 来源链接: utcz.com/z/377736.html