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

     

     

  • 相关阅读:
    网站安全编程 黑客入侵 脚本黑客 高级语法入侵 C/C++ C# PHP JSP 编程
    【算法导论】贪心算法,递归算法,动态规划算法总结
    cocoa2dx tiled map添加tile翻转功能
    8月30日上海ORACLE大会演讲PPT下载
    【算法导论】双调欧几里得旅行商问题
    Codeforces Round #501 (Div. 3) B. Obtaining the String (思维,字符串)
    Codeforces Round #498 (Div. 3) D. Two Strings Swaps (思维)
    Educational Codeforces Round 89 (Rated for Div. 2) B. Shuffle (数学,区间)
    洛谷 P1379 八数码难题 (BFS)
    Educational Codeforces Round 89 (Rated for Div. 2) A. Shovels and Swords (贪心)
  • 原文地址:https://www.cnblogs.com/zengfp/p/9598700.html
Copyright © 2011-2022 走看看