• Vue双向绑定原理详解


    前言:Vue最核心的功能之一就是响应式的数据绑定模式,即view与model任意一方改变都会同步到另一方,而不需要手动进行DOM操作,本文主要探究此功能背后的原理。

    思路分析

    以下是一个最简单的双向绑定的例子:

    <body>
      <div id="app">
        <input type="text" v-model="msg">
        <p>{{msg}}</p>
      </div>
    </body>
    <script src="https://unpkg.com/vue@2.3.4/dist/vue.js"></script>
    <script>
    var  vm = new Vue({
      el:"#app",
      data:{
        msg:"msg"
      }
    })
    </script>

    观察以上代码,我们可以看出双向绑定的涉及到的3个元素:input,data.msg和{{msg}}。双向绑定实现的就是:

    1)改变input时,data.msg和{{msg}}都会跟着变化;

    (监听input,使其发生变化时改变data.msg,实现了2,{{msg}}也就跟着改变了)

    2)改变data.msg时,input和{{msg}}都会变化;

    (监听data.msg,使其发生变化时改变input和{{msg}})

    第1件事容易实现,只需要找到相应的input,为其添加监听即可,但第2件事有两个关键点:一是如何监听到data.msg的变化;而是如何把data.msg的变化告知所有的{{msg}}。

    下面我们一个个来解决如上问题。

    实现数据监听observer

    JavaScript中有一个特别的方法Object.defineProperty()可以监听到数据的变化,并执行相应的操作。不熟悉此方法的可以参Object.defineProperty()用法

    我们对数据的属性遍历,逐个添加getter和setter

    function observe(data) {
      if (!data || typeof data !== 'object') {
        return;
      }
      // 取出所有属性遍历
      Object.keys(data).forEach(function(key) {
          defineReactive(data, key, data[key]);
        });
    };
     
    function defineReactive(data, key, val) {
     observe(val); // 监听子属性
     Object.defineProperty(data, key, {
      get: function() {
        return val;
      },
      set: function(newVal) {
        if (val === newVal) return;
        val = newVal;
        console.log("监听到变化了!");
      }
     });
    }
    var data = {msg: 'hello'};
    observe(data);
    data.msg="hello world";//监听到变化了

    通过遍历对data的每个属性添加getter和setter,当数据发生改变时,就会监听到变化并执行相应的操作了。变化监听到了,如何告诉{{msg}}和input呢?

    订阅/发布模式(subscribe&publish)

    Vue采用发布/订阅的方式来实现信息(此处即数据的变化)的传达:为每个{{msg}}添加一个订阅data.msg的订阅器,当数据data.msg发生改变时,发布这个变化通知,{{msg}}就会收到这个变化通知,从而自身做出相应的变化。

    首先我们得告诉data.msg,它对应了哪些节点,即有哪些节点订阅了msg,然后再将这个变化传播给对应的订阅者

    image

    通过一个数组dep保存data.msg对应的订阅者,当data.msg发生改变调用setter时,发布这个变化通知。那dep数组中的订阅者是如何加入进来的呢?

    我们可以在属性的getter中实现订阅者的加入,这样只要在新建订阅者时触发getter就可以将订阅者加入对应的属性中了。

    image

    Watcher为订阅者类,Dep.target为全局属性,每次new Watcher()时,Dep.target指向当前订阅者,并通过update出发data.msg的getter,从而实现将订阅者将入到data.msg的dep中。

    有了以上步骤,接下来我们就只需要遍历节点,对指定的节点新建订阅者Watcher,就可以接收到来自属性变化发布的通知了。

    实现complie

    compile主要做的事情是解析模板指令,将模板中的变量替换成数据,并为节点添加订阅者,一旦数据变化,可以接收到通知。

    image

    到这里,input和{{msg}}就能接收到data.msg的变化了,即实现了model->view的同步变化。此时我们再为input添加上监听,使其改变时将新值赋给data.msg从而触发data.msg的setter,就实现了view->model的同步变化了。

    image

    因为遍历解析的过程有多次操作dom节点,为了提高性能,Vue采用DocumentFragment,将挂载目标的所有子节点劫持(通过 append 方法,DOM 中的节点会被自动删除)到 DocumentFragment 中,经过一番处理后,再将 DocumentFragment 整体返回插入挂载目标。

    function nodeToFragment(node,vm){
      var fragment = document.createDocumentFragment();
      var child;
      while(child = node.firstChild){
        complie(child,vm);//解析节点
        fragment.append(child);//将节点劫持到fragment
      }
      return fragment ;
    }
    function Vue(options){
      this.data = options.data;
      observe(this.data,this);
      var dom =nodeToFragment(document.getElementById(options.el),this);
      document.getElementById(options.el).appendChild(dom);
    }

    实现

    最后,我们新建一个实例,将以上代码放入index.js中:

    <body>
      <div id="app">
        <input type="text" v-model="msg">
        <p>{{msg}}</p>
      </div>
    </body>
    <script src="index.js"></script>
    <script>
    var vm = new Vue({
      el:"app",
      data:{
        msg:"哈哈"
      }
    })
    </script>

    打开浏览器,即可发现无论是改变input还是在控制台改变data.msg,input、{{msg}}和data.msg这三者均为同步变化。

    小结

    根据以上的思路,可以总结如下:

    1)通过Object.defineproperty()中setter实现对数据的监听,并在数据改变后项所有订阅者发布这个变化通知;

    2)通过订阅/发布的方式实现信息(变化)的传达,每new一个订阅者即触发在data.msg的getter,从而实现将当前订阅者添加进data.msg的订阅者数组中;

    3)通过遍历节点,为data.msg对应的input和所有{{msg}}添加订阅者,从而可以收到来自data.msg发布变化通知;同时为input添加监听,实现data.msg跟随input改变;

    4)为了提高性能,采用数据劫持的方式,将挂载目标劫持(不是复制,是截取)出来,实现完所有操作后再返回挂载目标。

    为了方便理解,文中代码部分采用图片形式,完整代码见源码

  • 相关阅读:
    a gcc 4.2.4 bug(被stos指令累加后%edi作为参数的)
    gcc -02引起内存溢出'unsigned i'应修订为'volatile unsigned i'
    gcc优化引起get_free_page比__get_free_page返回值多4096
    gcc请不要优化
    change_bit 按位取反
    IBM messed up *AGAIN* in their thinkpad: 0xA0000 -> 0x9F000
    python正则实例
    详解volatile 关键字与内存可见性
    并发基础知识
    Spring通过注释配置Bean2 关联关系
  • 原文地址:https://www.cnblogs.com/youhong/p/7188449.html
Copyright © 2020-2023  润新知