剖析手写Vue,你也可以手写一个MVVM框架
剖析手写Vue,你也可以手写一个MVVM框架#
邮箱:563995050@qq.com
github: https://github.com/xiaoqiuxiong
作者:肖秋雄(eddy)
温馨提示:感谢阅读,笔者创作辛苦,如需转载请自觉注明出处哦
Vue MVVM响应式原理剖释
Vue是采用数据劫持配合发布者和订阅者模式,通过Object.definerProperty()来劫持各个属性的setter和setter,在数据变动时,发布消息给依赖收集器Dep,去通知观察者Watcher,触发对应的解释模板回调函数去更新视图。
详细点说就是MVVM作为绑定的入口,整合了Observer,Compile和Watcher三者,通过Observer来劫持且监听数据,通过Compile来解释编译模板指令,然后利用Watcher搭建Observer,Compile之间的联系,达到数据变化=>视图更新,视图交互变化=>数据变化=>视图更新双向绑定的效果。
示列:
手写Vue源码
inex.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>手写Vue</title>
</head>
<body>
<div id="app">
<h2>{{ person.name }} -- {{ person.age }}</h2>
<h3>{{ person.favorite }}</h3>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<h3>{{ msg }}</h3>
<div v-text='person.name'></div>
<div v-text='msg'></div>
<div v-html='htmlStr'></div>
<input type="text" v-model='msg'>
<button v-on:click="handlerClick">点击按钮</button>
<br>
<br>
<div>
数量:
<button v-on:click="sub">-</button>
{{num}}
<button v-on:click="add">+</button>
</div>
</div>
<script src="./Observer.js"></script>
<script src="./MVue.js"></script>
<script>
let vm = new MVue({
el: '#app',
data: {
num: 1,
person: {
name: '小马哥',
age: 18,
favorite: '喜欢大长腿妹妹!'
},
msg: '学习手写Vue框架',
htmlStr: '<h3>我爱学习vue</h3>'
},
methods: {
handlerClick() {
this.msg = '66'
this.person = {
name: '大马哥',
age: 99,
favorite: '喜欢大哥哥!'
}
},
add() {
this.num++
},
sub() {
if(this.num === 1) return
this.num = this.num -1
}
}
})
</script>
</html>
MVue.js
// 入口方法
class MVue {
constructor(options) {
this.$options = options
this.$el = options.el
this.$data = options.data
if (this.$el) {
// 1.实现一个数据观察者
new Observer(this.$data)
// 2.实现一个指令解释器
new Compile(this.$el, this)
// 代理this.$data => this
this.proxyData(this.$data)
}
}
// 代理
proxyData(data) {
for (const key in data) {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set: newVal => {
data[key] = newVal
}
})
}
}
}
// 指令解释器
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// 1.获取文档碎片对象,放入内存中可以减少页面回流和重绘
const fragment = this.node2Fragment(this.el)
// console.log(fragment);
// 2.编译模板
this.compile(fragment)
// 3.追加子元素到根元素
this.el.appendChild(fragment)
}
compile(fragment) {
// 1.获取每一个子节点
let childNodes = fragment.childNodes
childNodes = this.convertToArray(childNodes)
childNodes.forEach(child => {
if (this.isElementNode(child)) {
// console.log('元素节点', child);
this.compileElement(child)
} else {
// console.log('文档节点', child);
this.compileText(child)
}
if (child.childNodes && child.childNodes.length) {
this.compile(child)
}
});
}
isDirective(name) {
return name.startsWith('v-')
}
isElementNode(node) {
// 判断是否是元素节点
return node.nodeType === 1
}
convertToArray(nodes) {
// 将childNodes返回的数据转化为数组的方法
var array = null;
try {
array = Array.prototype.slice.call(nodes, 0);
} catch (ex) {
array = new Array();
for (var i = 0, len = nodes.length; i < len; i++) {
array.push(nodes[i]);
}
}
return array;
}
compileElement(node) {
// console.log(node);
// <div v-text="msg"></div>
const attributes = node.attributes
// console.log(attributes);
this.convertToArray(attributes).forEach(attr => {
const { name, value } = attr
// console.log(value);
if (this.isDirective(name)) {
// 是一个指令
const [, dirctive] = name.split('-')
const [dirName, eventName] = dirctive.split(':')
// console.log(dirName, eventName);
// 更新数据 数据驱动视图
compileUtil[dirName](node, value, this.vm, eventName)
// 删除标签上的指令
node.removeAttribute('v-' + dirctive)
}
})
}
compileText(node) {
// 匹配双大括号 {{}}
const content = node.textContent
if (/\{\{(.+?)\}\}/.test(content)) {
// console.log(content);
compileUtil['text'](node, content, this.vm)
}
}
node2Fragment(el) {
// 创建文档碎片
let f = document.createDocumentFragment()
while (el.firstChild) {
f.appendChild(el.firstChild)
}
return f
}
}
const compileUtil = {
text(node, expr, vm) {
let value
if (expr.indexOf('{{') !== -1) {
value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 绑定观察者,将来数据发生变化,触发这里的回调函数去更是对应的视图
new Watcher(vm, args[1], (newVal) => {
this.updater.textUpdater(node, this.getContentVal(expr, vm))
})
return this.getValue(args[1], vm)
})
} else {
value = this.getValue(expr, vm)
new Watcher(vm, expr, (newVal) => {
this.updater.textUpdater(node, newVal)
})
}
this.updater.textUpdater(node, value)
},
html(node, expr, vm) {
let value = this.getValue(expr, vm)
new Watcher(vm, expr, (newVal) => {
this.updater.htmlUpdater(node, newVal)
})
this.updater.htmlUpdater(node, value)
},
model(node, expr, vm) {
const value = this.getValue(expr, vm)
// 绑定更新函数 数据=>视图
new Watcher(vm, expr, (newVal) => {
this.updater.modelUpdater(node, newVal)
})
// 视图=>数据=>视图
node.addEventListener('input', e => {
// 设置值
this.setValue(expr, vm, e.target.value)
})
this.updater.modelUpdater(node, value)
},
on(node, expr, vm, eventName) {
let fn = vm.$options.methods && vm.$options.methods[expr]
node.addEventListener(eventName, fn.bind(vm), false)
},
getValue(expr, vm) {
expr = expr.replace(/\s+/g, "")
return expr.split('.').reduce((data, currentVal) => {
// console.log(currentVal);
return data[currentVal]
}, vm.$data)
},
setValue(expr, vm, newVal) {
expr = expr.replace(/\s+/g, "")
return expr.split('.').reduce((data, currentVal) => {
data[currentVal] = newVal
}, vm.$data)
},
updater: {
textUpdater(node, value) {
node.textContent = value
},
htmlUpdater(node, value) {
node.innerHTML = value
},
modelUpdater(node, value) {
node.value = value
}
},
getContentVal(expr, vm) {
return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(args[1], vm)
})
}
}
Observer.js
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
// 先把旧值保存起来
this.oldVal = this.getOldVal()
}
getOldVal() {
Dep.target = this
const oldVal = compileUtil.getValue(this.expr, this.vm)
Dep.target = null
return oldVal
}
update() {
const newVal = compileUtil.getValue(this.expr, this.vm)
this.cb(newVal)
}
}
class Dep {
constructor() {
// 定义观察者数组
this.subs = []
}
// 收集观察者
addSub(watcher) {
this.subs.push(watcher)
}
// 通知观察者去更新视图
notify() {
// console.log('通知了观察者');
this.subs.forEach(w => w.update())
}
}
// 数据劫持监听
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
if (data && typeof data === 'object') {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
}
defineReactive(obj, key, value) {
// 递归遍历,直到最后一个值不是对象
this.observer(value)
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: false,
get() {
// 订阅数据变化时,往Dep中添加观察者
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newVal) => {
this.observer(newVal)
// 重新更新值之前先对新值劫持监听
value = newVal
// 告诉Dep通知变化
dep.notify()
}
})
}
}
感谢阅读,笔者创作辛苦,如需转载请自觉注明出处哦
剖析手写Vue,你也可以手写一个MVVM框架的更多相关文章
- 手写MVVM框架 之vue双向数据绑定原理剖析
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...
- 实现一个类 Vue 的 MVVM 框架
Vue 一个 MVVM 框架.一个响应式的组件系统,通过把页面抽象成一个个组件来增加复用性.降低复杂性 主要特色就是数据操纵视图变化,一旦数据变化自动更新所有关联组件~ 所以它的一大特性就是一个数据响 ...
- Vue.js-----轻量高效的MVVM框架(一、初识Vue.js)
1.什么是Vue.js? 众所周知,最近几年前端发展非常的迅猛,除各种框架如:backbone.angular.reactjs外,还有模块化开发思想的实现库:sea.js .require.js .w ...
- 手写 Vue 系列 之 Vue1.x
前言 前面我们用 12 篇文章详细讲解了 Vue2 的框架源码.接下来我们就开始手写 Vue 系列,写一个自己的 Vue 框架,用最简单的代码实现 Vue 的核心功能,进一步理解 Vue 核心原理. ...
- 手写 Vue 系列 之 从 Vue1 升级到 Vue2
前言 上一篇文章 手写 Vue 系列 之 Vue1.x 带大家从零开始实现了 Vue1 的核心原理,包括如下功能: 数据响应式拦截 普通对象 数组 数据响应式更新 依赖收集 Dep Watcher 编 ...
- 「JavaScript」手起刀落-一起来写经典的贪吃蛇游戏
回味 小时候玩的经典贪吃蛇游戏我们印象仍然深刻,谋划了几天,小时候喜欢玩的游戏,长大了终于有能力把他做出来(从来都没有通关过,不知道自己写的程序,是不是能通关了...),好了,闲话不多谈,先来看一下效 ...
- python手写神经网络实现识别手写数字
写在开头:这个实验和matlab手写神经网络实现识别手写数字一样. 实验说明 一直想自己写一个神经网络来实现手写数字的识别,而不是套用别人的框架.恰巧前几天,有幸从同学那拿到5000张已经贴好标签的手 ...
- 看年薪50W的架构师如何手写一个SpringMVC框架
前言 做 Java Web 开发的你,一定听说过SpringMVC的大名,作为现在运用最广泛的Java框架,它到目前为止依然保持着强大的活力和广泛的用户群. 本文介绍如何用eclipse一步一步搭建S ...
- 手写一个RPC框架
一.前言 前段时间看到一篇不错的文章<看了这篇你就会手写RPC框架了>,于是便来了兴趣对着实现了一遍,后面觉得还有很多优化的地方便对其进行了改进. 主要改动点如下: 除了Java序列化协议 ...
随机推荐
- Python - requests发送请求报错:UnicodeEncodeError: 'latin-1' codec can't encode characters in position 13-14: 小明 is not valid Latin-1. Use body.encode('utf-8') if you want to send it encoded in UTF-8.
背景 在做接口自动化的时候,Excel作为数据驱动,里面存了中文,通过第三方库读取中文当请求参数传入 requests.post() 里面,就会报错 UnicodeEncodeError: 'lati ...
- 前端---css3优化
一.视差滚动(经过优化后的代码) .front::before { content: ''; position: fixed; // 代替background-attachment width: 10 ...
- springcloud项目实现自定义权限注解进行接口权限验证
一般在项目开发中会根据登录人员的权限大小对接口也会设置权限,那么对接口权限是怎么实现的呢,大多数都是用自定义权限注解,只需要在接口上加上一个注解就可以实现对接口的权限拦截,是否对该接口有权调用 接下来 ...
- Python 3:ImportError “No Module named Setuptools”的解决方法
sudo apt-get install python-setuptools python3-setuptools
- css hover 动画 transition:background-color 0.2s,color 0.2s; 外层套内层,正常是 里外层 鼠标上来 外层有hover,如果就想到里层hover触发外层hover,要用外层position 定义绝对定位,内层的hover跳出外层的div,这样视觉上就是两个单独的div,进行内外层联动。
css hover 动画 transition:background-color 0.2s,color 0.2s; 外层套内层,正常是 里外层 鼠标上来 外层有hover,如果就想到里层hover触发 ...
- Xamarine ContentPage.ToolbarItems 未显示/不显示在界面
将页面封装到NavigationPage中.在App.xaml.cs的构造函数中 将 MainPage = new MainPage(); 替换为 MainPage = new NavigationP ...
- Java 注解简介
一,什么叫注解 用一个词就可以描述注解,那就是元数据,即一种描述数据的数据.所以,可以说注解就是源代码的元数据.比如,下面这段代码: 1 2 3 4 @Override public String t ...
- 使用WireShark进行网络流量安全分析
WireShark的过滤规则 伯克利包过滤(BPF)(应用在wireshark的捕获过滤器上) ** 伯克利包过滤中的限定符有下面的三种:** Type:这种限定符表示指代的对象,例如IP地址,子网或 ...
- 如何查看QQ坦白说来自谁
近两天QQ新功能的坦白说开始席卷朋友圈,一个醒目的小窗就这样明晃晃出现在QQ对话列表"有人对你说:--"下面我们就来整理一下怎么看到是谁给你发送的坦白说呢? 方法一: 此方法仅限于 ...
- Natas33 Writeup(Phar反序列化漏洞)
Natas33: 又是一个上传文件的页面,源码如下: // graz XeR, the first to solve it! thanks for the feedback! // ~morla cl ...