vue源码之双向绑定原理

vue源码之双向绑定原理

    • 了解Object.defineProperty
    • 手动实现简单的绑定
    • vue双向绑定
      • 所有对象的属性劫持
      • 指令解析
      • 订阅器
      • watch
    • 结语

了解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中触发其他函数,从而来实现监听数据变动的目的。
根据以上描述,我们可以实现一个简单的双向绑定:代码如下




    
    
    
    Document


    

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

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监听器,通过递归的方法遍历所有的对象以及对象中的对象也就是属性值,从而来监听所有的属性

所有对象的属性劫持




    
    
    
    Document


    

以上代码实现了对象属性值的劫持,下面通过解析指令实现对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双向绑定的原理。本文的完整代码请参考这里。如果你觉得还行的话点个赞就行。如果发现有什么不足的话,欢迎指出。

你可能感兴趣的:(vue)