FE_Vue核心知识的学习 v-model 双向绑定原理

1 M[模型] - V[页面视图] - VM[VM实例对象]

虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示 Vue 实例。
在这里插入图片描述

<body>
<!--准备好一个容器-->
<div id="v-bind-date">
    单项数据流动: <input type="text" v-model="url">
    <h1>{{this.$createElement.length}}</h1>
</div>
<script>
    Vue.config.productionTip = false
    let app = new Vue({
        el: '#v-bind-date', // el用于指定当前Vue实例为哪个容器服务。
        data: {
            url: 'https://www.baidu.com'

        }
    })
</script>
</body>

MVVM模型:

1.M:模型(Model):对应data中的数据
2.V:视图(View):模板
3.VM:视图模型(ViewModel):Vue实例对象 观察发现:
1.data中所有的属性,最后都出现在VM身上。
2.VM身上的所有属性即Vue原型上所有属性,在Vue模板中都可以直接使用。

2 JS 相关知识 - 为对象添加一个属性 - Object.defineProperty

<script>
    let person = {
        name: 'zhaoshuai-lc',
        sex: 'male'
    }
    Object.defineProperty(person, 'age', {
        value: 18
    })
    console.log(person)
    for (let personKey in person) {
        console.log(person[personKey])
    }
</script>

在这里插入图片描述
通过遍历对象可以发现 - age 是不可以被枚举的
在这里插入图片描述

2.1 控制属性是否可以枚举 enumerable:true

    Object.defineProperty(person, 'age', {
        value: 18,
        enumerable:true, //控制属性是否可以枚举,默认值为false
    })

在这里插入图片描述
可以被枚举,但是不可以被修改
在这里插入图片描述

2.2 控制属性是否可以被修改 writable:true && configurable:true - 控制属性是否可以被删除

    Object.defineProperty(person, 'age', {
        value: 18,
        enumerable:true, //控制属性是否可以枚举,默认值为false
        writable:true, //控制属性是否可以被修改,默认值为false
        configurable:true //控制属性是否可以被删除,默认值为false
    })

2.3 对象属性存在的 - get() set()

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script type="text/javascript">
    let number = 19
    let person = {
        name: 'zhaoshuai-lc',
        sex: 'male'
    }
    Object.defineProperty(person, 'age', {
        // 当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
        get() {
            console.log('有人读取age属性了')
            return number
        },

        // 当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
        set(value){
            console.log('有人修改了age属性,且值是',value)
            number = value
        }
    })
    console.info("person", person)
    console.info("number", number)
</script>
</body>
</html>

在这里插入图片描述

3 引出理解数据代理

数据代理:通过一个对象代理对另一个对象中属性的操作 (读/写)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script type="text/javascript">
    let obj1 = {
        x: 100
    }
    let obj2 = {
        y: 200
    }
    Object.defineProperty(obj2, 'x', {
        get() {
            return obj1.x
        },
        set(value) {
            obj1.x = value
        }
    })

</script>
</body>
</html>

在这里插入图片描述

4 Vue中的数据代理

将鼠标放到实例中的数据上,可以看到也提示了“Invoke property getter”。也就是说当有人访问name的时候,getter就在工作。
在这里插入图片描述
在这里插入图片描述
vm._data === data 为ture。也就是说_data完全来自于data:
在这里插入图片描述
1.Vue创建实例对象vm—>2.Vue收集data数据—>3.Vue往vm的_data上添加name、address(通过getter)等属性:
在这里插入图片描述
数据代理:通过vm.xxx来代理vm._data.xxx的操作(读/写),目的就是为了编码更方便。

_data中的属性做了一个数据劫持,把data里面的属性做了修改/升级,以便更好地完成响应式操作。

基本原理:

通过Object.defineProperty()把_data对象中所有属性添加到vm上。为每一个添加到vm上的属性,都指定一个getter/setter。在getter/setter内部去操作(读/写)_data中对应的属性。

5 vm数据更新时的一个问题-this.personList[0] = { 更新值 }不起作用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.1/vue.js"></script>
</head>
<body>
<div id="v-for">
    <button @click="updateMei">更新马冬梅的信息</button>
    <ul>
        <li v-for="(item, index) of personList" :key="item.id">
            {{ item.name }} - {{ item.age }}
        </li>
    </ul>

</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#v-for',
        data: {
            personList: [
                {id: '001', name: '马冬梅', age: 19, sex: '女'},
                {id: '002', name: '周冬雨', age: 24, sex: '女'},
                {id: '003', name: '周杰伦', age: 55, sex: '男'},
                {id: '004', name: '温兆伦', age: 12, sex: '男'}
            ]
        },
        methods: {
            updateMei() {
                this.personList[0].name = '马保国'
                this.personList[0].age = 50
                this.personList[0].sex = '男'
            }
        }
    })
</script>
</body>
</html>

在这里插入图片描述
以上代码是起作用的,这是为什么呢?原因如下所示【getter setter】
在这里插入图片描述
但是,我们修改为如下代码就不起作用了:
在这里插入图片描述
结果如下所示:这是为什么呢?
在这里插入图片描述
这是因为,如下所示:
在这里插入图片描述

