Vue源码解析:双向绑定原理

栏目: 编程语言 · 发布时间: 6年前

内容简介:在说双向绑定之前,我们先聊聊这是单向数据流的极简示意,即状态(数据源)映射到视图,视图的变化(用户输入)触发行为,行为改变状态。但在实际的开发中,大部分的情况是多个视图依赖同一状态,多个行为影响同一状态,Vuex的处理是将共同状态提取出来,转化成单向数据流实现。另外,在Vue的父子组件中

通过对 Vue2.0 源码阅读,想写一写自己的理解,能力有限故从 尤大佬2016.4.11第一次提交 开始读,准备陆续写:

其中包含自己的理解和源码的分析,尽量通俗易懂!由于是2.0的最早提交,所以和最新版本有很多差异、bug,后续将陆续补充,敬请谅解! 包含中文注释的Vue源码 已上传...

开始

在说双向绑定之前,我们先聊聊 单向数据流 的概念,引用一下Vuex官网的一张图:

Vue源码解析:双向绑定原理

这是单向数据流的极简示意,即状态(数据源)映射到视图,视图的变化(用户输入)触发行为,行为改变状态。但在实际的开发中,大部分的情况是多个视图依赖同一状态,多个行为影响同一状态,Vuex的处理是将共同状态提取出来,转化成单向数据流实现。另外,在Vue的父子组件中 prop传值 中,也有用到单向数据流的概念,即 父级 prop 的更新会向下流动到子组件中,但是反过来则不行。

无论是react还是vue都提倡单向数据流管理状态,那我们今天要谈的 双向绑定 是否和 单向数据流 理念有所违背?我觉得不是,从上篇文章 AST语法树转render函数 了解到,Vue双向绑定,实质是 value 的单向绑定和 oninput/onchange 事件侦听的语法糖 。这种机制在某些需要实时反馈用户输入的场合十分方便,这只是Vue内部对 action 进行了封装而形成的。

所以我们今天要说是,状态的变化怎么引起视图的变化?

  • 第一个难点是如何监听状态的变化。Vue2.0主要是采用 defineProperty ,但它有个缺点是不能检测到对象和数组的变化。尤大佬说3.0将采用 proxy ,不过兼容仍是问题,有兴趣的同学可以去了解下;
  • 另外一个难点就是状态变化后如何触发视图的变化。Vue2.0采用的发布/订阅模式,即每个状态都会有自己的一个订阅中心,订阅中心放着一个个订阅者,订阅者身上有关于dom的更新函数。当状态改变时会发布消息:我变了!订阅中心会挨个告诉订阅者,订阅者知道了就去执行自己的更新函数。

源码解析

今天涉及到的代码全在 observer 文件夹下。流程大致如下:

function Vue (options) {
    // ...
    var data = options.data;
    data = typeof data === 'function' ? data() : data || {};
    observe(data, this);
    Watcher(this, this.render, this._update);
    // ...
}

先对 data 进行数据劫持(observe),然后为当前实例创建一个订阅者(Watcher)。具体如何实现,下面将逐一阐述。

数据劫持

数据劫持的实质就是使用 defineProperty 重写对象属性的 getter/setter 方法。但由于 defineProperty 无法监测到对象和数组内部的变化 ,所以遇到子属性为对象时,会 递归观察 该属性直至简单数据类型;为数组时的处理是重写 pushpopshift 等方法,方法内部通知订阅中心:状态变化了!这样就能对所有类型数据进行监听了。

我们先看看入口函数 observe()

function observe (value, vm) {
  // 若检测数据不是对象,则退出
  if (typeof value !== 'object') return;
  var ob;
  if (value.__ob__ && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else {
    ob = new Observer(value);
  }
  return ob;
}

observe() 方法尝试为 value 创建观察者实例,观察成功则返回新的观察者或已有的观察者。 __ob__ 属性下面将提到,即对象被观察过后会有 __ob__ 属性,用于存储观察者实例。再来看看 Observer 类:

function Observer (value) {
  this.value = value;
  // 给value对象通过defineProperty追加__ob__属性
  def(value, '__ob__', this); 
  // 特殊处理数组
  if (Array.isArray(value)) {
    value.__proto__ = arrayMethods;
    value.forEach(item => {
      observe(item);
    })
  } else {
    this.walk(value);
  }
}

很明显看到, Observer 类除开属性的定义,就是对数组的特殊处理了。处理的方法是通过原型链去修改数组的 pushpopshift ...等等方法,当然,还需要对数组的每个元素进行observe(),因为数组元素也可能是对象,我们要继续劫持,直到基本类型!我们先来看下 arrayMethods 具体是怎么修改的这些方法:

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);

