1.MVVM
MVVM => Model(数据)-View(视图)-ViewModel(视图模型)
-
vue中的对应关系:Model => data,View => Template,ViewModel => new Vue()...
-
MVVM 将数据双向绑定作为核心思想,View 和 Model 之间没有关联,它们通过 ViewModel 这个桥梁进行交互。
-
Model 和 ViewModel 之间的交互是双向的, View 的变化会同步到 Model,而 Model 的变化也会立即同步到 View 上。
-
当用户操作 View,ViewModel 感知到变化,然后通知 Model 发生相应改变;反之当 Model 发生改变,ViewModel 也能感知到变化,使 View 作出相应更新。
2.手写vue.js
以下代码简单实现了vue的双向数据绑定,以及computed,methods功能。
注:CompierUtil为抽离出来的公共方法。
代码注释很详细(可以留言提问)
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="school.name">
<div>{{school.name}}{{school.age}}</div>
<div>{{school.age}}</div>
<div>{{desc}}</div>
<div v-html="message"></div>
<ul>
<li>1</li>
<li>2</li>
</ul>
<button v-on:click="change"></button>
</div>
<!-- <script src="./vue/dist/vue.js"></script> -->
<script src="./MVVM.js"></script>
<script>
// console.log(Vue);
var vm = new Vue({
el: '#app',
data: {
school:{
name: 'alex',
age:'18'
},
message: '<h1>哈哈</h1>'
},
computed:{
desc() {
return this.school.name + '厉害'
}
},
methods:{
change() {
this.school.age = 100
}
}
});
</script>
</body>
</html>
- MVVM.js
新建一个mvvm.js,将参数通过options传入mvvm中,并取出el和data绑定到mvvm的私有变量和data中。
// 创建自己的vue类
class Vue {
constructor(options) {
// options:实例化时传进来的参数
this.$el = options.el;
this.$data = options.data;
let computed = options.computed;
let methods = options.methods;
// 判断根元素是否存在
if (this.$el) {
// 数据劫持,给每一个属性添加一个dep
new Observer(this.$data)
// 代理 computed 数据到this.$data上,以便可以直接通过this.xxx访问数据
for (let key in computed) {
Object.defineProperty(this.$data, key, {
get:() => {
return computed[key].call(this)
}
})
}
// 代理 methods 数据到实例上,以便可以直接通过this.xxx访问数据
for (let key in methods) {
Object.defineProperty(this, key, {
get:() => {
return methods[key]
}
})
}
// 将this.$data 上的数据代理到this上
this.proxyVm(this.$data)
// 编译模版数据
new Compiper(this.$el, this)
}
}
proxyVm(data) {
// 访问this.xxx 即 this.$data.xxx
for (let key in data) {
Object.defineProperty(this, key, {
// 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
configurable: false,
// 当且仅当该属性的 enumerable 为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
enumerable: false,
get(){
return data[key]
},
set(newValue){
data[key] = newValue
}
})
}
}
}
实现Observer,监听所有的数据,并对变化数据发布通知;
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
// 判断属性值是否为object,只要对象才能做数据劫持
if (data && typeof data == 'object') {
for (let key in data) {
this.defineReactive(data, key, data[key])
}
}
}
// 对当前属性重新定义
defineReactive(obj, key, value) {
// 属性的值如果是对象的话,进行递归定义,以达到所有属性都被监测
this.observer(value)
// 实例化一个订阅器到当前属性作用域内,此dep只能被当前属性调用
let dep = new Dep();
Object.defineProperty(obj, key, {
get() {
// 判断
console.log(Dep.target)
// 将订阅者存储(为了不重复存储,当target存在时才执行,执行一次后在 watcher 中设为 null)
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newValue) => {
// 当新值发生变化时执行
if (newValue != value) {
// 对新值做监测
this.observer(newValue)
// 将新值覆盖老值
value = newValue
// 通知此属性的订阅者进行数据更新
dep.notify();
}
}
})
}
}
实现Compiper,进行模板的编译,包括编译元素(指令)、编译文本等,达到初始化视图的目的,并且添加订阅者
// 编译模版(核心代码)
class Compiper {
constructor(el, vm) {
// document.getElementById获取到的是动态的 document.querySelector获取的是静态的
// 获取根元素
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
// 将根元素获取到内存中
let fragment = this.nodeToFragment(this.el)
// 编译模版,将数据替换模版中的表达式({{school.name}}\v-model="school.name")
this.compier(fragment)
// 将替换后的内容塞到页面
this.el.appendChild(fragment)
}
compier(node) {
let childNode = node.childNodes; // 一层子元素,不包括儿子的儿子 类数组
[...childNode].forEach(child => {
if (this.isElementNode(child)) {
// 元素节点处理
this.compierElement(child)
// 递归将所有元素编译
this.compier(child)
} else {
// 文本节点处理
this.compierText(child)
}
})
}
// 判断属性是否为指令
isDirective(attrName) {
// startsWith es6的方法
return attrName.startsWith('v-')
}
// 编译元素
compierElement(node) {
// 获取元素属性
let attributes = node.attributes; // 类数组
[...attributes].forEach(attr => {
// console.log(attr); type=text v-model=school.name
let {
name,
value: expr
} = attr
// 判断属性是否为指令
if (this.isDirective(name)) { // v-model v-html v-bind v-on:click
let [, directive] = name.split('-') // directive:model\html\bind\on:click
// 如果是on:click进行分割
let [directiveName, eventName] = directive.split(':')
// 解析指令
CompierUtil[directiveName](node, expr, this.vm, eventName)
}
})
}
// 编译文本
compierText(node) {
// 获取文本节点的内容
let content = node.textContent;
if (/\{\{(.+?)\}\}/.test(content)) {
// 解析指令
CompierUtil['text'](node, content, this.vm)
}
}
nodeToFragment(node) {
// 创建文档碎片
let fragment = document.createDocumentFragment()
let firstChild
// 将模版添加到文档碎片这种
while (firstChild = node.firstChild) {
// appendChild可以将模版元素移到文档碎片中
fragment.appendChild(firstChild)
}
return fragment
}
isElementNode(node) {
// 判断是不是元素
//1.元素节点 2.属性节点 3.文本节点
return node.nodeType === 1
}
}
实现Watcher,作为一个中枢,接收observe发来的通知,并执行Dep中的更新方法。
//定义一个订阅者
class Watcher {
constructor(vm, expr, cb) {
// 缓冲当前值
this.vm = vm;
this.expr = expr;
this.cb = cb;
// 对老值进行存储
this.oldValue = this.getValue()
}
getValue() {
// 在获取老值的时候,首先将自己添加到全局
Dep.target = this; // watcher实例
// 获取已经被劫持的值,会调用 object.defineProperty 的 get 方法,从而将 watcher 添加到订阅器上
let newValue = CompierUtil.getValue(this.vm,this.expr)
// 清楚实例,以免重复添加
Dep.target = null;
return newValue
}
update() {
// 获取新值
let newValue = CompierUtil.getValue(this.vm,this.expr)
if (newValue != this.oldValue) {
// 调用新值的回掉函数
this.cb(newValue)
}
}
}
实现Dep:管理订阅者,通知更新
class Dep{
constructor() {
// 存储订阅者
this.subs = []
}
// 订阅
addSub(watcher) {
this.subs.push(watcher)
}
// 发布
notify() {
// 数据变化时通知订阅者更新
this.subs.forEach(watcher => {
watcher.update()
})
}
}
解析指令方法
// 解析指令的方法
CompierUtil = {
getValue(vm, expr) {
// 根据表达式获取值(school.name => alex,message => <h1>哈哈</h1>)
return expr.split('.').reduce((data, current) => {
return data[current]
}, vm.$data)
},
setValue(vm,expr,value) {
// 对表达式对应的属性重新赋值
expr.split('.').reduce((data, current, index, arr) => {
if (index === arr.length -1 ) {
return data[current] = value
}
return data[current]
}, vm.$data)
},
model(node, expr, vm) {
// 定义更新元素内容的方法
let fn = this.updater['modelUpdater']
// 根据表达式获取值
let value = this.getValue(vm, expr)
// 初始化视图渲染
fn(node, value)
// 对输入框(v-model)订阅
new Watcher(vm, expr, (newValue) => {
// 数据变化执行,将视图更新
fn(node, newValue)
})
// 监测视图的更新
node.addEventListener('input', (e) => {
let newValue = e.target.value;
// 将新值更新到数据中(vm.$data)
this.setValue(vm, expr, newValue)
})
},
html(node,expr,vm) {
// 定义更新元素内容的方法
let fn = this.updater['htmlUpdater']
// 根据表达式获取值
let value = this.getValue(vm, expr)
// 初始化视图渲染
fn(node, value)
// 对v-html订阅
new Watcher(vm, expr, (newValue) => {
// 数据变化执行,将视图更新
fn(node, newValue)
})
},
on(node,expr,vm,eventName) {
// 对on指令对应的元素进行事件监听 v-on:click="change"
node.addEventListener(eventName, (e) => {
// expr => change
// change 方法内的 this 指向 vm
vm[expr].call(vm,e)
})
},
// 获取文本框表达式对应的数据
getContentValue(vm,expr) {
let value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// console.log(args) ["{{school.name}}", "school.name", 0, "{{school.name}}{{school.age}}"]
return this.getValue(vm, args[1])
})
return value
},
text(node, expr, vm) {
// 定义更新文本内容的方法
let fn = this.updater['textUpdater']
// 对每一文本进行数据替换
// expr => {{school.name}}{{school.age}}
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 给每一个{{}}加入观察者
new Watcher(vm, args[1], () => {
// 对每一个{{}}所在的元素节点更新
fn(node, this.getContentValue(vm,expr) )
})
let value = this.getValue(vm, args[1])
return value
})
// 初始化视图渲染
fn(node, content)
},
updater: {
// 输入框更新方法
modelUpdater(node, value) {
node.value = value
},
// 文本更新方法
textUpdater(node, value) {
node.textContent = value
},
// 富文本更新方法
htmlUpdater(node,value) {
node.innerHTML = value
}
}
}
网友评论