6 引出 Vue监测数据的原理_对象

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.1/vue.js"></script>
</head>
<body>
<div id="v-school">
    学校名称:{{ name }} <br/>
    学校地址:{{ address }}
</div>
<script type="text/javascript">
    let vm = new Vue({
        el: '#v-school',
        data: {
            name: '北京大学',
            address: '北京'
        }
    })
</script>
</body>
</html>

在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
<script type="text/javascript">

    let data = {
        name: '尚硅谷',
        address: '北京',
    }

    // 创建一个监视的实例对象,用于监视data中属性的变化
    const obs = new Observer(data)
    console.log(obs)

    // 准备一个vm实例对象
    let vm = {}
    vm._data = data = obs

    function Observer(obj) {
        // 汇总对象中所有的属性
        const keys = Object.keys(obj)
        // 遍历
        keys.forEach((k) => {
            Object.defineProperty(this, k, {
                get() {
                    return obj[k]
                },
                set(val) {
                    console.log('${k}被改了,我要去解析模板,生成虚拟DOM...我要开始忙了')
                    obj[k] = val
                }
            })
        })
    }

</script>
</body>
</html>

Vue监测数据的原理,就是靠setter。

7 Vue.set()方法

  1. 假设数组a中不存在对象b,Vue中访问a.b不会报错,只是不显示(Vue中默认undefined不显示),Vue中访问b会报错。
  2. Vue.set 只能给data里的某个对象追加属性,不能直接给data追加属性。
  3. 向响应式对象中添加一个property,并确保这个新property同样是响应式的【具备getter setter】,且触发视图更新。
    在这里插入图片描述
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
    <script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.1/vue.js"></script>
</head>
<body>
<div id="v-set">
    <h1>学校信息</h1>
    <h2>学校名称:{{ name }}</h2>
    <h2>学校地址:{{ address }}</h2>
    <hr/>
    <h1>学生信息</h1>
    <button @click="addSex">添加一个性别属性,默认值是男</button>
    <h2>姓名:{{ student.name }}</h2>
    <h2>性别:{{ student.sex }}</h2>
    <h2>年龄:真实{{ student.age.rAge }}, 对外{{ student.age.sAge }}</h2>
    <h2>朋友们:</h2>
    <ul>
        <li v-for="(f,index) in student.friends" :key="index">
            {{ f.name }}--{{ f.age }}
        </li>
    </ul>
</div>

<script type="text/javascript">
    Vue.config.productionTip = false

    let vm = new Vue({
        el: '#v-set',
        data: {
            name: '北京大学',
            address: '北京',
            student: {
                name: 'tom',
                age: {
                    rAge: 40,
                    sAge: 29
                },
                friends: [
                    {name: 'jerry', age: 35},
                    {name: 'tony', age: 36}
                ]
            }
        },
        methods: {
            addSex() {
                // Vue.set(this.student,'sex','男')
                this.$set(this.student, 'sex', '男')
            }
        },
    })
</script>
</body>
</html>

8 Vue监测数据的原理_数组

原生Javascript数组使用的方法,例如push,就是从Array原型中找到的。可用 arr.push === Array.prototype.push 验证。 而Vue中的push却不等于 Array.prototype.push ,因为Vue中的push是经过包装的。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
    <script type="text/javascript" src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.1/vue.js"></script>
</head>
<body>
<div id="ddd">
    <h1>学生信息</h1>
    <h2>爱好:</h2>
    <ul>
        <li v-for="(item, index) in student.hobby" :key="index">
            {{ item }}
        </li>
    </ul>
</div>

<script type="text/javascript">
    Vue.config.productionTip = false

    const vm = new Vue({
        el: '#ddd',
        data: {
            student: {
                name: 'tom',
                age: 18,
                hobby: ['抽烟', '喝酒', '烫头'],
                friends: [
                    {name: 'jerry', age: 35},
                    {name: 'tony', age: 36}
                ]
            }
        },
        methods: {
        },
    })
</script>
</body>
</html>

在这里插入图片描述
在这里插入图片描述
针对于上面的数组,有了解决方案:如下
在这里插入图片描述
以后对于数组的修改,使用如下方法:
在这里插入图片描述
Vue监视数据的原理:

  1. Vue会监视data中所有层次的数据。
  2. 如何监测对象中的数据?
 通过setter实现监视,且要在new Vue时就传入要监测的数据。
(1)对象中后追加的属性,Vue默认不做响应式处理。
(2)如需给后添加的属性做响应式,请使用如下API:
  vm.set(target,propertyName/index,value) 或
  vm.$set(target,propertyName/index,value)
  1. 如何监测数组中的数据?
 通过包裹数组更新元素的方法实现,本质就是做了两件事:
(1)调用原生对应的方法对数组进行更新。
(2)重新解析模板,进而更新页面。
  1. 在Vue修改数组中的某个元素一定要用如下方法:
