这是本人的学习的记录,由于最近在准备面试,不少状况下会被提问到:请简述 mvvm
? 通常状况下我可能这么答:mvvm
是视图和逻辑的一个分离,是model view view-model
的缩写,经过虚拟dom的方式实现双向数据绑定(我随便答得)javascript
那么问题来了,你知道 mvvm
是怎么实现的? 回答: mvvm
主要经过 Object
的 defineProperty
属性,重写 data
的 set
和get
函数来实现。 ok,回答得60分,那么你知道具体实现过程么?想一想看,就算他没问到而你答了出来是否是更好?前提下,必定要手撸一下简单的mvvm
才会对它有印象~html
话很少说,接下来是参考自张仁阳老师的教学视频而做,采用的是ES6语法,其中也包含了我我的的理解,若是能帮助到您,我将十分高兴。若有错误之处,请各位大佬指正出来,不胜感激~~~vue
在实现以前,请先了解基本的mvvm
的编译过程以及使用java
编译的流程图 node
总体分析 git
能够发现new MVVM()
后的编译过程主体分为两个部分:es6
Compile
<div>我很帅</div>
不执行编译Observer
Dep
发布订阅,将全部须要通知变化的data
添加到一个数组中Watcher
若是数据发生改变,在Object
的defineProperty
的set
函数中调用Watcher
的update
方法Vue
实例中的属性能够正确绑定在标签中,而且渲染在页面中
{{}}
node.textContent
或者input
的value
编译出来observe
类劫持数据变化Object.defineProperty
在get
钩子中addSub
,set
钩子中通知变化dep.notify()
dep.notify()
调用的是Watcher
的update
方法,也就是说须要在input
变化时调用更新先明确咱们的目标是:视图的渲染和双向的数据绑定以及通知变化!步骤:先从怎么使用Vue入手一步步解析,从入口类Vue到编译compile 目标【实现视图渲染】,在此以前还有observe对数据进行劫持后再调用视图的更新,watcher 类监听变化到最后通知全部视图的更新等等。github
如何入手?首先从怎么使用Vue
开始。让咱们一步步解析Vue
的使用:面试
let vm = new Vue({
el: '#app'
data: {
message: 'hello world'
}
})
复制代码
上面代码能够看出使用Vue
,咱们是先new
一个Vue
实例,传一个对象参数,包含 el
和 data
。json
ok,以上获得了信息,接下来让咱们实现目标1:将Vue
实例的data
编译到页面中
先看看页面的使用:index.html
<div id="app">
<input type="text" v-model="jsonText.text">
<div>{{message}}</div>
{{jsonText.text}}
</div>
<script src="./watcher.js"></script>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./vue.js"></script>
<script> let vm = new Vue({ el: '#app', data: { message: 'gershonv', jsonText:{ text: 'hello Vue' } } }) </script>
复制代码
第一步固然是添加
Vue
类做为一个入口文件。
新建一个vue.js
文件,其代码以下 构造函数中定义$el
和$data
,由于后面的编译要使用到
class Vue {
constructor(options) {
this.$el = options.el; // 挂载
this.$data = options.data;
// 若是有要编译的模板就开始编译
if (this.$el) {
// 用数据和元素进行编译
new Compile(this.$el, this)
}
}
}
复制代码
obeserve
,实现目标1暂时未用到,后续再添加el
和相关数据,上面代码执行后会有编译,因此咱们新建一个执行编译的类的文件这里在入口文件
vue.js
中new
了一个Compile
实例,因此接下来新建compile.js
Compile
须要作什么? 咱们知道页面中操做dom
会消耗性能,因此能够把dom
移入内存处理:
dom
移入到内存中 (在内存中操做dom
速度比较快)
fragment
compile(fragment){}
v-model
{{}}
,而后进行相关操做。fragment
塞回页面里去class Compile {
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)
// 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{}}
this.compile(fragment)
// 3.编译好的fragment在塞回页面里去
this.el.appendChild(fragment)
}
}
/* 专门写一些辅助的方法 */
isElementNode(node) { // 判断是否为元素及节点,用于递归遍历节点条件
return node.nodeType === 1;
}
/* 核心方法 */
node2fragment(el) { // 将el的内容所有放入内存中
// 文档碎片
let fragment = document.createDocumentFragment();
while (el.firstChild) { // 移动DOM到文档碎片中
fragment.appendChild(firstChild)
}
return fragment;
}
compile(fragment) {
}
}
复制代码
补充:将el
中的内容移入文档碎片fragment
中是一个进出栈的过程。el 的子元素被移到fragment
【出栈】后,el
下一个子元素会变成firstChild
。
编译的过程就是把咱们的数据渲染好,表如今视图中
{{}}
isElementNode
表明是节点元素,也是递归的终止的判断条件。compileElement
和 编译文本{{}}
的方法
compileElement
对v-model
、v-text
等指令的解析compileText
编译文本节点 {{}}
class Compile{
// ...
compile(fragment) {
// 遍历节点 可能节点套着又一层节点 因此须要递归
let childNodes = fragment.childNodes
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) {
// 是元素节点 继续递归
// 这里须要编译元素
this.compileElement(node);
this.compile(node)
} else {
// 文本节点
// 这里须要编译文本
this.compileText(node)
}
})
}
}
复制代码
node.attributes
先判断是否包含指令v-html v-text v-model...
) 调用不同的数据更新方法
CompileUtil
CompileUtil[type](node, this.vm, expr)
CompileUtil.类型(节点,实例,v-XX 绑定的属性值)
class Compile{
// ...
// 判断是不是指令 ==> compileElement 中递归标签属性中使用
isDirective(name) {
return name.includes('v-')
}
compileElement(node) {
// v-model 编译
let attrs = node.attributes; // 取出当前节点的属性
Array.from(attrs).forEach(attr => {
let attrName = attr.name;
// 判断属性名是否包含 v-
if (this.isDirective(attrName)) {
// 取到对应的值,放到节点中
let expr = attr.value;
// v-model v-html v-text...
let [, type] = attrName.split('-')
CompileUtil[type](node, this.vm, expr);
}
})
}
compileText(node) {
// 编译 {{}}
let expr = node.textContent; //取文本中的内容
let reg = /\{\{([^}]+)\}\}/g;
if (reg.test(expr)) {
CompileUtil['text'](node, this.vm, expr)
}
}
// compile(fragment){...}
}
CompileUtil = {
getVal(vm, expr) { // 获取实例上对应的数据
expr = expr.split('.'); // 处理 jsonText.text 的状况
return expr.reduce((prev, next) => {
return prev[next] // 譬如 vm.$data.jsonText.text、vm.$data.message
}, vm.$data)
},
getTextVal(vm, expr) { // 获取文本编译后的结果
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
return this.getVal(vm, arguments[1])
})
},
text(node, vm, expr) { // 文本处理 参数 [节点, vm 实例, 指令的属性值]
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(vm, expr)
updateFn && updateFn(node, value)
},
model(node, vm, expr) { // 输入框处理
let updateFn = this.updater['modelUpdater'];
updateFn && updateFn(node, this.getVal(vm, expr))
},
updater: {
// 文本更新
textUpdater(node, value) {
node.textContent = value
},
// 输入框更新
modelUpdater(node, value) {
node.value = value;
}
}
}
复制代码
到如今为止 就完成了数据的绑定,也就是说new Vue
实例中的 data
已经能够正确显示在页面中了,如今要解决的就是如何实现双向绑定
结合开篇的vue
编译过程的图能够知道咱们还少一个observe
数据劫持,Dep
通知变化,添加Watcher
监听变化, 以及最终重写data
属性
vue.js
中劫持数据class Vue{
//...
if(this.$el){
new Observer(this.$data); // 数据劫持
new Compile(this.$el, this); // 用数据和元素进行编译
}
}
复制代码
observer.js
文件代码步骤:
observe
data
是否存在, 是不是个对象(new Vue 时可能不写data
属性)data
中的key
和value
class Observer {
constructor(data) {
this.observe(data)
}
observe(data) {
// 要对这个数据将原有的属性改为 set 和 get 的形式
if (!data || typeof data !== 'object') {
return
}
// 将数据一一劫持
Object.keys(data).forEach(key => {
// 劫持
this.defineReactive(data, key, data[key])
this.observe(data[key]) //递归深度劫持
})
}
defineReactive(obj, key, value) {
let that = this
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() { // 取值时调用的方法
return value
},
set(newValue) { // 当给data属性中设置的时候,更改属性的值
if (newValue !== value) {
// 这里的this不是实例
that.observe(newValue) // 若是是对象继续劫持
value = newValue
}
}
})
}
}
复制代码
虽然有了
observer
,可是并未关联,以及通知变化。下面就添加Watcher
类
新建watcher.js
文件
先回忆下watch
的用法:this.$watch(vm, 'a', function(){...})
咱们在添加发布订阅者时须要传入参数有: vm实例,v-XX绑定的属性, cb回调函数 (getVal
方法拷贝了以前 CompileUtil
的方法,其实能够提取出来的...)
class Watcher {
// 观察者的目的就是给须要变化的那个元素增长一个观察者,当数据变化后执行对应的方法
// this.$watch(vm, 'a', function(){...})
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 先获取下老的值
this.value = this.get();
}
getVal(vm, expr) { // 获取实例上对应的数据
expr = expr.split('.');
return expr.reduce((prev, next) => { //vm.$data.a
return prev[next]
}, vm.$data)
}
get() {
let value = this.getVal(this.vm, this.expr);
return value
}
// 对外暴露的方法
update(){
let newValue = this.getVal(this.vm, this.expr);
let oldValue = this.value
if(newValue !== oldValue){
this.cb(newValue); // 对应 watch 的callback
}
}
}
复制代码
Watcher
定义了可是尚未调用,模板编译的时候,须要调观察的时候观察一下 Compile
class Compile{
//...
}
CompileUtil = {
//...
text(node, vm, expr) { // 文本处理 参数 [节点, vm 实例, 指令的属性值]
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(vm, expr)
updateFn && updateFn(node, value)
expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
new Watcher(vm, arguments[1], () => {
// 若是数据变化了,文本节点须要从新获取依赖的属性更新文本中的内容
updateFn && updateFn(node, this.getTextVal(vm, expr))
})
})
},
//...
model(node, vm, expr) { // 输入框处理
let updateFn = this.updater['modelUpdater'];
// 这里应该加一个监控,数据变化了,应该调用watch 的callback
new Watcher(vm, expr, (newValue) => {
// 当值变化后会调用cb 将newValue传递过来()
updateFn && updateFn(node, this.getVal(vm, expr))
});
node.addEventListener('input', e => {
let newValue = e.target.value;
this.setVal(vm, expr, newValue)
})
updateFn && updateFn(node, this.getVal(vm, expr))
},
//...
}
复制代码
实现了监听后发现变化并无通知到全部指令绑定的模板或是{{}}
,因此咱们须要Dep
监控、实例的发布订阅属性的一个类,咱们能够添加到observer.js
中
注意 第一次编译的时候不会调用Watcher
,dep.target
不存在,new Watcher
的时候target
才有值 有点绕,看下面代码:
class Watcher {
constructor(vm, expr, cb) {
//...
this.value = this.get()
}
get(){
Dep.target = this;
let value = this.getVal(this.vm, this.expr);
Dep.target = null;
return value
}
//...
}
// compile.js
CompileUtil = {
model(node, vm, expr) { // 输入框处理
//...
new Watcher(vm, expr, (newValue) => {
// 当值变化后会调用cb 将newValue传递过来()
updateFn && updateFn(node, this.getVal(vm, expr))
});
}
}
复制代码
class Observer{
//...
defineReactive(obj, key, value){
let that = this;
let dep = new Dep(); // 每一个变化的数据 都会对应一个数组,这个数组存放全部更新的操做
Object.defineProperty(obj, key, {
//...
get(){
Dep.target && dep.addSub(Dep.target)
//...
}
set(newValue){
if (newValue !== value) {
// 这里的this不是实例
that.observe(newValue) // 若是是对象继续劫持
value = newValue;
dep.notify(); //通知全部人更新了
}
}
})
}
}
class Dep {
constructor() {
// 订阅的数组
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
复制代码
以上代码 就完成了发布订阅者模式,简单的实现。。也就是说双向绑定的目标2已经完成了
板门弄斧了,本人无心哗众取宠,这只是一篇个人学习记录的文章。想分享出来,这样才有进步。 若是这篇文章帮助到您,我将十分高兴。有问题能够提issue
,有错误之处也但愿你们能提出来,很是感激。
具体源码我放在了个人github了,有须要的自取。 源码连接