Vue学习(三)之数据双向绑定源码解析


基于vue2.0
基础知识:TypeScript、ES6、ES5

目录结构

我们将通过分析,简化源码概览数据绑定过程
基本目录结构

  • observer中实现数据双向绑定过程

Observer类

我们从Observer类的入口开始分析整个数据绑定过程;在源码中定义了这个类的作用:每个观察的值/对象都绑定了一个Observer类,一旦绑定,这个类能将目标对象的属性键值转换为进行依赖收集和调度更新的getter/setter;忽略处理数组和隐藏属性的操作,一个简化的Observer类如下:

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    this.walk(value)
  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}
  • observe():为值创建一个observer类实例

    function observe (value: any, asRootData: ?boolean): Observer | void {
    if (!isObject(value) || value instanceof VNode) {
      return
    }
    let ob = new Observer(value)
    }
    if (asRootData && ob) {
      ob.vmCount++
    }
    return ob
    }
    
  • defineReactive():为对象定义一个响应式的属性

    /**
    * Define a reactive property on an Object.
    */
    export function defineReactive (
    obj: Object,
    key: string
    ) {
    const dep = new Dep()
    //判断属性存在且能把属性修改为访问器属性
    const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
      return
    }
    val = obj[key]
    let childOb = observe(val)
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        const value = val
        if (Dep.target) {
          dep.depend() // 订阅器订阅
          if (childOb) {
            childOb.dep.depend()
          }
        }
        return value
      },
      //在对象上设置属性。如果属性不存在,则添加新属性并触发更改通知。
      set: function reactiveSetter (newVal) {
        const value = val
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        val = newVal
        childOb = observe(newVal)
        dep.notify()// 订阅器通知
      }
    })
    }
    

Dep类

Dep是一个依赖收集器,可以有多个指令订阅

class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

Watcher类

Watcher类能够解析一个表达式,收集依赖,并且在表达式的值改变的时候触发回调函数。可以用于$watch()api和指令

let uid = 0
/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  value: any;
  constructor (
    vm: Component
    cb: Function
  ) {
    this.cb = cb
    this.id = ++uid // uid for batching
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.get()
  }
  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value = this.getter.call(vm, vm);
    popTarget()
    return value
  }
  /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }
  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    this.run();
  }
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    const value = this.get()
    const oldValue = this.value
    if (value !== oldValue) {
      this.value = value
      this.cb.call(this.vm, value, oldValue)
    }
  }
  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

Compile类

我们知道,Observer类是在数据劫持时进行实例化并添加到Dep中的,那么Watcher则是在模板编译的时候实例化并添加订阅的。Compile类能够解析模板指令并将模板中的变量替换成实际数据,然后初始化渲染视图;同时,为每个指令绑定对应的订阅者和回调函数,当触发更新回调时,更新视图

MVVM总结

  • 在Observer类中通过数据劫持的方式,也就是ES5中的Object.defineProperty,递归遍历观测对象的属性值,通过getter/setter观测数据
  • Dep类是一个管理Observer(观察者)和Watcher(订阅者)的中心,观察者和订阅者是一对多的关系,当setter中数据发生变化,会通知订阅中心Dep,Dep遍历通知订阅者更新
  • Watcher类是一个订阅者,收到更新通知时,会执行自身的回调函数,完成视图更新

文章作者: Susie Chang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Susie Chang !
 上一篇
HTML5脚本编程 HTML5脚本编程
跨文档消息传递(XDM) 跨文档消息传递指的是来自不同域的页面间传递消息 XDM的核心是 postMessage() 方法:接收两个参数,一条消息和一个表示消息接收方来自哪个域的字符串 尝试向内嵌框架中发送一条消息,并指定框架中文档的来源v
2018-10-22 Susie Chang
下一篇 
Vue学习(二)之渲染函数 Vue学习(二)之渲染函数
基础 一般我们通过模板可以快速的创建一个HTML;但是在某些情况下使用render 函数可以让我们使用JavaScript更加快速地创建页面 vue渲染过程:模板通过编译生成AST,再由AST生成Vue的render函数(渲染函数),渲染函
2018-10-16
  目录