Vue 的双向绑定原理

229 阅读5分钟

Vue 的双向绑定原理

概述:Vue是采用数据劫持结合发布者-订阅者模式的方式, 通过ES5(vue2.x)提供的Object.defineProperty()方法来劫持(监视)各个属性的setter, getter,在数据变动时发布消息给订阅者, 触发响应的监听回调. 并且,由于是在不同的数据上触发同步,可以精确的变更发送给绑定数据的视图,而不是对所有的数据都执行一次检测

具体的步骤

  1. 需要observer的数据对象进行递归遍历, 包括子属性对象的属性, 都加上getter和setter,这样的话, 给这个对象的某个值赋值,就会触发setter, 那么就能监听到了数据变化
  2. compile解析模板指令, 将模板中的变量替换成数据,然后初始化渲染页面视图,,并将每个指令对应的节点绑定更新函数, 添加监听数据的订阅者,一旦数据有变动,收到通知, 更新视图
  3. Watcher订阅者是Observer和compile之间通信桥梁, 主要做的事情是:
    • 在自身实例化是往属性订阅器(dep)里面添加自己
    • 自身必须有一个update()的方法
    • 待属性变动 dep.notice()通知的时候,能调用自身的update()方法,并触发cpmpile中绑定的回调
  4. MVVM作为数据绑定的入口, 整合observer、compile和watcher三者,通过observer来监听自己的model的变化,通过compile来编译模板指令,最终利用watcher搭起的observer和compile之间的通信桥梁,达到数据变化--->视图更新。例如:视图交互变化(input)--->数据model变更的双向绑定效果

版本比较

vue是基于依赖收集的双向绑定;

1.基于数据劫持、依赖收集的双向绑定的特点

​ -不需要显示的调用,Vue利用数据劫持+发布订阅,可以直接通知变化并且驱动视图

​ -直接得到精确的变化数据,劫持了属性setter,当属性值改变,我们可以精确的获取变化的内容newValue,不需要额外的diff操作

2.Object.defineProperty缺点

​ -不能监听数组:因为数组没有getter和setter,因为数组长度不确定,太长性能负担太大

​ -只能监听属性,而不是整个对象,需要遍历循环整个属性

​ -只能监听属性变化,不能监听属性的删减

3.proxy 的好处

​ -可以监听数组

​ -监听整个对象而不是属性

​ -返回新对象而不是直接修改原对象,更符合immutable

immutable对象是不可直接赋值的对象,它可以有效的避免错误赋值的问题

​ -功能强大,多种拦截方法

//数组
let arr = [7, 8, 9];
    arr = new Proxy(arr, {
        get(target, prop) {
            return prop in target ? target[prop] : 'error'
        }
    })
    console.log(arr[3]) //error
    console.log(arr[1]) //8
//apply 拦截函数的调用,call 和 apply 操作
let sum = function(...args) {
    let count = 0;
    args.forEach((item) => {
        count += item
    })
    return count;
};

let p = new Proxy(sum, {
    apply(target, ctx, args) {
        return target(...args) * 2
    }
})
console.log(p(1, 2, 3, 4)) //20
console.log(p.call(null, 1, 2, 3, 4)) //20
console.log(p.apply(null, [1, 2, 3, 4])) //20

4.proxy缺点

​ -兼容性不好,无法用polyfill磨平

polyfill 在英文中有垫片的意思,意为兜底的东西。在计算机科学中,指的是对未能实现的客户端上进行的"兜底"操作。

详细解释

Observer

function defineReactive(data, key, val) {
    observe(val)
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: () => {
            return val;
        },
        set: (newVal) => {
            val = newVal;
            console.log('属性' + key + '已经被监听,现在值为:'+ newVal.toString())
        }
    })
}
function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    Object.keys(data).forEach(key => {
        defineReactive(data,key,data[key])
    })
}

let library = {
  book1: {
    name: "",
  },
  book2: {
    name: "",
  },
};

observe(library);
library.book1.name = "编译原理";//属性name已经被监听,现在值为:编译原理
library.book2.name = "高中数学";//属性name已经被监听,现在值为:高中数学 
library.book1.name = "编译原理2";//属性name已经被监听,现在值为:编译原理2
//待续

