vue源码之双向绑定原理

了解Object.defineProperty

了解过vue绑定原理的人都知道。双向绑定的原理是利用数据劫持结合发布者–订阅者模式的方式,通过Object.defineProperty来劫持各个属性setter、getter,在数据发生变动时发布消息给订阅者,触发响应的回调函数。
简单了解一下Object.defineProperty,具体用法查看MDN

手动实现简单的绑定

var obj  = {};
Object.defineProperty(obj, 'name', {
        get: function(val) {
            console.log('获取值被修改的值')
            return val;
        },
        set: function (newVal) {
            console.log('我被设置了'+ newVal)
        }
})
obj.name = '隔壁老王';//在给obj设置name属性的时候,触发了set这个方法
var val = obj.name;//在得到obj的name属性,会触发get方法

这样我们就可以在get和set中触发其他函数,从而来实现监听数据变动的目的。
根据以上描述,我们可以实现一个简单的双向绑定:代码如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="bindDemo"></div>
    <input type="text" id="iptVal">
    <script>
        var bindDemo = document.getElementById('bindDemo')
        var iptVal = document.getElementById('iptVal')
        var obj = {}
        Object.defineProperty(obj, 'name',{
            get:function(){
                return val
            },
            set:function(newVal){ // 给对象设置值得时候会触发该方法
                console.log(newVal)
                bindDemo.innerHTML = newVal
            }
        })
        iptVal.addEventListener('input', function(e){
            obj.name = e.target.value
        })
        obj.name = '老李' // 给obj设置了name属性从而触发了set方法
    </script>
</body>
</html>

这样就实现了一个简单的双向绑定。

vue双向绑定

原理图镇楼:

劫持监听所有属性
通知变化
通知变化
添加订阅者
解析指令
订阅数据变化
初始化视图
更新视图
MVVM
Observer
Dep
Watcher
compile
updater

原理图解析:
1、observer的作用:通过object.defineProperty()循环劫持vue中data的所有属性值,从而利用get和set来通知订阅者Dep,从而来更新视图。
2、指令解析:我们都知道在vue中实现双向绑定的常用指令有:v-model,v-text,{{}}等等。也就是说在渲染html节点时,碰到这些指令的时候会进行指令解析。每碰到一个指令,就会在Dep中增加一个订阅者,这个订阅者只是更新自己指令对应的数据。每当set方法触发,就会循环触发Dep中对应的订阅者。
实现一个observer监听器,通过递归的方法遍历所有的对象以及对象中的对象也就是属性值,从而来监听所有的属性

所有对象的属性劫持

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <div id="bindDemo"></div>
    <input type="text" id="iptVal">
    <script>
        var bindDemo = document.getElementById('bindDemo')
        var iptVal = document.getElementById('iptVal')
        function defineReactive(obj,key,val){
            observe(val);
            Object.defineProperty(obj, key, {
                get: function(){
                    //在这里进行依赖收集
                    return val
                },
                set: function(newVal){
                    if(newVal === val) return
                    console.log(newVal)
                    iptVal.value = newVal
                    bindDemo.innerHTML = newVal
                }
            })
        }
        /**通过遍历所有属性的方式对这个obj进行defineReactive的处理***/ 
        function observe (obj) {
            debugger
            if(!obj || (typeof obj !=='object')) {
                return
            } 
            Object.keys(obj).forEach(function(key){
                defineReactive(obj, key, obj[key]);
            })
        }
        var dataList ={
            person:{
                name: '老王',
                age: '17'
            }
        }
        observe(dataList)
        iptVal.addEventListener('input', function(e) {
            // 给obj的name属性赋值,进而触发该属性的set方法
            dataList.person.name = e.target.value;
        });
        dataList.person.name = '老李'
        // 这样就实现了所有的对象以及属性的监听
    </script>
</body>
</html>

以上代码实现了对象属性值的劫持,下面通过解析指令实现对view和model的绑定

指令解析

 /** 解析指令,实现对view和model的绑定*/ 
 compile(root,vm){
       // var _this = this
		var nodes =root.children
       // 节点类型为元素
       for (let i = 0; i < nodes.length; i++) {
           var node = nodes[i]
           if (node.children.length) {
               vm.compile(node,vm)
           }
           if (node.hasAttribute('v-click')) {
              node.onclick=(function(e){
                   var attrval = nodes[i].getAttribute('v-click')
                   console.log(attrval)
                   return vm.methods[attrval].bind(vm.data)
              })()
           }
           if (node.hasAttribute('v-model')&&(node.tagName == 'INPUT' || node.tagName == 'TEXTAREA' )) {
               node.addEventListener('input',(function(e){
                   var name= node.getAttribute('v-model')
                   new watcher(vm, node, name, 'value') 
                   return function(){
                       vm.data[name] = nodes[e].value
                   }
               })(i))
           }
           if (node.hasAttribute('v-bind')) {
               var name = node.getAttribute('v-bind')
               new watcher(vm, node, name, 'innerHTML') 
           }
           
       }
   }
}

订阅器

创建一个可以容纳订阅者的消息订阅器Dep,订阅器Dep主要负责收集订阅者,然后在属性变化的时候执行对应订阅者的更新函数。所以显然订阅器需要有一个容器,这个容器就是list,将上面的Observer稍微改造下,植入消息订阅器:

 defineReactive (obj,key,val){
    const dep = new Dep()
    Object.defineProperty(obj,key,{
        enumerable: true,
        configurable: true,
        get:function(){
            /*****进行依赖收集(需要一个方法)将Dep.target(即当前的Watcher对象存入dep的subs中)******/
            if (Dep.target) {
                dep.addsub(Dep.target)
            }
            return val 
        },
        set:function(newVal){
            if (newVal === val) return
            val = newVal
            dep.notify()
        }
    })
}
// 构造订阅者Dep
class Dep {
    constructor(){
        /* 用来存放Watcher对象的数组 */
        this.subs = []
    }
    /*在subs中添加一个watch对象*/
    addsub(sub){
        this.subs.push(sub)
    }
    /*通知所有对象更新视图*/ 
    notify(){
        this.subs.forEach((sub) =>{
            sub.update()
        })
    }
} 

从代码上看,我们将订阅器Dep添加一个订阅者设计在getter里面,这是为了让Watcher初始化进行触发,因此需要判断是否要添加订阅者,至于具体设计方案,下文会详细说明的。在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。到此为止,一个比较完整Observer已经实现了,接下来我们开始设计Watcher。

watch

我们知道,监听器Observer是在get函数执行了添加订阅者Wather的操作的,所以我们只要在订阅者Watcher初始化的时候触发对应的get函数去执行添加订阅者操作即可。而触发get函数只要获取对应的属性值就可以了。核心原因就是因为我们使用了Object.defineProperty( )进行数据监听。

 class watcher{
  constructor(vm, node, name, type){
       /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
       Dep.target = this
       this.name = name //指令对应的值
       this.node = node //节点
       this.vm = vm     //指令所属Vue
       this.type = type //绑定的属性值,本例为InnerHTML
       this.update()
       Dep.target = null
   }
   update() {
       this.get()
       // this.node.nodeValue = this.value 
       this.node[this.type] = this.value 
   }
   get() {
       this.value = this.vm.data[this.name]
   }
}

结语

到此为止,vue双向绑定的原理基本实现。这篇文章只是粗略的的概述了一下vue双向绑定的原理。本文的完整代码请参考这里。如果你觉得还行的话点个赞就行。如果发现有什么不足的话,欢迎指出。

猜你喜欢

转载自blog.csdn.net/duanshilong/article/details/88061596