['push','pop','shift','unshift','splice','sort','reverse']
.forEach(method => {
  // 拿到对应的原生方法
  var original = arrayProto[method];
  def(arrayMethods, method, () => {
    // 参数处理
    var i = arguments.length;
    var args = new Array(i);
    while (i--) {
      args[i] = arguments[i];
    }
    // 运行原生方法
    var result = original.apply(this, args);
    var ob = this.__ob__;
    // 特殊处理数组插入方法
    var inserted;
    switch (method) {
      case 'push':
        inserted = args;
        break;
      case 'unshift':
        inserted = args;
        break;
      case 'splice':
        inserted = args.slice(2);
        break;
    }
    // 对插入的参数进行数据劫持
    if (inserted) ob.observeArray(inserted);
    // 发布改变通知
    ob.dep.notify();
    return result;
  })
})

能看出 arrayMethods 的构造其实也很简单,首先是根据数组的 prototype 创建一个新对象,然后对数组方法进行逐个重写。方法重写的重点在于:

dep.notify()

到这, defineProperty 无法监听数组内部变化的问题解决了,当然,你通过数组下标修改内部数据还是察觉不到的!

我们继续来看, walk() 函数:

Observer.prototype.walk = function (obj) {
  var keys = Object.keys(obj);
  for (var i = 0, l = keys.length; i < l; i++) {
    this.convert(keys[i], obj[keys[i]]);
  }
}
Observer.prototype.convert = function (key, val) {
  defineReactive(this.value, key, val);
}

walk() 意思就是遍历对象的每个属性,并侵占( convert )它们的 getter/setter ,接下来就是整个数据劫持的重点函数 defineReactive() :

function defineReactive (obj, key, val) {
  var dep = new Dep();

  // 获取对象的对象描述
  var property = Object.getOwnPropertyDescriptor(obj, key);
  // 是否可配置
  if (property && property.configurable === false) return;
  // 获取原来的get、set
  var getter = property && property.get;
  var setter = property && property.set;

  // 递归:继续监听该属性值(只有val为对象时才有childOb)
  var childOb = observe(val);

  Object.defineProperty(obj, key, {
    enumerable: true,    // 可枚举
    configurable: true,    // 可配置
    get: ...,
    set: ...
  })
}

以上为 defineReactive() 函数的内部结构,先定义了依赖中心 Dep ,再获取对象的原生get/set方法,然后递归监听该属性,因为当前属性可能也是对象,最后通过 defineProperty 劫持 getter/setter 函数,依次看一下 get/set :

get: function reactiveGetter () {
  // 计算value
  var value = getter ? getter.call(obj) : val
  if (Dep.target) {
    // 添加依赖
    dep.depend();
    // 如果有子观察者,也给它添加依赖
    if (childOb) {
      childOb.dep.depend();
    }
    // 如果该属性是数组,查看每项是否含观察者对象,有则添加依赖
    if (isArray(value)) {
      for (var e, i = 0, l = value.length; i < l; i++) {
        e = value[i];
        e && e.__ob__ && e.__ob__.dep.depend();
      }
    }
  }
  return value;
}

大家看完这个函数,除开 if 语句,其他的都是 get 的基本逻辑。至于 Dep.target 的含义,我的理解是 它就像一个开关,当开关在打开的状态下访问该属性,则会被添加到订阅中心 。至于什么时候开关打开、关闭,以及把谁添加到订阅中心,先留下疑问。继续看下 set

set: function reactiveSetter (newVal) {
  // 计算value
  var value = getter ? getter.call(obj) : val;
  // 新旧值是否相等
  if (newVal === value) return;
  // 不相等,设置新值
  if (setter) {
    setter.call(obj, newVal);
  } else {
    val = newVal;
  }
  // 劫持新值
  childOb = observe(newVal);
  // 发送变更通知
  dep.notify();
}

set 也比较好理解,先是新旧值的比较,若不相等,则需要: 设置新值,劫持新值,发布通知

到这,数据劫持就完成了。总之, observe 对数据对象进行了递归遍历,递归包括数组和子对象,将每个属性的 getter/setter 进行了改造,使得 在特殊情况下 获取值( xxx.name )会添加到订阅中心,在设置值( xxx.name = 'Tom' )会触发订阅中心的 通知事件

