Vue实现MVVM
**js**创建对象的两种⽅式
```text
//第⼀种
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function(){
alert(this.name);
}
//第⼆种
var person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function() {
alert(this.name);
}
}
```
属性描述符:
数据属性:数据属性包含⼀个数据值的位置,在这个位置可以读取和写⼊值
```text
1、可配置性 [[Configurable]] : 表示能否通过delete删除属性,能否修改属性特性,能否把数据属性修改为访问器属性。
2、可枚举性[[Enumerable]]:表示能否通过for-in循环返回属性。
3、可写⼊性[[Writable]]:表示能否修改属性值。
4、属性值[[Value]]:表示属性值。
```
访问器属性:是包含⼀对**getter**和**setter**函数
**Object.defineProperty()**⽅法对数据属性和访问器属性进⾏修改
```text
该⽅法接受三个参数:属性所在对象,属性名字和⼀个描述符对象
```
**Object.getOwnPropertyDescriptor()**⽅法取得指定对象指定属性的描述符
```text
这个⽅法接收两个参数:属性所在对象,属性名字
```
如果要求对⽤户的输⼊进⾏特殊处理,或者设置属性的依赖关系,
就需要⽤到访问器属性了
### **Object.defineProperty**
ES6中某些方法的实现依赖于它,VUE通过它实现双向绑定
此方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象
### **语法**
```js
Object.defineProperty(object, attribute, descriptor)
这三个参数都是必输项
第一个参数为目标对象
第二个参数为需要定义的属性或者方法
第三个参数为目标属性所拥有的特性
descriptor
value: 属性的值
writable: 属性的值是否可被重写(默认为false)
configurable: 总开关,是否可配置,若为false, 则其他都为false(默认为false)
enumerable: 属性是否可被枚举(默认为false)
get: 获取该属性的值时调用
set: 重写该属性的值时调用
```
简单定义一下
```text
var a= {}
Object.defineProperty(a,"b",{
value:123
})
console.log(a.b) //123
a.b = 456
console.log(a.b) //123
a.c = 110
for (item in a) {
console.log(item, a[item]) //c 110
}
因为 writable 和 enumerable 默认值为 false, 所以对 a.b 赋值无效,也无法遍历它
```
configurable
总开关,是否可配置,设置为 false 后,就不能再设置了,否则报错, 例子
```text
var a= {}
Object.defineProperty(a,"b",{
configurable:false
})
Object.defineProperty(a,"b",{
configurable:true
})
//error: Uncaught TypeError: Cannot redefine property: b
```
## **writable**
是否可重写
```text
var a = {};
Object.defineProperty(a, "b", {
value : 123,
writable : false
});
console.log(a.b); // 打印 123
a.b = 25; // 没有错误抛出(在严格模式下会抛出,即使之前已经有相同的值)
console.log(a.b); // 打印 123, 赋值不起作用。
```
## **enumerable**
属性特性 enumerable 定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举
```text
var a= {}
Object.defineProperty(a,"b",{
value:3445,
enumerable:true
})
console.log(Object.keys(a));// 打印["b"]
```
enumerable改为false
```text
var a= {}
Object.defineProperty(a,"b",{
value:3445,
enumerable:false //注意咯这里改了
})
console.log(Object.keys(a));// 打印[]
```
## **set 和 get**
如果设置了 set 或 get, 就不能设置 writable 和 value 中的任何一个,否则报错
```text
var a = {}
Object.defineProperty(a, 'abc', {
value: 123,
get: function() {
return value
}
})
```
对目标对象的目标属性 赋值和取值 时, 分别触发 set 和 get 方法
```text
var a = {}
var b = 1
Object.defineProperty(a,"b",{
set:function(newValue){
b = 99;
console.log("你要赋值给我,我的新值是"+newValue);
},
get:function(){
console.log("你取我的值");
return 2 //注意这里,我硬编码返回2
}
})
a.b = 1 //打印 你要赋值给我,我的新值是1
console.log(b) //打印 99
console.log(a.b) //打印 你取我的值
//打印 2 注意这里,和我的硬编码相同的
```
上面的代码中,给a.b赋值,b的值也跟着改变了。原因是给a.b赋值,自动调用了set方法,在set方法中改变了b的值。vue双向绑定的原理就是这个。
```text
let obj = {
name: 'zs'
}
let temp=obj['name'];
// 数据劫持的核心属性
Object.defineProperty(obj, 'name', {
configurable: true, // 表示属性可以配置
enumerable: true, // 表示这个属性可以枚举(遍历)
get() {
// 每次获取对象的这个属性的时候,就会被这个get方法给劫持到
console.log('get执行了')
return temp;
},
// 每次设置这个对象的属性的时候,就会被set方法劫持到
// 设置的值也会劫持到
set(newValue) {
console.log('set方法执行了')
console.log(newValue)
temp=newValue;
}
})
```
## **Object.defineProperties**
此方法可以一次设置多个属性,例子:
```text
var book = {};
Object.defineProperties(book, {
_year: {
writable: true,
value: 2004
},
edition: {
value: 1
},
year: {
get: function() {
return this._year;
},
set: function(newValue) {
console.log("++++", newValue)
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
console.log(book.year)
book.year = 2007
console.log(book.year)
```
数据绑定⼩案例
```text
<body>
<input id="input" /></br>
<button id="btn">提交数据</button>
<script>
let inputNode=document.getElementById('input');
let person = {}
Object.defineProperty(person, 'name', {
configurable: true,
get: function() {
console.log('访问器的GET⽅法:' + inputNode.value)
return inputNode.value
},
set: function(newValue) {
console.log('访问器的SET⽅法:' + newValue)
inputNode.value = newValue
}
})
inputNode.oninput = function() {
console.log('输⼊的值: ' + inputNode.value)
person.name = inputNode.value;
}
let btn = document.getElementById('btn');
btn.onclick = function() {
alert(person.name)
}
</script>
</body>
```
### **什么是数据驱动**
数据驱动是vue.js最大的特点。在vue.js中,所谓的数据驱动就是**当数据发生变化的时候,用户界面发生相应的变化,开发者不需要手动的去修改dom**。
### **MVVM设计模式**
**Vue**双向绑定的的原理
vue.js 是采⽤数据劫持结合发布者-订阅者模式的⽅式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
具体思路:
第⼀步:需要**oberver**的数据对象进⾏递归遍历,包括⼦属性对象的属性,都加上**setter**和**getter**⽅法
这样做就可以监听到数据的变化。
第⼆步:**compile**解析模版指令,将模版中的变量替换成数据,然后初始化渲染⻚⾯视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,⼀旦数据有变动,收到通知,更新视图。
第三步:**Watcher**订阅者是**Observer**和**Compile**之间通信的桥梁,主要做事情是:
1. 在⾃身实例化时往属性订阅器(dep)⾥⾯添加
2.待属性变动通知时,触发Compile中的绑定的回调函数
第四步:**MVVM**作为数据绑定的⼊⼝,整合**Observer**、**Compile**和**Watcher**三者,通过**Observer**来
监听⾃⼰的**model**数据变化,通过**Compile**来解析编译模版指令,最终利⽤**Watcher**搭起**Observer**和**Compile**之间的通信桥梁,达到数据变化**->**视图更新**->**视图交互变化**(input)->**数据**model**变更的双向绑定效果
### **Object.defineProperty**
vue通过Object.defineProperty来实现数据劫持,会对数据对象每个属性添加对应的get和set方法,对数据进行读取和赋值操作就分别调用get和set方法。
```text
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
// do something
return val;
},
set: function(newVal) {
// do something
}
});
```
可以将一些方法放到里面,从而完成对数据的监听(劫持)和视图的同步更新。
### **ok,再解释一遍过程**
实现双向数据绑定,首先要对数据进行数据监听,需要一个监听器Observer,监听所有属性。如果属性发生变化,会调用setter和getter,再去告诉订阅者Watcher是否需要更新。由于订阅者有很多个,我们需要一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理。还有,我们需要一个指令解析器Complie,对每个元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或绑定相应函数。当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图
```text
//这是一个监听器Observer类,监听所有属性,如果属性发生变化,会调用setter和getter,再去告诉订阅者Watcher是否需要更新
class Observe {}
//这个是具体某个订阅者的类
class Watcher {}
//由于订阅者有很多个,我们需要一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理
class Dep {}
//写一些公共方法来获取值,设置值,方便调用
const utils = {
setValue(){},
getValue(){},
changeValue(){}
}
//还有一个具体实例化出来的类,来达到双向数据绑定的目的
class Myvue{
//按照正常的逻辑来说,还应该有一个类来专门处理指令解析器Complie,对每个元素进行扫描和解析,但是讲那么麻烦不掌握等于白讲,所以就只针对于v-model一种情况写一些代码来获取它的值
}
```
一步步来实现它
### **实现Observer**
Observer是一个数据监听器,核心方法是Object.defineProperty。如果要监听所有属性的话,则需要通过递归遍历,对每个子属性defineProperty。
```text
/**
* 监听器构造函数
* @param {Object} data 被监听数据
*/
class Observe {
constructor(data) {
if (typeof data !== "object" || data === "") {
return;
}
this.$data = data;
this.init()
}
init () {
//Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组
Object.keys(this.$data).forEach(key => {
//key = name 数据劫持 将数据属性转换成访问器属性并挂载到this上
this.observe(this.$data, key, this.$data[key])
})
}
/**
* 监听函数
* */
observe (target, key, val) {
new Observe(val)
Object.defineProperty(target, key, {
get () {
if (Dep.target) {
//一会创建一个watcher方法能够表示订阅者,每次有订阅都可以把它放在dep的静态属性身上,监听函数时候就可以
//判断静态属性上是否有whatch实例如果有的话就把实例push到数组中
dep.addWatch(Dep.target)
}
return val
},
set (newVal) {
val = newVal
dep.targetWatch()
//监听到值变化的时候就调用dep中的targetwatch方法来调用wahcher类中的回调函数
// console.log(val); val就是输入框的最新值
new Observe(val)
}
})
}
}
```
### **实现Dep**
要创建一个可以订阅者的订阅器Dep,主要负责收集订阅者,属性变化的时候执行相应的订阅者,更新函数。
```text
class Dep {
constructor() {
this.watchs = []
}
addWatch (watch) {
this.watchs.push(watch)
}
targetWatch () {
this.watchs.length > 0 && this.watchs.forEach(watcher => {
//循环遍历存放watch实例的数组,调用watch实例中的回调函数的方法
watcher.targetcbk()
})
}
}
Dep.target = null
let dep = new Dep()
```
### **实现Watcher**
订阅者Watcher在初始化的时候需要将自己添加到订阅器Dep中,那该如何添加呢?咱们刚刚写的监听器Observer是在get函数执行添加了订阅者Watcher的操作,所以需要在订阅者Watcher初始化的时候触发对应的get函数去执行添加订阅者操作。那么,怎样去触发get函数?很简单,只需获取对应的属性值就可以触发了,因为已经用Object.defineProperty监听了所有属性。
```text
class Watcher {
constructor(data, key, cbk) {
//把传递过来的参数全部挂载到watch的实例上
Dep.target = this; //把当前实例对象赋给Dep的静态属性上
this.data = data;
this.key = key;
this.cbk = cbk;
this.init()
}
init () {
this.value = utils.getValue(this.data, this.key)
//获取值
Dep.target = null
//给静态属性赋值null 否则会一直往数组里面push实例造成浏览器崩溃
}
targetcbk () {
let res = utils.getValue(this.data, this.key)
this.cbk(res)
//获取值传给回调函数,在回调函数中给当前属性赋输入框的值
}
}
那么问题来了,什么时间实例化,去调用之呢,当然是假如咱们是个表单输入框,当然是输入框里面的值发生变化的时候去实例化喽
```
一些工具类,公共方法
```text
const utils = {
setValue (node, data, val, type) {
// console.log(node[type]);
node[type] = this.getValue(data, val)
},
getValue (data, key) {
if (key.indexOf('.') > -1) {
let keys = key.split('.')
keys.forEach(item => {
data = data[item]
})
return data
} else {
return data[key]
}
},
changeValue (data, key, newValue) {
if (key.indexOf(".") > -1) {
let keys = key.split(".")
for (let i = 0; i < keys.length - 1; i++) {
data = data[keys[i]]
}
data[keys[keys.length - 1]] = newValue
} else {
data[key] = newValue
}
}
}
```
完成之前的话,咱们还是要知道重绘和回流这个东西
```text
html 加载时发生了什么
在页面加载时,浏览器把获取到的HTML代码解析成1个DOM树,DOM树里包含了所有HTML标签,包括display:none隐藏,还有用JS动态添加的元素等。
浏览器把所有样式(用户定义的CSS和用户代理)解析成样式结构体
DOM Tree 和样式结构体组合后构建render tree, render tree类似于DOM tree,但区别很大,因为render tree能识别样式,render tree中每个NODE都有自己的style,而且render tree不包含隐藏的节点(比如display:none的节点,还有head节点),因为这些节点不会用于呈现,而且不会影响呈现的,所以就不会包含到 render tree中。我自己简单的理解就是DOM Tree和我们写的CSS结合在一起之后,渲染出了render tree。
什么是回流
当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候,这时候是一定会发生回流的,因为要构建render tree。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。
什么是重绘
当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。
区别:
他们的区别很大:
回流必将引起重绘,而重绘不一定会引起回流。比如:只有颜色改变的时候就只会发生重绘而不会引起回流
当页面布局和几何属性改变时就需要回流
比如:添加或者删除可见的DOM元素,元素位置改变,元素尺寸改变——边距、填充、边框、宽度和高度,内容改变
浏览器的帮忙
所以我们能得知回流比重绘的代价要更高,回流的花销跟render tree有多少节点需要重新构建有关系
因为这些机制的存在,所以浏览器会帮助我们优化这些操作,浏览器会维护1个队列,把所有会引起回流、重绘的操作放入这个队列,等队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会flush队列,进行一个批处理。这样就会让多次的回流、重绘变成一次回流重绘。
自己的优化
但是靠浏览器不如靠自己,可以改变一些写法减少回流和重绘
比如改变样式的时候,不去改变他们每个的样式,而是直接改变className 就要用到cssText 但是要注意有一个问题,会把原有的cssText清掉,比如原来的style中有’display:none;’,那么执行完上面的JS后,display就被删掉了。
为了解决这个问题,可以采用cssText累加的方法,但是IE不支持累加,前面添一个分号可以解决。
还有添加节点的时候比如要添加一个div里面有三个子元素p,如果添加div再在里面添加三次p,这样就触发很多次回流和重绘,我们可以用cloneNode(true or false) 来避免,一次把要添加的都克隆好再appened就好了,还有其他很多的方法就不一一说了
```
要实现的html
```text
<body>
<div id="box">
<input type="text" v-model="name">
<div>{{name}}</div>
<div>{{info.age}}</div>
</div>
</body>
<script src="./mvvm.js"></script>
<script>
let res = new Vue({
el: "box",
data: {
name: "小明",
info: {
age: 18
}
}
})
</script>
```
接下来就是联动所有的类来进行封装进行咱们自己的双向数据绑定
关于文档碎片
```text
<body>
<div id="box">
<input type="text">
<div>1</div>
<div>2</div>
</div>
</body>
<script>
let fragment = document.createDocumentFragment()
let el=document.querySelector("#box");
let firstChild;
while (firstChild = el.firstChild) {
console.log(firstChild)
fragment.appendChild(firstChild)
}
el.appendChild(fragment);
console.log(el)
</script>
class Vue {
constructor({ el, data }) {
this.$el = document.getElementById(el)
this.$data = data;
this.init()
this.initDom()
}
init () {
//初始化 将数据属性转换成访问器属性并挂载到this上
new Observe(this.$data)
}
initDom () {
//创建文档碎片 相当于虚拟的空间,把页面的节点放入文档碎片 为了解决重绘或回流
// document_createDocumentFragment()说白了就是为了节约使用DOM。每次JavaScript对DOM的操作都会改变页面的变现,并重新刷新整个页面,从而消耗了大量的时间。为解决这个问题,可以创建一个文档碎片,把所有的新节点附加其上,然后把文档碎片的内容一次性添加到document中。这也就只需要一次页面刷新就可
let fragment = document.createDocumentFragment()
let firstChild;
while (firstChild = this.$el.firstChild) {
fragment.appendChild(firstChild)
}
this.compiler(fragment)
this.$el.appendChild(fragment)
}
//一个指令解析器Complie,对每个元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或绑定相应函数。当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。
方便可言,咱们只要实现v-model双向数据绑定即可,不解析其他指令
compiler (node) {
if (node.nodeType === 1) { //为元素节点
let isInp = [...node.attributes].filter(nodeType => {
// nodeName 属性可依据节点的类型返回其名称。
如果节点是一个元素节点 , nodeName 属性将返回标签名。
如果节点是一个属性节点, nodeName 属性将返回属性名。
return nodeType.nodeName === "v-model"
})
if (isInp.length > 0) {
//nodeValue 属性根据节点的类型设置或返回节点的值。
let inpKey = isInp[0].nodeValue;
node.addEventListener("input", (e) => {
//给input绑定input事件,用来获取到输入框的值
let newValue = e.target.value
utils.changeValue(this.$data, inpKey, newValue) //然后找到键值把新值赋给他
})
utils.setValue(node, this.$data, inpKey, "value")
}
} else if (node.nodeType === 3) { //文本节点
//textContent 属性设置或者返回指定节点的文本内容。
//如果你设置了 textContent 属性, 任何的子节点会被移除及被指定的字符串的文本节点替换。
let contextKey = node.textContent && node.textContent.indexOf("{{") > -1 && node.textContent.split("{{")[1].split("}}")[0]
//contextKey = name info.age
contextKey && utils.setValue(node, this.$data, contextKey, "textContent")
contextKey && new Watcher(this.$data, contextKey, (res) => {
//监听所对应属性的变化
node.textContent = res
})
// console.log(contextKey);
}
// console.log(node.childNodes);
//判断他是不是有子项
if (node.childNodes && node.childNodes.length > 0) {
node.childNodes.forEach(item => {
this.compiler(item)
})
}
}
}
```
更多关于“html5培训”的问题,欢迎咨询千锋教育在线名师。千锋已有十余年的培训经验,课程大纲更科学更专业,有针对零基础的就业班,有针对想提升技术的提升班,高品质课程助理你实现梦想。