Vue实现MVVM

Vue实现MVVM

js创建对象的两种⽅式

//第⼀种
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);
 }
}

属性描述符:

数据属性:数据属性包含⼀个数据值的位置,在这个位置可以读取和写⼊值

1、可配置性 [[Configurable]] : 表示能否通过delete删除属性,能否修改属性特性,能否把数据属性修改为访问器属性。
2、可枚举性[[Enumerable]]:表示能否通过for-in循环返回属性。
3、可写⼊性[[Writable]]:表示能否修改属性值。
4、属性值[[Value]]:表示属性值。

访问器属性:是包含⼀对gettersetter函数

**Object.defineProperty()**⽅法对数据属性和访问器属性进⾏修改

该⽅法接受三个参数:属性所在对象,属性名字和⼀个描述符对象

**Object.getOwnPropertyDescriptor()**⽅法取得指定对象指定属性的描述符

这个⽅法接收两个参数:属性所在对象,属性名字

如果要求对⽤户的输⼊进⾏特殊处理,或者设置属性的依赖关系,

就需要⽤到访问器属性了

Object.defineProperty

ES6中某些方法的实现依赖于它,VUE通过它实现双向绑定

此方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象

语法

Object.defineProperty(object, attribute, descriptor)
这三个参数都是必输项
第一个参数为目标对象
第二个参数为需要定义的属性或者方法
第三个参数为目标属性所拥有的特性

descriptor
value: 属性的值
writable: 属性的值是否可被重写(默认为falseconfigurable: 总开关,是否可配置,若为false, 则其他都为false(默认为falseenumerable: 属性是否可被枚举(默认为falseget: 获取该属性的值时调用
set: 重写该属性的值时调用

简单定义一下

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 后,就不能再设置了,否则报错, 例子

var a= {}
Object.defineProperty(a,"b",{
  configurable:false
})
Object.defineProperty(a,"b",{
  configurable:true
})
//error: Uncaught TypeError: Cannot redefine property: b

writable

是否可重写

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() 中被枚举

var a= {}
Object.defineProperty(a,"b",{
  value:3445,
  enumerable:true
})
console.log(Object.keys(a));// 打印["b"]

enumerable改为false

var a= {}
Object.defineProperty(a,"b",{
  value:3445,
  enumerable:false //注意咯这里改了
})
console.log(Object.keys(a));// 打印[]

set 和 get

如果设置了 set 或 get, 就不能设置 writable 和 value 中的任何一个,否则报错

var a = {}
Object.defineProperty(a, 'abc', {
    value: 123,
    get: function() {
        return value
    }
})

对目标对象的目标属性 赋值和取值 时, 分别触发 set 和 get 方法

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双向绑定的原理就是这个。

 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

此方法可以一次设置多个属性,例子:

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)

数据绑定⼩案例

<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

img

MVVM设计模式

Vue双向绑定的的原理

vue.js 是采⽤数据劫持结合发布者-订阅者模式的⽅式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

具体思路:

第⼀步:需要oberver的数据对象进⾏递归遍历,包括⼦属性对象的属性,都加上settergetter⽅法

这样做就可以监听到数据的变化。

第⼆步:compile解析模版指令,将模版中的变量替换成数据,然后初始化渲染⻚⾯视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,⼀旦数据有变动,收到通知,更新视图。

第三步:Watcher订阅者是ObserverCompile之间通信的桥梁,主要做事情是:

  1. 在⾃身实例化时往属性订阅器(dep)⾥⾯添加

2.待属性变动通知时,触发Compile中的绑定的回调函数

第四步:MVVM作为数据绑定的⼊⼝,整合ObserverCompileWatcher三者,通过Observer

监听⾃⼰的model数据变化,通过Compile来解析编译模版指令,最终利⽤Watcher搭起ObserverCompile之间的通信桥梁,达到数据变化**->视图更新->视图交互变化(input)->数据model**变更的双向绑定效果

Object.defineProperty

vue通过Object.defineProperty来实现数据劫持,会对数据对象每个属性添加对应的get和set方法,对数据进行读取和赋值操作就分别调用get和set方法。

Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
        // do something
        return val;
    },
    set: function(newVal) {
        // do something
    }
});

可以将一些方法放到里面,从而完成对数据的监听(劫持)和视图的同步更新。

img

ok,再解释一遍过程

实现双向数据绑定,首先要对数据进行数据监听,需要一个监听器Observer,监听所有属性。如果属性发生变化,会调用setter和getter,再去告诉订阅者Watcher是否需要更新。由于订阅者有很多个,我们需要一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理。还有,我们需要一个指令解析器Complie,对每个元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或绑定相应函数。当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图

img

//这是一个监听器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。

/**
 * 监听器构造函数
 * @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,主要负责收集订阅者,属性变化的时候执行相应的订阅者,更新函数。

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监听了所有属性。

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) 
        //获取值传给回调函数,在回调函数中给当前属性赋输入框的值
    }
}
那么问题来了,什么时间实例化,去调用之呢,当然是假如咱们是个表单输入框,当然是输入框里面的值发生变化的时候去实例化喽

一些工具类,公共方法

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
        }
    }
}

完成之前的话,咱们还是要知道重绘和回流这个东西

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

<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>

接下来就是联动所有的类来进行封装进行咱们自己的双向数据绑定

关于文档碎片

<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)
            })
        }
    }
}

Content.indexOf(“{ {”) > -1 && node.textContent.split(“{ {”)[1].split(“}}”)[0]
//contextKey = name info.age
contextKey && utils.setValue(node, this.KaTeX parse error: Expected 'EOF', got '&' at position 45: …t") contextKey &̲& new Watcher(t…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)
})
}
}
}






猜你喜欢

转载自blog.csdn.net/sdasadasds/article/details/125562045
今日推荐