概述
文章目录
- 介绍
- 1、小试一下
- 2、运行机制
- 3、源码
- 关键点
- 3.1 html.js
- 3.2 vue.js
- 3.3 compile.js
介绍
MVVM可以分解成(Model-View-VIewModel)。ViewModel可以理解为在
presenter基础上的进阶版。
- ViewModel通过实现一套数据响应式机制自动响应Model中数据变化。
- 同时Viewmodel会实现一套更新策略自动将数据变化转换为视图更新。
- 通过事件监听响应View中用户交互修改Model中数据,这样在ViewModel中就减少了大量DOM操作代码。
- MVVM在保持View和Model松耦合的同时,还减少了维护它们关系的代码,使用户专注于业务逻辑,兼顾开发效率和可维护性。
- Vue的双向数据绑定的设计思想为观察者模式。
- Dep对象:Dependency依赖的简写,包含有三个主要属性id, subs, target和四个主要函数addSub,removeSub, depend, notify,是观察者的依赖集合,负责在数据发生改变时,使用notify()触发保存在subs下的订阅列表,依次更新数据和DOM。
- Observer对象:即观察者,包含两个主要属性value, dep。做法是使用getter/setter方法覆盖默认的取值和赋值操作,将对象封装为响应式对象,每一次调用时更新依赖列表,更新值时触发订阅者。绑定在对象的_ ob _原型链属性上。
1、小试一下
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<p>你好,<span id="name"></span></p>
</div>
</body>
<script>
var obj = {}
// Object.defineProperty() 方法会直接在一个对象上定义一个新属性,
// 或者修改一个对象的现有属性, 并返回这个对象。
Object.defineProperty(obj, 'name', {
get: function () {
return document.getElementById('name').innerHTML
},
set: function (inner) {
document.getElementById('name').innerHTML = inner
}
})
console.log(obj.name) // null
obj.name = '张三' // 这里相当于调用了name属性的set方法
console.log(obj.name) // 张三
</script>
</html>
2、运行机制
3、源码
关键点
1、实现一个数据监听Observer,对数据对象的所有属性进行监听,数据发生变化可以获取到最新值通知订阅者。
2、实现一个解析器Compile解析页面节点指令,初始化视图。
3、实现一个观察者Watcher,订阅数据变化同时绑定相关更新函数。并且将自己放入观察者集合Dep中。Dep是Observer和Watcher的桥梁,数据改变通知到Dep,然后Dep通知相应的Watcher去更新视图。
3.1 html.js
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<div v-html="html">html tag</div>
{{text}}
<div v-text="text2"></div>
<input type="text" v-model="text" />
</div>
</body>
<script src="vue.js"></script>
<script src="compile.js"></script>
<script>
const obj = new Vue({
el: '#app',
data: {
text: 'this is a text',
text2: 'this is a v-text content',
html: '<button @click="onClick">click</button>'
},
methods: {
onClick() {
alert('blabla')
}
}
})
</script>
</html>
3.2 vue.js
class Vue {
// 传入配置选项
constructor(options) {
this.$data = options.data // 保存data选项
this.observe(this.$data) // 执行响应式
this.$options = options
this.$compile= new Compile(options.el, this)
}
// 监听属性
observe(value) {
if (!value || typeof value !== 'object') {
return
}
// 遍历data选项
Object.keys(value).forEach(key => {
// 为每一个key定义响应式
this.defineReactive(value, key, value[key])
// 为vue的data做属性代理,方便complie中vm[exp]的使用
this.proxyData(key)
})
}
proxyData(key) {
Object.defineProperty(this, key, {
get(){
return this.$data[key];
},
set(newVal){
this.$data[key] = newVal;
},
});
}
// 定义响应式
defineReactive(obj, key, val) {
// 递归查找嵌套
this.observe(val)
// 创建Dep
const dep = new Dep()
// 为data对象定义属性
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举(可遍历)
configurable: true, // 可修改或删除
get() {
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if (newVal === val) {
return; // 新老数据无变化,不处理
}
val = newVal
dep.notify()
}
})
}
}
// 依赖管理器:负责将视图中所有依赖收集管理,包括依赖添加和通知
class Dep {
constructor() {
this.deps = [] // deps 里面存放的是watcher的实例
}
addDep(dep) {
this.deps.push(dep)
}
// 通知所有watcher执行更新
notify() {
this.deps.forEach(dep => {
dep.update()
})
}
}
// 具体的更新执行者
class Watcher {
constructor(vm, key, callback) {
this.vm = vm
this.key = key
this.callback = callback
// 将来new一个监听器时,将当前watcher实例附加到Dep.target中
Dep.target = this // 因为是弱语言,所以可以直接对Dep增加属性
this.vm[this.key] // 这里会执行get, 就执行了Dep.target && dep.addDep(Dep.target)
Dep.target = null // 避免不必要的重复添加
}
// 更新
update() {
this.callback.call(this.vm, this.vm[this.key]) // .call: 为了上下文(this)正确
//通过 .call(),您能够使用属于另一个对象的方法。
//调用this.vm中的callback方法,并将 this.vm[this.key] 作为参数传入。
}
}
3.3 compile.js
// 扫描模板中所有依赖,创建更新函数和watcher
class Compile {
// el是宿主元素或其选择器
// vm当前Vue实例
constructor(el, vm) {
this.$vm = vm
this.$el = document.querySelector(el) // 默认选择器
if (this.$el) {
// 将dom节点转换为Fragment片段,以提高执行效率
// DocumentFragments: 一次将所有的更改内容插入到DOM树中
this.$fragment = this.node2Fragment(this.$el)
// 执行编译
this.compile(this.$fragment)
// 将生成的结果追加至宿主元素
this.$el.appendChild(this.$fragment)
}
}
node2Fragment(el) {
// 创建一个新的Fragment
const fragment = document.createDocumentFragment()
let child
// 将原生节点拷贝至fragment
while ((child = el.firstChild)) {
// 因为child引用的是el.firstChild的内存地址,
// 将child插入到fragment,就相当于el.firstChild移动到了fragment,
// 因此下次调用el.firstChild 其实是调用的上次元素的下一个兄弟元素
fragment.appendChild(child)
}
return fragment
}
// 编译指定片段
compile(el) {
let childNodes = el.childNodes
// 将一个类数组对象或者可遍历对象转换成一个真正的数组
Array.from(childNodes).forEach(node => {
// 判断node类型,做响应处理
if (this.isElementNode(node)) {
// 元素节点要识别v-xx或@xx
this.compileElement(node)
} else if (this.isTextNode(node) && /{{(.*)}}/.test(node.textContent)) {
// 文本类型,只关心{{xx}}格式
this.compileText(node, RegExp.$1) // RegExp.$1: 指的是(.*)里面的内容
}
// 遍历可能存在的子节点
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点
compileElement(node) {
//例:<div v-text="lalala" @click="onclick"></div>
const attrs = node.attributes
Array.from(attrs).forEach(attr => {
const attrName = attr.name // 属性名
const exp = attr.value // 属性值
if (this.isDirective(attrName)) { // 指令:v-text
const dir = attrName.substr(2)
this[dir] && this[dir](node, this.$vm, exp) // this[dir],这时指的是this.text
} else if (this.isEventDirective(attrName)) { // 事件:@click
const dir = attrName.substr(1)
this.eventHandler(node, this.$vm, exp, dir)
}
})
}
// 编译文本节点
compileText(node, exp) {
this.text(node, this.$vm, exp)
}
// 判断是否是元素节点
isElementNode(node) {
return node.nodeType === 1 // 元素节点
}
// 判断是否是文本节点
isTextNode(node) {
return node.nodeType === 3 // 文本节点
}
isDirective(attr) {
return attr.indexOf("v-") === 0 // 判断是否由'v-'开头,是则就是指令
}
isEventDirective(attr) {
return attr.indexOf("@") === 0
}
// 文本更新
text(node, vm, exp) {
this.update(node, vm, exp, 'text')
}
// 处理html
html(node, vm, exp) {
this.update(node, vm, exp, 'html')
}
// 处理双向绑定
model(node, vm, exp) {
this.update(node, vm, exp, 'model')
const val = vm.exp
// 双向绑定还要处理视图对模型的更新
node.addEventListener('input', e => {
vm[exp] = e.target.value
// val = e.target.value
})
}
//更新
update(node, vm, exp, dir) {
let updatorFn = this[dir+'Updator']
updatorFn && updatorFn(node, vm[exp]) // 立刻更新一次,相当于初始化更新
new Watcher(vm, exp, function(value) {
updatorFn && updatorFn(node, value);
})
}
textUpdator(node, value) {
node.textContent = value
}
htmlUpdator(node, value) {
node.innerHTML = value
}
modelUpdator(node, value) {
node.value = value
}
eventHandler(node, vm, exp, dir) {
// 拿到 vue中methods对应的函数
let fn = vm.$options.methods && vm.$options.methods[exp] // exp: 为@οnclick="xx"中的xx
if (dir && fn) {
node.addEventListener(dir, fn.bind(vm)) // fn.bind(vm):绑定实例(上下文),类似:fn.bind(this)
}
}
}
最后
以上就是小巧烤鸡为你收集整理的MVVM 原理解析介绍1、小试一下2、运行机制3、源码的全部内容,希望文章能够帮你解决MVVM 原理解析介绍1、小试一下2、运行机制3、源码所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
发表评论 取消回复