从一张图开始:
vue的整个内部实现原理都可以抽象成这样一张图,在这张图中,对于抽象Virtual DOM Tree暂不进行实现,以直接操作DOM代替抽象映射的过程。(该图来自掘金小册,侵删。https://juejin.im/book/5a36661851882538e2259c0f/section/5a37bbb35188257d167a4d64)
1.实现功能:
- 数据劫持
- 模板编译(以v-model和{ {}}实现为例
2.文件目录:
- mvvm.html(所谓视图文件,大家都懂)
- mvvm.js(整合数据劫持与模板编译部分)
- compile.js(模板编译)
- observer.js(数据劫持)
- watcher.js(处理依赖)
3.具体实现:
- Observer类:
- 实现数据劫持,为data中每一项属性设置存取器方法。
- observer(data)递归遍历data对象的每一项属性。
- defineReactive(obj,key,value)对每一项属性进行存取器设置,基于Object.defineProperty实现。
Object.defineProperty(obj,key,{ get(){}; set(){};})复制代码
- 模板编译,对html文件中的'v-'指令以及{ {}}进行编译,并实现相应的数据更新updater。
- 先将html文档推入到fragment文档碎片中进行管理,此行为将清空页面,这也就是平时用vue进行开发时会出现也页面闪烁的原因。v-cloak即是用display:none将页面的初次渲染去掉,避免闪烁。
- 递归将html节点推入fragment
let fragment=document.createDocumentFragment();let firstChild; //首个子节点 while(firstChild=el.firstChild){//注意文本节点的坑 空格换行 #text fragment.appendChild(firstChild); //此行为将直接转移页面中的节点}复制代码
-
- compile对fragment中的内容进行编译
- 对节点进行递归遍历,根据nodeType是否为1分为元素节点(可使用指令)和文字节点({ {...}}),分别采用不同的编译方法。
- compileElement用
node.attributes
取出元素的属性列表,对属性进行遍历,将属性为‘v-’开头的指令拿出来,寻找对应的数据更新方法CompileUtil[type](node,this.vm,expr);
。 - compileText用
node.textContent
拿出文本节点的内容,用正则/\{\{([^}]+)\}\}/g
进行匹配,是否满足{ {}}语法,若匹配则执行文本节点的数据更新方法CompileUtil['text'](node,this.vm,expr);
。 - compileUtil对象管理和执行数据更新。
- 对于文本节点:这里我们传入表达式时传入的是类似{ {message.a}}的形式,所以需要用正则换掉两边的括号
return expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{ arguments[1]即是我们需要的值});复制代码
- 同时从data中取出相应的值,这里我们需要处理常见的一种情况,比如message.a.b,所以不能直接data[message]进行取值,这里用reduce进行遍历,取出真实的值。
expr.reduce((prev,next)=>{ return prev[next];},vm.$data)复制代码
- 而对于元素节点,则可以直接用reduce取出真实的值。
- 之后就是执行更新方法了,因为我们并没有实现virtual DOM,这里就是很简单操作DOM改变值就行。例如更新文本时
updater && updater(node,this.getVal(vm,expr));textUpdater(node,value){ node.textContent=value;},复制代码
3.Watcher类
-
- 数据监听,观察到数据变化时通知编译,影响视图,对视图上出现的表达式进行依赖收集,在第一次初始化渲染视图时就为每一个出现的数据确定依赖。
- 依赖收集:我们这里的watcher指的就是视图上的节点对象,将他加入依赖,我们这里的做法是,在编译的时候即在compile中new一个Watcher对象出来,然后在watcher中这样处理:
Dep.target=this;let value=Watcher.getVal(this.vm,this.expr);//触发getDep.target=null;复制代码
- 将属于此节点的watcher,他长这样:
this.vm=vm;this.expr=expr;this.cb=cb;this.value=this.get();//伪造数据读取,将Watcher对象存入订阅者数组中复制代码
- 如上面那段代码中,我们将Dep.target指向watcher实例,然后主动触发数据的get方法,将Dep.target指向的watcher对象进行处理,加入到相应的数据的订阅者数组中。
get(){ Dep.target && dep.addSub(Dep.target); return value;}复制代码
- 完成依赖处理。
4.Dep类
-
- 就是我们所谓的发布订阅者。
- 他很简单,长这样。
class Dep{ constructor(){//订阅的数组 this.subs=[]; } addSub(watcher){ this.subs.push(watcher); } notify(){ this.subs.forEach(watcher=>watcher.update()); }}复制代码
- addSub用来将watcher加入到相应数据的和观察者数组中,subs及用来存储watcher。noyify即遍历数组中每一个观察者,并执行相应的更新方法update。而update方法则指向实例化Watcher对象时传入的cb回调函数。
update(){ this.cb();//触发视图更新}复制代码
- cb即是相应元素对应的更新方法。
new Watcher(vm,expr,()=>{ updater && updater(node,this.getVal(vm,expr)); })复制代码
这样我们的框架就完成了,写这篇博客初衷是为整理自己的思路,如果有错误或不合适还希望指出。
这里是仓库地址:https://github.com/sugarhaining/Book_List/tree/master/javascript/MVVM