需要创建一个可以容纳订阅者的消息订阅器Dep,订阅器Dep主要负责收集订阅者,然后在属性变化的时候执行对应订阅者的更新函数。所以订阅器需要一个容器。

function Dep() {
    this.subs = []
}
Dep.prototype = {
    addSub: (sub) => {
        this.subs.push(sub)
    },
    notify: () => {
        this.subs.forEach(sub => {
            sub.update()
        })
    }
}

Watcher

get 函数时执行添加订阅者操作(初始化时),set时执行notify

   function Watcher(vm, exp, cb) {
      this.cb = cb;
      this.vm = vm;
      this.exp = exp;
      this.value = this.get();
    }

    Watcher.prototype = {
      update: function () {
        this.run();
      },
      run: function () {
        const value = this.vm.data[this.exp];
        const oldVal = this.value;
        if (value !== oldVal) {
          this.value = value;
          this.cb.call(this.vm, value, oldVal);
        }
      },
      get: function () {
        Dep.target = this;
        const value = this.vm.data[this.exp];
        Dep.target = null;
        return value;
      },
    };

测试代码:过2s界面上会显示无名二字

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1 id="name">{{name}}</h1>
  </body>
  <script>
    function Dep() {
      this.subs = [];
    }
    Dep.prototype = {
      addSub: function (sub) {
        this.subs.push(sub);
      },
      notify: function () {
        this.subs.forEach((sub) => {
          sub.update();
        });
      },
    };
    
    function Watcher(vm, exp, cb) {
      this.cb = cb;
      this.vm = vm;
      this.exp = exp;
      this.value = this.get();
    }

    Watcher.prototype = {
      update: function () {
        this.run();
      },
      run: function () {
        const value = this.vm.data[this.exp];
        const oldVal = this.value;
        if (value !== oldVal) {
          this.value = value;
          this.cb.call(this.vm, value, oldVal);
        }
      },
      get: function () {
        Dep.target = this;
        const value = this.vm.data[this.exp];
        Dep.target = null;
        return value;
      },
    };
    function defineReactive(data, key, val) {
      observe(val);
      const dep = new Dep();
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: () => {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return val;
        },
        set: (newVal) => {
          if (val === newVal) {
            return;
          }
          val = newVal;
          console.log(
            "属性" + key + "已经被监听,现在值为:" + newVal.toString()
          );
          dep.notify();
        },
      });
    }
    function observe(data) {
      if (!data || typeof data !== "object") {
        return;
      }
      Object.keys(data).forEach((key) => {
        defineReactive(data, key, data[key]);
      });
    }
  </script>
  <script>
    function Vue1(data, el, exp) {
      const self = this;
      this.data = data;
      Object.keys(data).forEach(key=>{
          self.proxyKeys(key)
      })
      observe(data);
      el.innerHTML = this.data[exp];
      new Watcher(this, exp, (value) => {
        el.innerHTML = value;
      });
      return this;
    }
    Vue1.prototype = {
        proxyKeys:function(key){
            const self = this 
            Object.defineProperty(this,key,{
                enumerable:false,
                configurable:true,
                get:()=>{
                    return self.data[key]
                },
                set:(newVal)=>{
                    self.data[key] = newVal
                }
            })
        }
    }
    const ele = document.querySelector("#name");
    const vue1 = new Vue1(
      {
        name: "hellow world",
      },
      ele,
      "name"
    );
    window.setTimeout(() => {
      console.log("name值改变了");
      vue1.name = "无名";
    }, 2000);
  </script>
</html>

Compile