(1)使用这些API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
(2)Vue.set() 或 vm.$set()

特别注意:Vue.set() 和 vm.$set() 不能给 vm 或 vm的根数据对象添加属性!

9 v-model双向绑定原理

定义:vue中双向绑定就是指v-model指令,可以绑定一个响应式数据到视图,同时视图中变化能同步改变该值。

在这里插入图片描述
通过Object.defineProperty( )对属性设置一个set函数,当数据改变了就会来触发这个函数,所以我们只要将一些需要更新的方法放在这里面就可以实现data更新view了。
在这里插入图片描述
vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。

我们已经知道实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性。

如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。

因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。

接着,我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令(如v-model,v-on)对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。

10 手撕源码-模板解析

在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script type="text/javascript" src="./vue.js"></script>
<div id="app">
    <h1>{{ name }}</h1>
    {{ title }}
</div>

<script type="text/javascript">
    new Vue({
        el: '#app',
        data: {
            name: 'zhaoshuai-lc',
            title: 'inspur'
        }
    })
</script>

</body>
</html>

我们自定义vue.js源码:
class Vue {
    constructor(options) {
        this.$el = document.querySelector(options.el)
        this.$data = options.data
        this.compile(this.$el)
    }

    compile(node) {
        node.childNodes.forEach((item, index) => {
            if (item.nodeType == 1) {
                this.compile(item)
            }
            if (item.nodeType == 3) {
                let reg = /\{\{(.*?)\}\}/g;
                let text = item.textContent;
                item.textContent = text.replace(reg, (match,vmKey) => {
                    vmKey = vmKey.trim()
                    return this.$data[vmKey]
                })
            }
        })
    }
}


11 手撕源码-添加事件

在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script type="text/javascript" src="./vue.js"></script>
<div id="app">
    <h1>{{ name }}</h1>
    {{ title }}
    <button @click="btn">点击事件</button>
</div>

<script type="text/javascript">
    new Vue({
        el: '#app',
        data: {
            name: 'zhaoshuai-lc',
            title: 'inspur'
        },
        methods: {
            btn(e) {
                console.log('手写点击事件ok ... ')
                console.log(e)
            }
        }
    })
</script>

</body>
</html>

class Vue {
    constructor(options) {
        this.$optinos = options
        this.$el = document.querySelector(options.el)
        this.$data = options.data
        this.compile(this.$el)
    }

    compile(node) {
        node.childNodes.forEach((item, index) => {
            if (item.nodeType == 1) {
                if (item.hasAttribute('@click')) {
                    let vmKey = item.getAttribute('@click').trim();
                    item.addEventListener('click', (event) => {
                        this.eventFn = this.$optinos.methods[vmKey].bind(this);
                        this.eventFn(event)
                    })
                }
                if (item.childNodes.length > 0) {
                    this.compile(item)
                }
            }
            if (item.nodeType == 3) {
                let reg = /\{\{(.*?)\}\}/g;
                let text = item.textContent;
                item.textContent = text.replace(reg, (match,vmKey) => {
                    vmKey = vmKey.trim()
                    return this.$data[vmKey]
                })
            }
        })
    }
}

12 手撕源码-数据劫持

引出一个问题:
在这里插入图片描述
我们在绑定事件的方法 btn() 方法中打印 this:data里面的数据我们需要使用 this.$data.xxx方可获取到,但是我们vue中直接通过this.xxx就可以获取到数据这里就引出了本章节要讨论的问题了- 数据劫持的问题
在这里插入图片描述
我们这里的解决问题的思路是:Object.defineProperty()

在这里插入图片描述
在这里插入图片描述
源码如下所示:

class Vue {
    constructor(options) {
        this.$optinos = options
        this.$el = document.querySelector(options.el)
        this.$data = options.data
        this.proxyData()
        this.compile(this.$el)
    }
    // 使用来自于data里面的数据给Vue赋值属性
    //  data中的属性值和Vue对象的属性保持双向(劫持)
    proxyData() {
        for (let key in this.$data) {
            Object.defineProperty(this, key, {
                get() {
                    return this.$data[key]
                },
                set(v) {
                    this.$data[key] = v
                }
            })
        }
    }


    compile(node) {
        node.childNodes.forEach((item, index) => {
            if (item.nodeType == 1) {
                if (item.hasAttribute('@click')) {
                    let vmKey = item.getAttribute('@click').trim();
                    item.addEventListener('click', (event) => {
                        this.eventFn = this.$optinos.methods[vmKey].bind(this);
                        this.eventFn(event)
                    })
                }
                if (item.childNodes.length > 0) {
                    this.compile(item)
                }
            }
            if (item.nodeType == 3) {
                let reg = /\{\{(.*?)\}\}/g;
                let text = item.textContent;
                item.textContent = text.replace(reg, (match,vmKey) => {
                    vmKey = vmKey.trim()
                    return this.$data[vmKey]
                })
            }
        })
    }
}

13 手撕源码-更新视图

在这里插入图片描述
对以上代码的理解为:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

15 手撕源码-v-model 的双向绑定

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值