订阅中心

订阅中心也就是前面提到的 Dep ,它要做的事情很简单,维护一个容器(数组) 存储订阅者 ,也就是说它有 添加订阅者 功能和 发布通知 功能。简单看一下:

let uid = 0;
function Dep () {
  this.id = uid++;
  this.subs = [];
}
// 添加订阅者
Dep.prototype.addSub = function (sub) {
  this.subs.push(sub);
}
// 将自己作为依赖传给目标订阅者
Dep.prototype.depend = function () {
  Dep.target.addDep(this);
}
// 通知所有订阅者
Dep.prototype.notify = function () {
  var subs = this.subs.slice();
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
}
Dep.target = null;

数据劫持中提到,当 Dep.target 存在时调用 get ,会触发 dep.depend() 添加订阅者,那么这个 Dep.target.addDep() 方法里肯定含添加订阅者 addSub() 方法。

注意 Dep.target 的默认值为 null

订阅者

订阅者也就是前面提到的 Watcher ,因为它也用于 $watch() 接口,所以这边对其简化分析。

Watcher 接收3个参数, vm :Vue实例对象, fn :渲染函数, cb :更新函数。先看看 Watcher 对象:

function Watcher (vm, fn, cb) {
  this.vm = vm;
  this.fn = fn;
  this.cb = cb;
  this.depIds = new Set();

  this.value = this.get();
}

// 向当前watcher添加依赖项
Watcher.prototype.addDep = function (dep) {
  var id = dep.id;
  // 防止重复向订阅中心添加订阅者
  if (!this.depIds.has(id)) {
    this.depIds.add(id);
    dep.addSub(this);
  }
}

WatcheraddDep() 方法内为了防止重复添加订阅者到订阅中心,故维护了一个 Set 用于存储订阅中心( Dep )的id,每次添加前看是否已存在。

Watcher 在初始化时,执行了 get() 函数,看看方法内部:

Watcher.prototype.get = function () {
  // 打开开关,指向自身(Watcher)
  Dep.target = this;
  // 指向渲染函数,会触发getter
  var value = this.fn.call(this.vm);
  // 关闭开关
  Dep.target = null;
  return value;
}

之前一直不理解这边为什么会将订阅者推入各个订阅中心,后来才发现巧妙的地方: Dep.target 指向当前 Watcher (打开开关),然后执行渲染函数,渲染函数用到的数据都会触发其 get ,这样就把当前 Watcher 加入到这些数据的订阅中心了!然后 Dep.target = null (开关关闭)。

另外还有一个就是 update 函数,也就是数据的 set 被触发是,其订阅中心会发布通知( notify() ),而 notify() 方法的本质就是依次执行订阅者的 update() 方法。让我们看一下:

Watcher.prototype.update = function () {
  var value = this.get();
  if (value !== this.value) {
    var oldValue = this.value;
    this.value = value;
    this.cb.call(this.vm, value, oldValue);
  }
}

update() 方法其实就是拿新值和旧值比较,如果不一样就把它们作为参数,执行更新回调函数。

到这,关于订阅者部分的已经说完了。再回看到前面的调用 Watcher(this, this.render, this._update); ,这边的渲染函数也就是前篇文章讲的 render 函数,而 _update 函数是用于比较 vdom 并更新的函数,这是下一篇文章要说的内容。

总结

最后再来理一遍, observe 递归遍历整个 data ,给每个属性创建一个订阅中心,而且重写他们的 getter/setter 方法:在特殊情况( Dep.target 存在)下 get 会添加订阅者到订阅中心,在 set 时会通知订阅中心,继而通知每位订阅者;订阅者会特殊情况( Dep.target 存在)下,执行 render 函数, get 每一个涉及到的数据。这样,以后只要有数据发生变动,就会触发该订阅者的更新函数,就会引起 dom 的变化!

最近工作比较忙,博客写的比较慢,可能也会有各种问题(┬_┬)...

溜了溜了


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Developing Large Web Applications

Developing Large Web Applications

Kyle Loudon / Yahoo Press / 2010-3-15 / USD 34.99

As web applications grow, so do the challenges. These applications need to live up to demanding performance requirements, and be reliable around the clock every day of the year. And they need to withs......一起来看看 《Developing Large Web Applications》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

MD5 加密
MD5 加密

MD5 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具