还是否记得第一次使用Vue时的感觉?双向数据绑定,竟然那么神奇,可以让代码变的如此简单。

一、Vue核心思想

在了解Vue的双向绑定之前,你先要回忆一下Vue的核心思想。还记得getter/setter方法、还记得Watcher?
Vue核心思想

关于getter/setter方法

1.当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项 (图中就是 a 对象中的 b 属性,即 a.b),Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter 方法。
2. 用户看不到 getter/setter 方法,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。
3. 同时 Vue 会对模板进行编译,解析之后会生成指令对象,也例如 v-text, v-hide 等。当指令中 v-text='a.b'时,其实就是触发 getter 方法,获取对应的数据。

另外每个属性都对应有一个 Watcher,它有什么作用?

1.当 data 选项中 a.b 值发生改变时,就会触发 setter 方法,会通知到对应的 watcher。
2. 之后再通知指令去调用 update 方法,由于指令是 DOM 的封装,所在在 update 时其实就是调用了原生 javascript 的 DOM 方法来更新界面。

二、关于Object.defineProperty

在讲解具体原理前,必须先要了解到的方法,可能你在看到这个方法之后,就有点感觉了。
Object.defineProperty该方法允许精确添加或修改对象的属性。

语法 Object.defineProperty(obj, prop, descriptor)
    obj: 要在其上定义属性的对象。
    prop: 要定义或修改的属性的名称。
    descriptor: 将被定义或修改的属性描述符。
        configurable
当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
        enumerable: 当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
        get:  一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
        set: 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。

在Vue中,其实是使用Object.defineProperty进行了数据劫持,当获取数据或者修改数据时,都能通过自定义的getter/setter截取到。

<script type="text/javascript">
    document.onclick = function(){
        var user = {name: 'atom'}
        key = 'name'
        
        function defineProperty(obj, key, val){
            Object.defineProperty(obj, key, {
                enumerable: true, 
                
                get(){    // getter方法
                    console.log('getter方法获取到的值:' + val)
                    return val
                },
                set(newVal){    // setter方法
                    if (val !== newVal){
                        console.log(val + ' (setter方法)修改为: ' + newVal)
                        val = newVal
                    }
                }
            })
        }
        


        // 调用(即添加getter和setter方法)
        defineProperty(user, key, user[key])
        
        console.log(user.name)    // 获取值
        user.name = '阿童木'    // 设置值
    }
</script>

Snip20190603_8.png

三、DOM元素和数据的对应

从上面简单的例子可以看到,在获取值的时候触发get方法,在设置值的时候触发set方法。
即此时我们就可以在get/set方法中,截取到对应的数据。

接下来就要思考一下,在set方法中 对应的修改视图的函数,就能在data发生变化时,从而达到视图同步更新。相关问题:

1、如何获取到视图中的DOM元素?
    最简单,最暴力的方法 ---- 遍历

2、多个属性时,如何解决属性和对应DOM保持一致性问题?
    每个属性对应一个回调函数。
    当属性发生改变之后,直接调用自身的回调函数。
    
    类似: 一个公司中有100个员工,每个员工的名字不同、技能不一样,如何保证老板不弄错他们的姓名以及技能?这需要老板记住每个员工的相貌特征、对应的名字以及技能吗?
    解决: 只要每个员工挂一个工牌,添加上姓名、技能。当老板想知道你的时,只需要拿你的工牌看一下就可以了。 
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    
    <body>
        <div id="app">
            <h3>名字: {{name}}</h3>
            <h3>技能: {{skill}}</h3>
        </div>
        
        <div>
            请输入你的名字:<input type="text" class="text" value="" />
            <input type="button" class="bt" value="确定" />
        </div>
        
        
        <script type="text/javascript">
            function Vue(options){    // 动态原型方式  创建名字为Vue的类
                this.el = options.el        // 元素
                this.data = options.data     // 数据
                this.watcher = {}    // 属性、数据、元素 的关联
                
                // 给每个属性添加get/set方法
                var self = this 
                Object.keys(this.data).forEach(function(key){    // 遍历所有key
                    // 通过原型属性添加的方法
                    self.defineProperty(self.data, key, self.data[key])    
                })
                
                // 通过原型属性添加的方法-解析DOM
                this.compile()    
            }
            
            
            Vue.prototype = {    // 添加原型属性
                defineProperty(obj, key, val){    // 属性添加get/set方法
                    var self = this         // 后续this指向问题的处理
                    Object.defineProperty(obj, key, {
                        enumerable: true, 
                        get(){
                            return val
                        },
                        set(newVal){
                            if (val !== newVal){
                                val = newVal
                                self.watcher[key](newVal)    // 调用watcher中key对应的函数 进行更新
                            }
                        }
                    })
                },
                
                compile(){    // 解析DOM即更新数据
                    // 获取到对象绑定的元素
                    var ele = document.querySelector(this.el);
                    
                    // 所有子元素
                    var childEls = ele.childNodes;
                    
                    // 遍历所有子元素
                    // Array.slice.call(arguments),目的是将arguments对象的数组提出来转化为数组,arguments本身并不是数组而是对象
                    [].slice.call(childEls).forEach(el => {    // ES6写法,这就可以避免this指向问题(上面是没使用ES6写法)
                        // 定义正则表达式,即两个花括号 括住的
                        var reg = /\{\{(.*)\}\}/;
                        
                        // 文本内容
                        var text = el.textContent;
                        
                        if(reg.test(text)){    // 正则匹配
                            var key = reg.exec(text)[1]; // 获取到key
                            el.textContent = this.data[key]; // data中存储的key值渲染到视图中
                            
                            // 更新的方法操作  每个key都对应有更新的方法
                            // 当数据发生改变时,就有对应的方法进行更新
                            this.watcher[key] = function(newVal){
                                el.textContent = newVal
                            }
                        }
                    })
                }
            }
            
            
            
            // 创建Vue对象
            var myVue = new Vue({
                el: '#app',
                data: {
                    name: '阿童木',
                    skill: 'web前端开发',
                }
            })
            
            // 修改名字
            document.querySelector('.bt').onclick = function(){
                myVue.data.name = document.querySelector('.text').value
            }
        </script>
    </body>
</html>
代码大致执行过程:
1、实例化对象
2、实例化过程中,代码中重要的点是遍历data选项中的每个key
3、在defineProperty中添加上get方法
4、在set方法中添加 界面更新的函数调用 【注意: set方法触发后才是调用,此时只是添加set方法】
5、解析DOM 【即将data中的值,先更新到界面中】
6、并给每个key对应上 更新方法  存储在 watcher 中

之后,只要数据发生改变,此时就是触发了set方法,再从watcher中找到key对应的方法进行更新页面。

代码执行过程


本文由 zyz 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

楼主残忍的关闭了评论