面试过程当中,面试官必定会问你 描述一下 你所知道的MVVM?html
MVVM 在Vue中是用什么来实现的?vue
OK,咱们来攻克这个题目node
首先第一个M,指的是 Model, 也就是**数据模型
,其实就是数据, 换到Vue里面,其实指的就是 Vue组件实例中的data
**, 可是这个data 咱们从一开始就定义了 它叫 响应式数据
react
第二个V,指的是View, 也就是**页面视图
, 换到Vue中也就是 咱们的template
转化成的DOM对象
**面试
第三个 VM, 指的是**ViewModel
, 也就是 视图和数据的管理者, 它管理着咱们的数据 到 视图变化的工做,换到Vue中 ,它指的就是咱们的当前的Vue实例
, Model数据 和 View 视图通讯的一个桥梁
**数组
数据驱动视图
, 数据变化 =>视图更新 双向 绑定 视图更新 => 数据变化Vue ==>MVVM => 双向数据绑定 => this.name = '张三 '浏览器
React => MVVM => 单向数据绑定 => 只能从数据 => 视图 => this.setState({ name: '张三' })app
<!-- 视图 --> <template> <div>{{ message }}</div> </template> <script> // Model 普通数据对象 export default { data () { return { message: 'Hello World' } } } </script> <style> </style>
接下里,咱们来重点研究MVVM的原理及实现方式,Vuejs官网给出了MVVM的原理方式框架
Vue文档说明dom
经过上面的文档咱们能够发现, Vue的响应式原理(MVVM)实际上就是下面这段话:
当你把一个普通的 JavaScript 对象传入 Vue 实例做为
data
选项,Vue 将遍历此对象全部的属性,并使用Object.defineProperty
把这些属性所有转为 getter/setter。Object.defineProperty
是 ES5 中一个没法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的缘由。
从上面的表述中,咱们发现了几个关键词, Object.defineProperty
getter/setter
什么是 Object.defineProperty?
定义:Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
语法: Object.defineProperty(obj, prop, descriptor)
参数: obj => 要在其上定义属性的对象。
prop => 要新增或者修改的属性名
descriptor => 将被定义或修改的属性描述符。
返回值 : 被传递给函数的对象。 也就是 传入的obj对象
经过上面的笔记 咱们来看下 有哪些参数 须要学习
obj 就是一个对象 能够 new Object() 也能够 { }
prop 就是属性名 也就是一个字符串
descriptor 描述符是什么 ? 有哪些属性
对象里目前存在的属性描述符有两种主要形式:数据描述符
和存取描述符
。数据描述符
是一个具备值的属性,该值多是可写的,也可能不是可写的。存取描述符
是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是二者
。
上面是官方描述 ,它告诉咱们 defineProterty设计上有**
两种模式
存在,一种数据描述
, 一种存取描述
**描述符必须是这两个中的一个 ,不能同时是二者, 也就是
一山不容二虎
, 也不能一山两虎都无
咱们写一个最简单的 **数据描述符
**的例子
// Object.defineProperty(obj,prop, desciptor) // desciptor => 数据描述符 存取描述符 var obj = { name: '曹扬' } var o = Object.defineProperty(obj, 'weight', { // 描述符是一个对象 // 数据描述 存取描述 value: '280kg' // 数据描述符 value }) console.log(o)
接下来进行详细分析
数据描述符
模式数据描述符有哪些属性?
value
=>该属性对应的值。能够是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 unfinedwritable
=> 当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。就这两个 ? 还有吗 ?
configurable
=> 当且仅当该属性的 configurable 为 true 时,该属性描述符
才可以被改变,同时该属性也能从对应的对象上被删除。默认为 false。决定writable可不可改enumerable
=> 当且仅当该属性的enumerable
为true
时,该属性才可以出如今对象的枚举属性中。默认为 false。为何
configurable
和enumerable
不一样时 和 value 还有 writable一块儿写呢 ?
由于这两个属性不但能够在数据描述符里出现 还能够在 存取描述符里出现
咱们经过writeable 和 value属性来写一个 可写的属性 和不写的属性
var obj = { name: '曹扬' } Object.defineProperty(obj, 'money', { value: "10k" // 薪水 此时薪水是不可改的 }) Object.defineProperty(obj, 'weight', { value: '150斤', // 给一万根头发 writable: true }) obj.money = '20k' obj.hair = '200斤' console.log(obj)
接下来 ,咱们但愿 去让一个不可变的属性变成可变的
var obj = { name: '曹扬' } Object.defineProperty(obj, 'money', { value: '10k', // 薪水 此时薪水是不可改的 configurable: true // 只有这里为true时 才能去改writeable属性 }) Object.defineProperty(obj, 'weight', { value: '150斤', // 给一万根头发 writable: true }) obj.money = "20k" obj.weight = '200斤' console.log(obj) Object.defineProperty(obj, 'money', { writable: true }) obj.money = '20k' console.log(obj)
接下来,咱们但愿能够在遍历的时候 遍历到新添加的两个属性
var obj = { name: '曹扬' } Object.defineProperty(obj, 'money', { value: '10k', // 薪水 此时薪水是不可改的 configurable: true, enumerable: true }) Object.defineProperty(obj, 'weight', { value: '150斤', // 给一万根头发 writable: true, enumerable: true }) obj.money = "20k" obj.weight = '200斤' console.log(obj) Object.defineProperty(obj, 'money', { writable: true }) obj.money = '20k' console.log(obj) for(var item in obj) { console.log(item) }
存取描述符
模式上一小节中,数据描述符 独有的属性 是 value 和 writable , 这也就意味着, 在存取描述模式中
value 和 writable属性不能出现
那么 存储描述符有啥属性 ?
get
一个给属性提供 getter 的方法,若是没有 getter 则为 undefined
。当访问该属性时,该方法会被执行,方法执行时没有参数传入,可是会传入this
对象(因为继承关系,这里的this
并不必定是定义该属性的对象)。set
一个给属性提供 setter 的方法,若是没有 setter 则为 undefined
。当属性值修改时,触发执行该方法。该方法将接受惟一参数,即该属性新的参数值。get/set 其实就是咱们最多见的 读取值 和设置值得方法
读取值得时候 调用 get方法
设置值得时候调用 set方法
咱们作一个 能够 经过 get 和 set 读取设置的方法
var obj = { name: '曹操' } var wife = '小乔' Object.defineProperty(obj, 'wife',{ get () { return wife }, set (value) { wife = value } }) console.log(obj.wife) obj.wife= '大乔' console.log(obj.wife)
可是,咱们想要遍历怎么办 ? 注意哦 , 存储描述符的时候 依然拥有 configurable 和 enumerable属性,
依然能够配置哦
var obj = { name: '曹操' } var wife = '小乔' Object.defineProperty(obj, 'wife',{ configurable:true, enumerable: true, get () { return wife }, set (value) { wife = value } }) console.log(obj.wife) obj.wife= '大乔' console.log(obj.wife) for(var item in obj) { console.log(item) }
经过两个小节,学习了 defineProperty的基本使用, 接下里咱们要经过defineProperty模拟 Vue实例化的效果
Vue实例化的时候, 咱们明明给data赋值了数据,可是却能够经过 **vm实例.属性
**进行访问和设置
怎么作的 ?
实际上这就是 经过 Object.defineProperty实现的
var data = { name: "张三" }; var vm = {}; Object.defineProperty(vm, "name", { set(value) { data.name = value; }, get() { return data.name; } }); console.log(vm.name); vm.name = "李四"; console.log(vm.name);
上面代码中,咱们实现了 vm中的数据代理了 data中的name 直接改vm就是改data
总结
: 咱们在 set和get的存取描述符中 代理了 data中的数据,
MVVM =>数据代理 =>object.defineProperty =>存取描述符get/set =>代理数据
MVVM不但要获取这些数据,而且将这些数据 进行 响应式的更新到DOM中, 也就是 数据变化时,咱们要把数据**反映
**到视图上
经过调试咱们发现,咱们是能够在set函数里面监听到数据的变化的,只须要在数据变化的时候, 通知对应的视图来更新就能够了
那么 怎么通知 ? 用什么技术来作 ? 下一小节中咱们将带来发布订阅模式
发布订阅模式为什么物?
其实咱们早已用过不少遍, 发布 /订阅 即 有人**发布消息
**, 有人 订阅消息
,到了 数据层面 就是 多 => 多
即 A程序 能够触发多个消息 也能够订阅 多个消息
在黑马头条项目1 和项目2 中咱们 曾经 用过一个eventBus 就是发布订阅模式的体现
这个模式咱们拿来作什么?
上个小节,咱们已经可以捕捉数据的变化,接下来,咱们就要尝试在数据变化的时候经过 发布订阅这个模式 来改变咱们的视图
咱们先写出这个发布订阅核心代码的几个要素
首先,咱们但愿 能够经过实例化 获得 发布订阅对象
发布消息 $emit
订阅消息 $on
根据上述思想,咱们获得以下代码
// 建立一个构造函数 function Events (){} // 订阅消息 Events.prototype.$on = function(){} // 发布消息 Events.prototype.$emit = function(){}
function Events () { this.subs = {} } Events.prototype.$on = function (eventName, fn) { this.subs[eventName] = this.subs[eventName] || [] this.subs[eventName].push(fn) } Events.prototype.$emit = function (eventName, ...params) { if(this.subs[eventName]) { this.subs[eventName].forEach(fn => { //fn.apply(this, [...params]) //fn.call(this, ...params) fn.bind(this,...params)() }); } } var test = new Events() test.$on("updateABC", function(a,b,c){ console.log(a+'-'+b+'-'+c) console.log(this) }) var go = function(){ test.$emit("updateABC", 1,2,3) }
这里用到了call/apply/bind方法修改函数内部的this指向
利用发布订阅模式能够实现当事件触发时会通知到不少人去作事情,Vue中作的事情是更新DOM
咱们学习了 Object.defineProperty 和 发布订阅模式, 几乎拥有了手写一个MVVM的能力,
可是在实现MVVM以前,咱们仍是复习一下 View中也就是 Dom中的含义及结构
DOM是什么?
文档对象模型 document
Dom的做用是什么?
能够经过**
对象
**去操做页面元素
Dom中的对象节点都有什么类型
能够经过下面的一个小例子检查
<div id="app"> <h1>众志成城,共抗疫情</h1> <div> <span style='color:red;font-weight: bold;'>老高:</span> <span>祝全部同窗前程似锦</span> </div> </div> <script> var app = document.getElementById("app") console.dir(app) </script>
经过上面的输出查看, 咱们能够发现
元素类型的节点类型 nodeType 为1 文本类型为 3, document对象里面的每一个内容都是**节点
**
childNodes 是全部的节点 childer 值的是 全部的元素 =>nodeType =>节点类型
全部的子节点都放在 childNodes 这个属性下,childNodes是伪数组 => 伪数组不具备数组方法,有length属性
全部标签的属性集合是什么?
attributes
分析DOM对象作什么呢? 咱们前面准备的数据捕获和 发布订阅就是为了来更新DOM的
接下来咱们开始手写一个MVVM示例
挑战来了,咱们要手写 一个简易的**
vuejs
**, 提高咱们自身的技术实力.
咱们要实现mvvm的构造函数
构造函数 模仿vuejs 分别有 data /el
data最终被代理给当前的vm实例, 便可以经过 vm访问,也能够经过 this.$data访问
// 首先实现一个构造函数 function Vue (options) { this.$options = options // 全部属性都给this的一个属性 this.$data = options.data || {} this.$el = typeof options.el ==="string" ? document.querySelector(options.el) : options.el // 把全部data的数据 代理给 当前实例 this.$proxyData() } // 代理数据 Vue.prototype.$proxyData = function () { Object.keys(this.$data).forEach(key => { Object.defineProperty(this, key, { get () { return this.$data[key] }, set (value) { if(this.$data[value] === value) return this.$data[key] = value } }) }) } var vm = new Vue({ el: '#app', data: { name: '曹扬', company: '揽月一颗' } }) console.log(vm.company) vm.company = '九天揽月' console.log(vm.$data.company) vm.$data.company = '下海捉鳖' console.log(vm.company)
OK,接下来这一步很是关键,咱们要作**
数据劫持
**, 劫持谁? 为何要劫持?
上小节代码中, 咱们能够经过 vm.company = '值' 也能够经过 vm.$data.name = '值', 那么在哪里捕捉数据的变化呢?
不管是 this.data 仍是 this.$data 改的都是data的数据,因此咱们须要对 data的数据进行**
劫持
**, 也就是监听它的set
// 监听数据 Vue.prototype.observer = function () { Object.keys(this.$data).forEach(key => { let value = this.$data[key] Object.defineProperty(this.$data, key, { get () { return value }, set (newValue) { if(newValue === value) return; value = newValue // 若是数据变化了 咱们须要 去改变视图 } }) }) }
在构造函数中完成对数据的劫持
// 首先实现一个构造函数 function Vue (options) { this.$options = options // 全部属性都给this的一个属性 this.$data = options.data || {} this.$el = typeof options.el ==="string" ? document.querySelector(options.el) : options.el // 把全部data的数据 代理给 当前实例 this.$proxyData() this.observer() // 开启监听数据 }
如今咱们基本实现了 实例化数据,而且完成了对数据的劫持,接下来咱们须要实现几个方法
数据变化时 => 根据最新数据把模板转化成最新的对象
判断是不是文本节点
判断是不是 元素节点
判断是不是指令
处理元素节点
处理文本节点
因此咱们定义下面几个方法
// 编译模板 Vue.prototype.compile = function () {} // 处理文本节点 Vue.prototype.compileTextNode = function (node){} // 处理元素节点 Vue.prototype.compileElementNode = function (node) {} // 判断是不是文本节点 Vue.prototype.isTextNode = function(node) {}; // 判断是不是元素节点 Vue.prototype.isElementNode = function(node) {}; // 判断属性是不是指令 Vue.prototype.isDirective = function(attr) {};
咱们已经经过构造函数拿到了$el,也就是页面的dom元素,接下来咱们能够实现 一下编译的基本逻辑
// 编译模板 Vue.prototype.compile = function (rootnode) { let nodes = Array.from(rootnode.childNodes) // 先把伪数组转成数组 nodes.forEach(item => { if(this.isTextNode(item)) { this.compileTextNode(node) } if(this.isElementNode(item)) { this.compileElementNode(node) this.compile(node) // 递归的思路 } }) } // 处理文本节点 Vue.prototype.compileTextNode = function (node){} // 处理元素节点 Vue.prototype.compileElementNode = function (node) {} // 判断是不是文本节点 Vue.prototype.isTextNode = function(node) { return node.nodeType === 3; }; // 判断是不是元素节点 Vue.prototype.isElementNode = function(node) { return node.nodeType === 1; }; // 判断属性是不是执行 Vue.prototype.isDirective = function(attr) { return attr.startsWith("v-"); };
上述代码的基本逻辑就是 碰到 文本节点就用文本节点的方法处理 碰到元素节点 用元素节点的方法处理
// 处理文本节点 Vue.prototype.compileTextNode = function (node){ const text = node.textContent const reg = /\{\{(.+?)\}\}/g if(reg.test(text)) { // 若是知足双大括号 const key = RegExp.$1.trim() this.$on(key, () => { node.textContent = text.replace(reg, this[key]) // 若是找到大括号 就替换对应的数据 }) node.textContent = text.replace(reg, this[key]) // 若是找到大括号 就替换对应的数据 } }
提示: 实际开发时正则不须要记 可是要能看懂
// 处理元素节点 Vue.prototype.compileElementNode = function (node) { let atts = Array.from(node.attributes) attrs.forEach(attr => { if(this.isDirective(attr.name)) { // 判断是不是指令 if(attr.name === 'v-text') { node.textContent = this[attr.value] // 等于当前属性的值 } if(attr.name === 'v-model') { // v-model绑定的是表单的value属性 node.value = this[attr.value] } } }) }
目前响应式数据有了, 编译模板也有了, 咱们须要在数据变化的时候编译模板
以前讲了, 这一步须要 经过发布订阅来作 ,因此咱们在Vue的基础上实现发布订阅
// 首先实现一个构造函数 function Vue (options) { this.subs = {} //发布订阅管理器 this.$options = options // 全部属性都给this的一个属性 this.$data = options.data || {} this.$el = typeof options.el ==="string" ? document.querySelector(options.el) : options.el // 把全部data的数据 代理给 当前实例 this.$proxyData() this.observer() // 开启监听数据 } Vue.prototype.$on= function (eventName, fn) { this.subs[eventName] = this.subs[eventName] || [] this.subs[eventName].push(fn) } Vue.prototype.$emit = function (eventName, ...params) { if(this.subs[eventName]) { this.subs[eventName].forEach(fn => { //fn.apply(this, [...params]) //fn.call(this, ...params) fn.bind(this,...params)() }); } }
如今万事俱备,只欠东风
咱们的数据代理,数据劫持,模板编译, 事件发布订阅通通搞定 如今只须要在数据变化时 ,经过事件发布,而后
通知 数据进行编译便可
// 监听数据 Vue.prototype.observer = function () { Object.keys(this.$data).forEach(key => { let value = this.$data[key] Object.defineProperty(this.$data, key, { get () { return value }, set: (newValue) => { if(newValue === value) return; value = newValue // 若是数据变化了 咱们须要 去改变视图 this.$emit(key) //触发数据改变 } }) }) }
监听数据改变
// 处理文本节点 Vue.prototype.compileTextNode = function (node){ const text = node.textContent if(/\{\{(.+)\}\}/.test(text)) { // 若是知足双大括号 const key = RegExp.$1.trim() this.$on(key, () => { node.textContent = text.replace(reg, this[key]) // 若是找到大括号 就替换对应的数据 }) node.textContent = text.replace(reg, this[key]) // 若是找到大括号 就替换对应的数据 } } // 处理元素节点 Vue.prototype.compileElementNode = function (node) { let atts = Array.from(node.attributes) attrs.forEach(attr => { if(this.isDirective(attr.name)) { // 判断是不是指令 if(attr.name === 'v-text') { node.textContent = this[attr.value] // 等于当前属性的值 this.$on(attr.value, () => { node.textContent = this[attr.value] // 若是找到大括号 就替换对应的数据 }) } if(attr.name === 'v-model') { // v-model绑定的是表单的value属性 node.value = this[attr.value] this.$on(attr.value, () => { node.value = this[attr.value] // 若是找到大括号 就替换对应的数据 }) } } }) }
而后咱们写个例子来测试一把
最后咱们但愿实现双向绑定,即视图改变时 数据同时变化
// 处理元素节点 Vue.prototype.compileElementNode = function (node) { let attrs = Array.from(node.attributes) attrs.forEach(attr => { if(this.isDirective(attr.name)) { // 判断是不是指令 if(attr.name === 'v-text') { node.textContent = this[attr.value] // 等于当前属性的值 this.$on(key, () => { node.textContent = this[attr.value] // 若是找到大括号 就替换对应的数据 }) } if(attr.name === 'v-model') { // v-model绑定的是表单的value属性 node.value = this[attr.value] this.$on(key, () => { node.value = this[attr.value] // 若是找到大括号 就替换对应的数据 }) node.oninput = () => { this[attr.value] = node.value } } } }) }