目录

* 框架到底为我们做了什么? <https://www.cnblogs.com/dora-zc/p/11111813.html#框架到底为我们做了什么>
* 如何理解 MVVM ? <https://www.cnblogs.com/dora-zc/p/11111813.html#如何理解-mvvm>
* 如何实现 MVVM - 以 Vue.js 为例
<https://www.cnblogs.com/dora-zc/p/11111813.html#如何实现-mvvm---以-vue.js-为例>
* Vue 如何实现响应式 <https://www.cnblogs.com/dora-zc/p/11111813.html#vue-如何实现响应式>
* Vue 如何解析模板 <https://www.cnblogs.com/dora-zc/p/11111813.html#vue-如何解析模板>
* Vue.js 运行机制 <https://www.cnblogs.com/dora-zc/p/11111813.html#vue.js-运行机制>
* 手写一个 Vue.js <https://www.cnblogs.com/dora-zc/p/11111813.html#手写一个-vue.js>
框架到底为我们做了什么?

* 数据和视图分离,解耦(开放封闭原则)
* 所有数据和视图不分离的,都会命中开放封闭原则
* Vue 数据独立在 data 里面,视图在 template 中
* 以数据驱动视图,只关心数据变化,dom 操作被封装
* 使用原生js是直接通过操作dom来修改视图,例如 ducument.getElementById('xx').innerHTML="xxx"
* 以数据驱动视图就是,我们只管修改数据,视图的部分由框架去帮我们修改,符合开放封闭模式
如何理解 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函数
* 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); } } }

友情链接
ioDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信