浅析Vue2双向事件绑定

135 阅读2分钟

简单说法

vue2中的双向绑定一般是使用v-model/.sync实现的,v-modelv-bind:valuev-on:input的语法糖。

  • v-bind:value实现了data向UI的单项绑定
  • v-on:input实现了UI向data的单项绑定
  • 二者结合就是双向绑定了

这两个单项绑定的原理是:

  • 前者通过Object.defineProperty API给data创建getter和setter,用于监听data的改变,data一变就会安排改变UI
  • 后者通过编译器给DOM事件添加事件监听,DOM的input值变了就回去修改data

简单分析

1-1.png 这张图片显示了vue2双向绑定的核心概念,按自己的话说,就是创建一个Vue实例时,会使用Observer来劫持监听所有的属性,其次会用Compile来编译dom节点,在Observer中会使用Dep来对每一个key管理data,在Compile中会初始化视图,如果UI发生变化,Compile会通知Watcher来执行Dep中的更新函数来更新data,并且同时更新 dom视图。
借用代码来更好的说明这一点:

1.创建Vue类和Observer类

class Vue {  
  constructor(options) {  
    this.$options = options;  
    this.$data = options.data;  
        
    // 对data选项做响应式处理  
    observe(this.$data);  
        
    // 代理data到vm上  
    proxy(this);  
        
    // 执行编译  
    new Compile(options.el, this);  
  }  
}  

function observe(obj) {  
  if (typeof obj !== "object" || obj == null) {  
    return;  
  }  
  new Observer(obj);  
}  
  
class Observer {  
  constructor(value) {  
    this.value = value;  
    this.walk(value);  
  }  
  walk(obj) {  
    Object.keys(obj).forEach((key) => {  
      defineReactive(obj, key, obj[key]);  
    });  
  }  
}  

这里创建了一个Vue的类,并且将data绑定在observe上,并且将挂载的dom节点和实例本身传递给Compile执行编译。
我们可以看到在observe上,会对每一个节点执行一个函数defineReactive,这个函数能帮助Observer来管理data。

编译Compile

class Compile {  
  constructor(el, vm) {  
    this.$vm = vm;  
    this.$el = document.querySelector(el);  // 获取dom  
    if (this.$el) {  
      this.compile(this.$el);  
    }  
  }  
  compile(el) {  
    const childNodes = el.childNodes;   
    Array.from(childNodes).forEach((node) => { // 遍历子元素  
      if (this.isElement(node)) {   // 判断是否为节点  
        console.log("编译元素" + node.nodeName);  
      } else if (this.isInterpolation(node)) {  
        console.log("编译插值⽂本" + node.textContent);  // 判断是否为插值文本 {{}}  
      }  
      if (node.childNodes && node.childNodes.length > 0) {  // 判断是否有子元素  
        this.compile(node);  // 对子元素进行递归遍历  
      }  
    });  
  }  
  isElement(node) {  
    return node.nodeType == 1;  
  }  
  isInterpolation(node) {  
    return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);  
  }  
}  

编译阶段会分析dom节点,会根据dom上绑定的指令绑定相应的更新函数。

收集依赖

这个阶段会使用Dep来管理数据,Observer会对每一个key创建一个Dep,在Dep中有两个方法,一个用来添加新的数据,一个用于更新数据

// 负责更新视图  
class Watcher {  
  constructor(vm, key, updater) {  
    this.vm = vm  
    this.key = key  
    this.updaterFn = updater  
  
    // 创建实例时,把当前实例指定到Dep.target静态属性上  
    Dep.target = this  
    // 读一下key,触发get  
    vm[key]  
    // 置空  
    Dep.target = null  
  }  
  
  // 未来执行dom更新函数,由dep调用的  
  update() {  
    this.updaterFn.call(this.vm, this.vm[this.key])  
  }  
}  

class Dep {  
  constructor() {  
    this.deps = [];  // 依赖管理  
  }  
  addDep(dep) {  
    this.deps.push(dep);  
  }  
  notify() {   
    this.deps.forEach((dep) => dep.update());  
  }  
}  

function defineReactive(obj, key, val) {  
  this.observe(val);  
  const dep = new Dep();  
  Object.defineProperty(obj, key, {  
    get() {  
      Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例  
      return val;  
    },  
    set(newVal) {  
      if (newVal === val) return;  
      dep.notify(); // 通知dep执行更新方法  
    },  
  });  
}  

当data数据改变,Watcher会立即更新Dep中的数据,并且更新视图。
本文参考:点击这里,代码也是这里的。