Vue.js 是如何实现 MVVM 的?
框架到底为我们做了什么?
- 数据和视图分离,解耦(开放封闭原则)
- 所有数据和视图不分离的,都会命中开放封闭原则
Vue
数据独立在data
里面,视图在template
中
- 以数据驱动视图,只关心数据变化,
dom
操作被封装- 使用原生js是直接通过操作
dom
来修改视图,例如ducument.getElementById('xx').innerHTML="xxx"
- 以数据驱动视图就是,我们只管修改数据,视图的部分由框架去帮我们修改,符合开放封闭模式
- 使用原生js是直接通过操作
如何理解 MVVM ?
- MVC
Model
数据 →View
视图 →Controller
控制器
- MVVM
MVVM
不算是一种创新- 但是其中的
ViewModel
是一种创新 ViewModel
是真正结合前端应用场景的实现
- 如何理解MVVM
MVVM - Model View ViewModel
,数据,视图,视图模型- 三者与
Vue
的对应:view
对应template
,vm
对应new Vue({…})
,model
对应data
- 三者的关系:
view
可以通过事件绑定的方式影响model
,model
可以通过数据绑定的形式影响到view
,viewModel
是把model
和view
连起来的连接器
如何实现 MVVM - 以 Vue.js 为例
MVVM
框架的三大要素
- 响应式:
Vue
如何监听到data
的每个属性变化 - 模板引擎:
Vue
的模板如何被解析,指令如何处理 - 渲染:
Vue
的模板如何被渲染成html
,渲染过程是怎样的
Vue 如何实现响应式
- 什么是响应式
- 修改
data
属性之后,Vue
立刻监听到,立刻渲染页面 data
属性被代理到vm
上
- 修改
- Object.defineProperty
- 将对象属性的值的设置和访问 (get,set) 都变成函数,可以在当中加入我们自己的逻辑(进行监听)
- 普通的
JavaScript
对象,做属性修改,我们监听不到,所以需要用到Object.defineProperty
- 既能get,又能set,才是双向数据绑定
Vue 如何解析模板
- 模板是什么
- 本质:模板就是字符串
- 与html格式很像,但是模板中是有逻辑的,可以嵌入JS变量,如v-if, v-for等
- 视图最终还是需要由模板生成
html
来显示 - 模板必须先要转换成JS代码
- 有逻辑(v-if, v-for),必须用JS才能实现(图灵完备)
- 转换为html渲染页面,必须用JS才能实现
- 因此,模板要转换成render函数
- render函数
- render函数包含了模板中所有的信息,返回
vnode
,解决了模板中的逻辑(v-if, v-for)问题 - 如何找到最终生成的render函数
- 找到vue源码,搜索
code.render
,将code打印出来,就是生成的render函数
- 找到vue源码,搜索
- render函数包含了模板中所有的信息,返回
- render函数与vdom
- 模板生成
html
:vm._c
vm._c
和snabbdom
中的h
函数的实现很像,都是传入标签,属性,子元素作为参数Vue.js
的vdom
实现借鉴了snabbdom
updateComponent
中实现了vdom
的patch
- 页面首次渲染执行
updateComponent
data
中每次修改属性,都会执行updateComponent
- 模板生成
Vue.js 运行机制
- 第一步:解析模板成
render
函数- 因为在打包的时候就已经生成了render函数,所以编译是第一步;响应式监听是在代码执行的时候才开始监听。
- 模板中的所有信息都被render函数包含
- 模板中用到的data中的属性,都变成了js变量
- 模板中的 v-model v-for v-on都变成了js逻辑
- render函数返回vnode
- 第二步:响应式开始监听
- 通过Object.definedProperty监听到对象属性的get和set
- 将data的属性代理到vm上
- 第三步:首次渲染,显示页面,且绑定依赖
- 初次渲染,执行
updateComponent
,执行vm._render()
- 执行
render
函数,会访问到data
中的值,访问时会被响应式的get
方法监听到 - 执行
updateComponent
,会走到vdom
的patch
方法 patch
将vnode
渲染成dom
,初次渲染完成- 疑问:为何要监听
get
,而不是直接监听set
?- 因为
data
中有很多属性,有些被用到,有些可能不被用到 - 只有被用到的才会走
get
- 没有走到
get
中的属性,set
的时候我们也无需关心 - 避免不必要的重新渲染
- 因为
- 初次渲染,执行
- 第四步:
data
属性变化,触发re-render
- 修改属性,被响应式的
set
监听到 set
中执行updateComponent
updateComponent
重新执行vm._render()
- 生成的
vnode
和prevVnode
,通过patch
进行对比 - 渲染到
html
中
- 修改属性,被响应式的
手写一个 Vue.js
index.html
这是最终的测试代码,我们自己实现的 Vue 在 XVue.js
和 compile.js
两个文件中,加起来大概200行代码左右,主要包括功能如下:
- 数据响应式:页面中能直接引用data中的变量 test,我们给data.test重新赋值时,页面能随test值改变
- 双向数据绑定:v-model
- 模板解析,处理指令和事件绑定:v-text v-model @click
- 渲染页面:将模板转化为 html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title></title>
</head>
<body>
<div id="app">
{{test}}
<div v-text="test"></div>
<p>
<input type="text" v-model="test" />
</p>
<p v-html="html"></p>
<p>
<button @click="onClick">按钮</button>
</p>
</div>
<script src="./compile.js"></script>
<script src="./XVue.js"></script>
<script>
const o = new XVue({
el: '#app',
data: {
test: '123',
foo: { bar: 'bar' },
html: '<button>html test</button>'
},
methods: {
onClick() {
alert('按钮点击了')
}
}
})
console.log(o.$data.test) //123
o.$data.test = 'hello, Xvue!'
console.log(o.$data.test) //hello, Xvue!
</script>
</body>
</html>
Mini Vue 的组成部分:
- 监听器 observe :数据劫持,实现响应式;属性代理
- 依赖管理器 Dep :负责将视图中所有依赖收集管理,包括依赖添加和通知更新
- 监听器 Watcher :具体更新的执行者
- 编译器 Compile :扫描模板中所有依赖(指令、插值、绑定、事件等),创建更新函数和监听器( Watcher )
XVue.js
class XVue {
constructor(options) {
this.$data = options.data;
this.observe(this.$data);
// 执行编译
new Compile(options.el, this);
}
observe(value) {
if (!value || typeof value !== 'object') {
return;
}
Object.keys(value).forEach(key => {
this.defineReactive(value, key, value[key]);
// 为vue的data做属性代理
this.proxyData(key);
});
}
defineReactive(obj, key, val) {
// 递归查找嵌套属性
this.observe(val);
// 创建Dep
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖
Dep.target && dep.addDep(Dep.target);
// console.log(dep.deps);
return val;
},
set(newVal) {
if (newVal === val) {
return;
}
val = newVal;
dep.notify();
},
});
}
proxyData(key) {
Object.defineProperty(this, key, {
get() {
return this.$data[key];
},
set(newVal) {
this.$data[key] = newVal;
},
});
}
}
// 依赖管理器:负责将视图中所有依赖收集管理,包括依赖添加和通知
class Dep {
constructor() {
// deps里面存放的是Watcher的实例
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
// 通知所有watcher执行更新
notify() {
this.deps.forEach(dep => {
dep.update();
});
}
}
// Watcher: 具体的更新执行者
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
// 将来 new 一个监听器时,将当前 Watcher 实例附加到 Dep.target
// 将来通过 Dep.target 就能拿到当时创建的 Watcher 实例
Dep.target = this;
// 读取操作,主动触发 get,当前 Watcher 实例被添加到依赖管理器中
this.vm[this.key];
// 清空操作,避免不必要的重复添加(再次触发 get 就不需要再添加 watcher 了)
Dep.target = null;
}
update() {
// console.log('from Watcher update: 视图更新啦!!!');
// 通知页面做更新
this.cb.call(this.vm, this.vm[this.key]);
}
}
compile.js
// 扫描模板中所有依赖(指令、插值、绑定、事件等)创建更新函数和watcher
class Compile {
// el是宿主元素或其选择器
// vm当前Vue实例
constructor(el, vm) {
this.$el = document.querySelector(el);
this.$vm = vm;
if (this.$el) {
// 将dom节点转换为Fragment提高执行效率
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)) {
// appendChild 是移动操作,移动一个节点,child 就会少一个,最终结束循环
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)
) {
// 文本节点,只关心{{msg}}格式
this.compileText(node, RegExp.$1); // RegExp.$1匹配{{}}之中的内容
}
// 遍历可能存在的子节点
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
compileElement(node) {
// console.log('编译元素节点');
// <div v-text="test" @click="onClick"></div>
const attrs = node.attributes;
Array.from(attrs).forEach(attr => {
const attrName = attr.name; // 获取属性名 v-text
const exp = attr.value; // 获取属性值 test
if (this.isDirective(attrName)) {
// 指令
const dir = attrName.substr(2); // text
this[dir] && this[dir](node, this.$vm, exp);
} else if (this.isEventDirective(attrName)) {
// 事件
const dir = attrName.substr(1); // click
this.eventHandler(node, this.$vm, exp, dir);
}
});
}
compileText(node, exp) {
// console.log('编译文本节点');
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;
}
isEventDirective(dir) {
return dir.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');
let val = vm.exp;
// 双绑还要处理视图对模型的更新
node.addEventListener('input', e => {
vm[exp] = e.target.value; // 这里相当于执行了 set
});
}
// 更新
// 能够触发这个 update 方法的时机有两个:1-编译器初始化视图时触发;2-Watcher更新视图时触发
update(node, vm, exp, dir) {
let updaterFn = this[dir + 'Updater'];
updaterFn && updaterFn(node, vm[exp]); // 立即执行更新;这里的 vm[exp] 相当于执行了 get
new Watcher(vm, exp, function (value) {
// 每次创建 Watcher 实例,都会传入一个回调函数,使函数和 Watcher 实例之间形成一对一的挂钩关系
// 将来数据发生变化时, Watcher 就能知道它更新的时候要执行哪个函数
updaterFn && updaterFn(node, value);
});
}
textUpdater(node, value) {
node.textContent = value;
}
htmlUpdater(node, value) {
node.innerHTML = value;
}
modelUpdater(node, value) {
node.value = value;
}
eventHandler(node, vm, exp, dir) {
let fn = vm.$options.methods && vm.$options.methods[exp];
if (dir && fn) {
node.addEventListener(dir, fn.bind(vm), false);
}
}
}
Vue.js 是如何实现 MVVM 的?的更多相关文章
- Vue.js入门
之前一直用的是jQuery,jQuery手动操作DOM导致性能不够好,因为DOM修改导致的页面重绘.重新排版!重新排版是用户阻塞的操作,同时,如果频繁重排,CPU使用率也会猛涨! Vue.js是数据驱 ...
- Vue.js起手式+Vue小作品实战
本文是小羊根据Vue.js文档进行解读的第一篇文章,主要内容涵盖Vue.js的基础部分的知识的,文章顺序基本按照官方文档的顺序,每个知识点现附上代码,然后根据代码给予个人的一些理解,最后还放上在线编辑 ...
- Vue.js响应式原理
写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:answershuto/learnV ...
- Vue.js 基础知识
0. Vue.js 是轻量级的MVVM框架: 1. index.html:<app></app>:组件载入:入口文件会默认调用一个 main.js: 2. App.vue:入口 ...
- 学习vue.js的正确姿势(转载)
最近饶有兴致的又把最新版 Vue.js 的源码学习了一下,觉得真心不错,个人觉得 Vue.js 的代码非常之优雅而且精辟,作者本身可能无 (bu) 意 (xie) 提及这些.那么,就让我来吧:) 程序 ...
- Vue.js简介及指令
1.Vue.js的特点 Vue.js是一个Javascript MVVM(Model-View-ViewModel)库,与传统Jquery的区别在于,Vue.js舍弃了繁杂的DOM操作, 如取DOM值 ...
- Vue.js 基础快速入门
Vue.js是一个JavaScript MVVM库,它是以数据驱动和组件化的思想构建的.Vue.js提供了简洁.易于理解的API,使得我们能够快速地上手并使用Vue.js 如果之前已经习惯了用jQue ...
- vue.js与react.js相比较的优势
vue.js的简介 vue.js是一个javascript mvvm库,它是以数据驱动和组件化的思想构建的.我们平时多用js去操作dom,vue.js则是使用了数据绑定驱动来操作dom的,也就是说创建 ...
- Vue.js 源码学习笔记
最近饶有兴致的又把最新版 Vue.js 的源码学习了一下,觉得真心不错,个人觉得 Vue.js 的代码非常之优雅而且精辟,作者本身可能无 (bu) 意 (xie) 提及这些.那么,就让我来吧:) 程序 ...
随机推荐
- 开源|LightGBM:三天内收获GitHub 1000+ 星
原创 2017-01-05 LightGBM 微软研究院AI头条 [导读]不久前微软DMTK(分布式机器学习工具包)团队在GitHub上开源了性能超越其他boosting工具的LightGBM,在三天 ...
- SQL中关键字的执行顺序
作为一个SQL新手,看到每种不熟悉的关键字时已经够迷茫了,可往往见到的语句关键字顺序还是各种各样,太难理解了.网上搜索了两篇文章,总结一下: 关于Sql关键字SELECT FROM GROUP ORD ...
- Qt、Qte与Qtopia(Qt嵌入式的发展历程)
Qt的授权是分为两条线,商业版和开源版.如果使用商业版的Qt,那么开发出的程序可以是私有的和商业的:如果使用的是开源版的Qt,由于其使用的是GPL协议,那么可发出的程序也必须是GPL的.不过自从qt ...
- win10下Linux子系统开启ssh服务
原文:win10下Linux子系统开启ssh服务 为了便于交流共同学习,博主QQ群242629020(stm32-MCU认认真真交流群) 欢迎批评指导!!!电梯:https://jq.qq.com/? ...
- Python Numpy基础教程
Python Numpy基础教程 本文是一个关于Python numpy的基础学习教程,其中,Python版本为Python 3.x 什么是Numpy Numpy = Numerical + Pyth ...
- 希尔伯特空间(Hilbert Space)
欧氏空间 → 线性空间 + 内积 ⇒ 内积空间(元素的长度,元素的夹角和正交) 内积空间 + 完备性 ⇒ 希尔伯特空间 0. 欧几里得空间 欧氏空间是一个特别的度量空间,它使得我们能够对其的拓扑性质, ...
- ATS项目更新(2) 命令行编译Studio解决方案
1: rem "D:\Microsoft Visual Studio 8\SDK\v2.0\Bin\sdkvars.bat" 2: D: 3: cd ..\..\..\..\..\ ...
- OpenGL(八) 显示列表
OpenGL在即时模式(Immediate Mode)下绘图时,程序中每条语句产生的图形对象被直接送进绘图流水线,在显示终端立即绘制出来.当需要在程序中多次绘制同一个复杂的图像对象时,这种即时模式会消 ...
- OpenGL(十六) 鼠标、键盘交互响应事件
OpenGL中通过鼠标和键盘跟程序交互的实现需要实现注册鼠标和键盘响应事件,在一定条件下,该事件被触发,事件里的程序被执行,达到交互的目的. 通过glutMouseFunc(&OnMouse) ...
- 【转】关于List排序的时效性
不多说了,就是说明List排序的时效性,仅仅用来备忘,改造自: http://blog.csdn.net/wanzhuan2010/article/details/6205884,感谢原作者 usin ...