zoukankan      html  css  js  c++  java
  • Vue之双向绑定原理动手记

      Vue.js的核心功能有两个:一是响应式的数据绑定系统,二是组件系统。本文是通过学习他人的文章,从而理解了双向绑定原理,从而在自己理解的基础上,自己动手实现数据的双向绑定。

      目前几种主流的mvc(vm)框架都实现了单向数据绑定,而双向数据绑定我觉得就是在单向绑定的基础上,给input、textarea等可输入元素添加change事件,从而动态修改model和view。我们可以动手做一个。

    <body>
          <input type="text" id="test">
          <span id="show"></span>
          <script type="text/javascript">
              let obj = {};
              Object.defineProperty(obj,'name',{
                  configurable: false,
                  enumerable: true,
                  get: function() {
                      return val
                  },
                  set: function(newVal) {
                      document.getElementById('test').value = newVal;
                      document.getElementById('show').innerHTML = newVal;
                  }
              })
              document.addEventListener('keyup',function(e) {
                  obj.name = e.target.value
              })
          </script>
    </body>

    此时的效果是:在input中输入内容,会同步显示到span中;在控制台中修改输入obj.name的值,视图也会得到相应的更新。这样就实现了一个简单的数据双向绑定。

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

      整理了一下实现数据双向绑定的思路:

      1.实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如果有变动可以拿到最新值并通知订阅者;

      2.实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定到相应的更新函数

      3.实现一个Watcher,作为Observer和Compile的桥梁,能够订阅并接收每个属性变动的通知,执行绑定回调函数相应的更新函数,从而更新视图

      4.入口函数Vue

    以下是我们需要实现的:

    <div id="app">
        <input type="text" v-model="name">
        <p>{{name}}</p>
        <p v-text="name"></p>
        {{name}}
    </div>
    <script>
        let vm = new Vue({
            el: 'app',
            data: {
                name: 'zengfp'
            }
        })
    </script>

    实现Observer

    我们知道可以利用Object.defineProperty()来监听属性的变动,那么我们将需要用observer对数据对象进行递归遍历,包括子属性对象的属性,(但是在这里,我做的比较简单,所以就不需要对进行递归遍历了。)都加上setter和getter。如果给这个对象赋值,就会触发setter函数,那么就能监听到数据变化。相关代码如下:

            function Observe(obj){
                  if(!obj || typeof obj !== 'object'){
                      return
                  }
                  Object.keys(obj).forEach(function(key) {
                      defineReactive(obj,key,obj[key])
                  })
              }    
    
            function defineReactive(obj,key,val){
                  Object.defineProperty(obj,key,{
                      configurable: false,
                      enumerable: true,
                      get: function() {
                          return val
                      },
                      set: function(newVal) {
                          val = newVal;
                          console.log('我的数据发生了改变:'+ val + '->' + newVal)
                      }
                  })
              }

    这样我们就可以监听到每个数据的变化了,接下来就是在监听到数据变化之后怎么通知订阅者了,所有接下来需要实现一个消息订阅器,用一个简单的数组,作为订阅者收集器,数据变动触发notify(),在调用订阅者的update()方法,代码改善后如下:

         function Dep() {
                this.subs = []
            }
            Dep.prototype = {
                addSub: function(sub) {
                    this.subs.push(sub)
                },
                notify: function() {
                    this.subs.forEach(function(sub) {
                        sub.update()
                    })
                }
            }
        function defineReactive(obj,key,val){
            let dep = new Dep() Object.defineProperty(obj,key,{ configurable: false, enumerable: true, get: function() { return val }, set: function(newVal) { val = newVal; console.log('我的数据发生了改变:'+ val + '->' + newVal
                  
                  dep.notify()// 通知所有订阅者
               
    }
            })
        }
     

    上面的思路整理中我们已经明确订阅者应该是Watcher, 而且let dep = new Dep();是在defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以在 getter里面动手脚:

          Object.defineProperty(obj,key,{
                    ......
                    get: function() {
                //由于需要在闭包内添加watcher,所以通过dep定义一个全局属性target,暂存于watcher,添加完移除 Dep.target
    && dep.addSub(Dep.target) return val }, ....... })

    至此就实现了一个observer,接下来需要实现Compile了

    实现Compile

    compile主要做的事情就是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,就会收到通知更新视图,因为遍历解析的时候有多次操作dom节点,为提高性能和效率,会先将根节点el转换成文档碎片documentFragment进行解析编译操作,待解析完成,在将documentFragment添加到真实的dom节点中。

        function Compile(el,vm) {
                this.$vm = vm;
                this.$el =  document.getElementById(el)
                if(this.$el){
                    this.$fragment = this.nodeToFragment(this.$el);
                    this.compile(this.$fragment);
                    this.$el.appendChild(this.$fragment)
                }
            }
      
    Compile.prototype = {
                isElementNode: function(node) {
                    return node.nodeType == 1
                },
                isTextNode: function(node) {
                    return node.nodeType == 3
                },
                nodeToFragment: function(node) {
                    let fragment = document.createDocumentFragment() , child;
                    while(child = node.firstChild) {
                        fragment.appendChild(child)
                    }
                    return fragment
                },
                compile: function(node) {
                    let childNodes = node.childNodes,
                        _this = this;
                    [].slice.call(childNodes).forEach(function(node) {
                        let value = node.textContent,
                            reg = /{{(.*)}}/;
                        if(_this.isElementNode(node)){
                            _this.compileElement(node)//元素节点
                        }else if(_this.isTextNode(node) && reg.test(value)) {
                            _this.compileText(node,RegExp.$1)//文本节点
                        }
                    })
                },
                compileElement: function(node) {
                    let attrs = node.attributes,
                        value = node.textContent,
                        reg = /{{(.*)}}/,
                          _this = this;
                    if(attrs && attrs.length >0){
                        [].slice.call(attrs).forEach(function(attr) {
                            let attrName = attr.name;
                            if(attrName == 'v-model'){
                                let key = attr.value;
                                node.addEventListener('input',function(e) {
                                    _this.$vm.data[key] = e.target.value;
                                })
                                node.value = _this.$vm.data[key]
                            }else if(attrName == 'v-text'){
                                let key = attr.value
                                // node.textContent  = _this.$vm.data[key]
                                new Watcher(_this.$vm,node,key)
                            }
                            node.removeAttribute(attrName)
                        })
                    }else if(reg.test(value)){
                        let key = RegExp.$1;
                        new Watcher(_this.$vm,node,key)
                    }
                },
                compileText: function (node,key) {
                    new Watcher(this.$vm,node,key)
                }
            }

    实现Watcher

    watcher订阅者作为observer和compile之间通信的桥梁,主要做以下事情:

    在自身实例化时往属性订阅器中dep中添加自己;

    自身有一个update的方法

    待属性自身变动dep.notify通知时,能够调用update方法,并触发compile中的回调

    function Watcher(vm,node,key){
                Dep.target = this;//将当前订阅器指向自己
                this.key = key;
                this.node = node;
                this.vm = vm;
                this.update();//触发get()函数,从而在dep中添加自己:Dep.target && dep.addSub(Dep.target)
                Dep.target = null
            }
            Watcher.prototype = {
                update: function () {
                    this.get();
                    this.node.textContent = this.value;
                },
                get: function() {
                    this.value = this.vm.data[this.key]
                }
            }

    实现Vue

    function Vue(options) {
                this.data = options.data;
                let data = this.data;
                observe(data);
                let id = options.el;
                this.compile = new Compile(id || document.body, this)
            }

    总结

    本文主要是让自己更加理解双向绑定原理。

    observe每个数据的属性,object.defineProperty(),setter、getter函数 ——>在compile的时候为每个元素节点添加订阅者watcher——>添加watcher的过程中触发了update函数,进而调用watcher里面的get函数——>从而触发了访问器里面的get函数,从而触发了订阅器中的addSub函数,就把watcher添加到订阅器中——>从而实现了model->view的实现;

    当在input输入框中输入文本,触发了input的监听事件,把input中的值赋给observe中的对象属性——>从而触发了Object.defineProperty()的set函数,从而消息订阅器发出通知dep.notify()——>从而每个订阅者执行更新函数sub.update(),从而触发watcher的get函数——>获取最新值时又触发了访问器里面的get函数——>从而更新最新值到视图中,实现了view->model

     

     

  • 相关阅读:
    【bzoj2962】序列操作 线段树
    【bzoj1922】[Sdoi2010]大陆争霸 堆优化Dijkstra
    .NET Core / C# 开发 IOT 嵌入式设备的个人见解
    C#中Equals和= =(等于号)的比较)
    VS 2017常用快捷键
    【你不一定知晓的】C#取消异步操作
    工程实践:给函数取一个"好"的名字
    接口测试入门篇
    博客园知名博主 Vamei 英年早逝!
    人生苦短,我用 Python
  • 原文地址:https://www.cnblogs.com/zengfp/p/9598700.html
Copyright © 2011-2022 走看看