实现解析器去解析dom节点

  1. 解析模板指令,并替换模板数据,初始化视图
  2. 将模板指令对应的及诶按绑定对应的更新函数,初始化响应的订阅器
    function Compile(el, vm) {
        this.vm = vm;
        this.el = document.querySelector(el);
        this.fragment = null;
        this.init();
    }

    Compile.prototype = {
        init: function () {
            this.fragment = this.nodeToFragment(this.el);
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
        },
        nodeToFragment: function (el) {
            const fragment = document.createDocumentFragment();
            let child = el.firstChild;
            while (child) {
                //将dom元素移入fragment中
                fragment.appendChild(child);
                child = el.firstChild;
            }
            return fragment;
        },
        compileElement: function (el) {
            const childNodes = el.childNodes;
            const self = this;
            [].slice.call(childNodes).forEach(function (node) {
                const reg = /\{\{(.*)\}\}/;
                const text = node.textContent;
                if (self.isElementNode(node)) {
                    self.compile(node);
                } else if (self.isTextNode(node) && reg.test(text)) {
                    self.compileText(node, reg.exec(text)[1]);
                }
                if (node.childNodes && node.childNodes.length) {
                    self.compileElement(node);
                }
            })
        },
        compile: function (node) {
            const nodeAttrs = node.attributes;
            const self = this;
            if(nodeAttrs){
                Array.prototype.forEach.call(nodeAttrs, function (attr) {
                    const attrName = attr.name;
                    if (self.isDirective(attrName)) {
                        const exp = attr.value;
                        const dir = attrName.substring(2);
                          
                        if (self.isEventDirective(dir)) {//事件指令
                        
                            self.compileEvent(node, self.vm, exp, dir);
                        } else {//v-model指令
                            self.compileModel(node, self.vm, exp, dir);
                        }
                        node.removeAttribute(attrName);
                    }
                })
            }
    
        },
        compileEvent: function (node, vm, exp, dir) {
            const eventType = dir.split(':')[1];
            const cb = vm.methods && vm.methods[exp];
            if (eventType && cb) {
                node.addEventListener(eventType, cb.bind(vm), false);
            }
        },
        compileModel: function (node, vm, exp, dir) {
            const self = this;
            const val = this.vm[exp];
            this.modelUpdater(node, val);
            new Watcher(this.vm, exp, function (value) {
                self.modelUpdater(node, value);
            })
            node.addEventListener('input', function (e) {
                const newValue = e.target.value;
                if (val === newValue) {
                    return;
                }
                self.vm[exp] = newValue;
                val = newValue;
            })
        },
    compileText: function (node, exp) {
            const self = this;
            const initText = this.vm[exp];
            this.updateText(node, initText);
            new Watcher(this.vm, exp, function (value) {
                self.updateText(node, value);
            })

        },
        modelUpdater: function (node, value, oldValue) {
            node.value = typeof value == 'undefined' ? '' : value;
        },
    updateText: function (node, value) {
            node.textContent = typeof value == 'undefined' ? '' : value;
        },
        isDirective: function (attr) {
            return attr.indexOf('v-') == 0;
        },
        isElementNode: function (node) {
            return node.nodeType == 1;
        },
        isTextNode: function (node) {
            return node.nodeType == 3;
        },
        isEventDirective: function (dir) {
            return dir.indexOf('on:') === 0;
        }
    }

测试

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
<div id="app">
    <h2>{{title}}</h2>
    <input v-model="name">
    <h1>{{name}}</h1>
    <button v-on:click="clickMe">click me!</buttuon>
</div>
  </body>
  <script>
    function Dep() {
      this.subs = [];
    }
    Dep.prototype = {
      addSub: function (sub) {
        this.subs.push(sub);
      },
      notify: function () {
        this.subs.forEach((sub) => {
          sub.update();
        });
      },
    };
    
    function Watcher(vm, exp, cb) {
      this.cb = cb;
      this.vm = vm;
      this.exp = exp;
      this.value = this.get();
    }

    Watcher.prototype = {
      update: function () {
        this.run();
      },
      run: function () {
        const value = this.vm.data[this.exp];
        const oldVal = this.value;
        if (value !== oldVal) {
          this.value = value;
          this.cb.call(this.vm, value, oldVal);
        }
      },
      get: function () {
        Dep.target = this;
        const value = this.vm.data[this.exp];
        Dep.target = null;
        return value;
      },
    };
    function defineReactive(data, key, val) {
      observe(val);
      const dep = new Dep();
      Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: () => {
          if (Dep.target) {
            dep.addSub(Dep.target);
          }
          return val;
        },
        set: (newVal) => {
          if (val === newVal) {
            return;
          }
          val = newVal;
          console.log(
            "属性" + key + "已经被监听,现在值为:" + newVal.toString()
          );
          dep.notify();
        },
      });
    }
    function observe(data) {
      if (!data || typeof data !== "object") {
        return;
      }
      Object.keys(data).forEach((key) => {
        defineReactive(data, key, data[key]);
      });
    }
  </script>
  <script>
    function Compile(el, vm) {
        this.vm = vm;
        this.el = document.querySelector(el);
        this.fragment = null;
        this.init();
    }

    Compile.prototype = {
        init: function () {
            this.fragment = this.nodeToFragment(this.el);
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
        },
        nodeToFragment: function (el) {
            const fragment = document.createDocumentFragment();
            let child = el.firstChild;
            while (child) {
                //将dom元素移入fragment中
                fragment.appendChild(child);
                child = el.firstChild;
            }
            return fragment;
        },
        compileElement: function (el) {
            const childNodes = el.childNodes;
            const self = this;
            [].slice.call(childNodes).forEach(function (node) {
                const reg = /\{\{(.*)\}\}/;
                const text = node.textContent;
                if (self.isElementNode(node)) {
                    self.compile(node);
                } else if (self.isTextNode(node) && reg.test(text)) {
                    self.compileText(node, reg.exec(text)[1]);
                }
                if (node.childNodes && node.childNodes.length) {
                    self.compileElement(node);
                }
            })
        },
        compile: function (node) {
            const nodeAttrs = node.attributes;
            const self = this;
            if(nodeAttrs){
                Array.prototype.forEach.call(nodeAttrs, function (attr) {
                    const attrName = attr.name;
                    if (self.isDirective(attrName)) {
                        const exp = attr.value;
                        const dir = attrName.substring(2);
                          
                        if (self.isEventDirective(dir)) {//事件指令
                        
                            self.compileEvent(node, self.vm, exp, dir);
                        } else {//v-model指令
                            self.compileModel(node, self.vm, exp, dir);
                        }
                        node.removeAttribute(attrName);
                    }
                })
            }
    
        },
        compileEvent: function (node, vm, exp, dir) {
            const eventType = dir.split(':')[1];
            const cb = vm.methods && vm.methods[exp];
            if (eventType && cb) {
                node.addEventListener(eventType, cb.bind(vm), false);
            }
        },
        compileModel: function (node, vm, exp, dir) {
            const self = this;
            const val = this.vm[exp];
            this.modelUpdater(node, val);
            new Watcher(this.vm, exp, function (value) {
                self.modelUpdater(node, value);
            })
            node.addEventListener('input', function (e) {
                const newValue = e.target.value;
                if (val === newValue) {
                    return;
                }
                self.vm[exp] = newValue;
                val = newValue;
            })
        },
    compileText: function (node, exp) {
            const self = this;
            const initText = this.vm[exp];
            this.updateText(node, initText);
            new Watcher(this.vm, exp, function (value) {
                self.updateText(node, value);
            })

        },
        modelUpdater: function (node, value, oldValue) {
            node.value = typeof value == 'undefined' ? '' : value;
        },
    updateText: function (node, value) {
            node.textContent = typeof value == 'undefined' ? '' : value;
        },
        isDirective: function (attr) {
            return attr.indexOf('v-') == 0;
        },
        isElementNode: function (node) {
            return node.nodeType == 1;
        },
        isTextNode: function (node) {
            return node.nodeType == 3;
        },
        isEventDirective: function (dir) {
            return dir.indexOf('on:') === 0;
        }
    }
  </script>
  <script>
    function Vue1(options) {
        const self = this;
        this.data = options.data;
        this.methods = options.methods;
        Object.keys(this.data).forEach(function (key) {
            self.proxyKeys(key);
        });
        observe(this.data);
        new Compile(options.el, this);
        // options.mounted.call(this);//所有事情处理好后执行mounted函数
    }
    Vue1.prototype = {
        proxyKeys:function(key){
            const self = this 
            Object.defineProperty(this,key,{
                enumerable:false,
                configurable:true,
                get:()=>{
                    return self.data[key]
                },
                set:(newVal)=>{
                    self.data[key] = newVal
                }
            })
        }
    }

    const vue1 =  new Vue1({
            el: '#app',
            data: {
                title: 'hello world',
                name: '123'
            },
            methods: {
                clickMe: function () {
                    this.title = 'hello world2';
                },
                modifyName:function(){
                    this.name='modify'
                }
            },
            mounted(){
               this.name = 'modify'
            }
        })

  </script>
</html>