zoukankan      html  css  js  c++  java
  • vue数据绑定源码

    思路分析

    数据的双向绑定,就是数据变化了自动更新视图,视图变化了自动更新数据,实际上视图变化更新数据只要通过事件监听就可以实现了,并不是数据双向绑定的关键点。关键还是数据变化了驱动视图自动更新。

    所有接下来,我们详细了解下数据如何驱动视图更新的。
    数据驱动视图更新的重点就是,如何知道数据更新了,或者说数据更新了要如何主动的告诉我们。可能大家都听过,vue的数据双向绑定原理是Object.defineProperty( )对属性设置一个set/get,是这样的没错,其实get/set只是可以做到对数据的读取进行劫持,就可以让我们知道数据更新了。但是你详细的了解整个过程吗?
    先来看张大家都不陌生的图:

    • Observe 类劫持监听所有属性,主要给响应式对象的属性添加 getter/setter 用于依赖收集与派发更新
    • Dep 类用于收集当前响应式对象的依赖关系
    • Watcher 类是观察者,实例分为渲染 watcher、计算属性 watcher、侦听器 watcher三种

    介绍数据驱动更新之前,先介绍下面4个类和方法,然后从数据的入口initState开始按顺序介绍,以下类和方法是如何协作,达到数据驱动更新的。

    defineReactive

    这个方法,用处可就大了。

    我们看到他是给对象的键值添加get/set方法,也就是对属性的取值和赋值都加了拦截,同时用闭包给每个属性都保存了一个Dep对象。

    当读取该值的时候,就把当前这个watcherDep.target)添加进他的dep里的观察者列表,这个watcher也会把这个dep添加进他的依赖列表。
    当给设置值的时候,就让这个闭包保存的dep去通知他的观察者列表的每一个watcher

    
    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      const dep = new Dep()
    
      const property = Object.getOwnPropertyDescriptor(obj, key)
      if (property && property.configurable === false) {
        return
      }
    
      // cater for pre-defined getter/setters
      const getter = property && property.get
      if (!getter && arguments.length === 2) {
        val = obj[key]
      }
      const setter = property && property.set
    
      let childOb = !shallow && observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          if (Dep.target) {
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = !shallow && observe(newVal)
          dep.notify()
        }
      })
    }
    

    Observer

    什么是可观察者对象呢?

    简单来说:就是数据变更时可以通知所有观察他的观察者。

    1、取值的时候,能把要取值的watcher(观察者对象)加入它的dep(依赖,也可叫观察者管理器)管理的subs列表里(即观察者列表);

    2、设置值的时候,有了变化,所有依赖于它的对象(即它的dep里收集到的观察者watcher)都得到通知。

    这个类功能就是把数据转化成可观察对象。针对Object类型就调用defineReactive方法循环把每一个键值都转化。针对Array,首先是对Array经过特殊处理,使它可以监控到数组发生了变化,然后对数组的每一项递归调用Observer进行转化。

    对于Array是如何处理的呢?这个放在下面单独说。

    
    export class Observer {
    
      /**
       *如果是对象就循环把对象的每一个键值都转化成可观察者对象
       */
      walk (obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i])
        }
      }
    
      /**
       * 如果是数组就对数组的每一项做转化
       */
      observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    }
    

    Dep

    这个类功能简单来说就是管理数据的观察者的。当有观察者读取数据时,保存观察者到subs,以便当数据变化了的时候,可以通知所有的观察者去update,也可以删除subs里的某个观察者。

    
    export default class Dep {
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
    
      removeSub (sub: Watcher) {
        remove(this.subs, sub)
      }
        
      // 这个方法非常绕,Dep.target就是一个Watcher对象,Watcher把这个依赖加进他的依赖列表里,然后调用dep.addSub再把这个Watcher加入到他的观察者列表里。
      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()
        }
      }
    }
    

    Watcher

    
    export default class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        // 省去了初始化各种属性和option
        this.dirty = this.lazy // for lazy watchers
        // 解析expOrFn,赋值给this.getter
        // expOrFn也要明白他是什么?
        // 当是渲染watcher时,expOrFn是updateComponent,即重新渲染执行render
        // 当是计算watcher时,expOrFn是计算属性的计算方法
        // 当是侦听器watcher时,expOrFn是watch属性的取值表达式,可以去读取要watch的数据,this.cb就是watch的handler属性
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        } else {
          this.getter = parsePath(expOrFn)
        }
        this.value = this.lazy
          ? undefined
          : this.get()
      }
    
      /**
       * 执行this.getter,同时重新进行依赖收集
       */
      get () {
        pushTarget(this)
        const vm = this.vm
        let value = this.getter.call(vm, vm)
        if (this.deep) {
          // 对于deep的watch属性,处理的很巧妙,traverse就是去递归读取value的值,
          // 就会调用他们的get方法,进行了依赖收集
          traverse(value)
        }
        popTarget()
        this.cleanupDeps()
        return value
      }
    
      /**
       * 不重复的把当前watcher添加进依赖的观察者列表里
       */
      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)
          }
        }
      }
    
      /**
       * 清理依赖列表:当前的依赖列表和新的依赖列表比对,存在于this.deps里面,
       * 却不存在于this.newDeps里面,说明这个watcher已经不再观察这个依赖了,所以
       * 要让个依赖从他的观察者列表里删除自己,以免造成不必要的watcher更新。然后
       * 把this.newDeps的值赋给this.deps,再把this.newDeps清空
       */
      cleanupDeps () {
        let i = this.deps.length
        while (i--) {
          const dep = this.deps[i]
          if (!this.newDepIds.has(dep.id)) {
            dep.removeSub(this)
          }
        }
        let tmp = this.depIds
        this.depIds = this.newDepIds
        this.newDepIds = tmp
        this.newDepIds.clear()
        tmp = this.deps
        this.deps = this.newDeps
        this.newDeps = tmp
        this.newDeps.length = 0
      }
    
      /**
       * 当一个依赖改变的时候,通知它update
       */
      update () {
        if (this.lazy) {
          // 对于计算watcher时,不需要立即执行计算方法,只要设置dirty,意味着
          // 数据不是最新的了,使用时需要重新计算
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          // 调度watcher执行计算。
          queueWatcher(this)
        }
      }
    
      /**
       * Scheduler job interface.
       * Will be called by the scheduler.
       */
      run () {
        if (this.active) {
          const value = this.get()
          if (
            value !== this.value ||
            isObject(value) ||
            this.deep
          ) {
              this.cb.call(this.vm, value, oldValue)
          }
        }
      }
    
      /**
       * 对于计算属性,当取值计算属性时,发现计算属性的watcher的dirty是true
       * 说明数据不是最新的了,需要重新计算,这里就是重新计算计算属性的值。
       */
      evaluate () {
        this.value = this.get()
        this.dirty = false
      }
    
      /**
       * 把这个watcher所观察的所有依赖都传给Dep.target,即给Dep.target收集
       * 这些依赖。
       * 举个例子:具体可以看state.js里的createComputedGetter这个方法
       * 当render里依赖了计算属性a,当渲染watcher在执行render时就会去
       * 读取a,而a会去重新计算,计算完了渲染watcher出栈,赋值给Dep.target
       * 然后执行watcher.depend,就是把这个计算watcher的所有依赖也加入给渲染watcher
       * 这样,即使data.b没有被直接用在render上,也通过计算属性a被间接的是用了
       * 当data.b发生改变时,也就可以触发渲染更新了
       */
      depend () {
        let i = this.deps.length
        while (i--) {
          this.deps[i].depend()
        }
      }
    }
    

    综上所述,就是vue数据驱动更新的方法了,下面是对整个过程的简单概述:
    每个vue实例组件都有相应的watcher对象,这个watcher是负责更新渲染的。他会在组件渲染过程中,把属性记录为依赖,也就是说,她在渲染的时候就把所有渲染用到的prop和data都添加进watcher的依赖列表里,只有用到的才加入。同时把这个watcher加入进data的依赖的订阅者列表里。也就是watcher保存了它都依赖了谁,data的依赖里保存了都谁订阅了它。这样data在改变时,就可以通知他的所有观察者进行更新了。渲染的watcher触发的更新就是重新渲染,后续的事情就是render生成虚拟DOM树,进行diff比对,将不同反应到真实的DOM中。

    queueWatcher

    下面是Watcher的update方法,可以看的除了是计算属性和标记了是同步的情况以外,全部都是推入观察者队列中,下一个tick时调用。也就是数据变化不是立即就去更新的,而是异步批量去更新的。

    
    update () {
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    
    

    下面来看看queueWatcher方法

    
    export function queueWatcher (watcher: Watcher) {
      const id = watcher.id
      if (has[id] == null) {
        has[id] = true
        if (!flushing) {
          queue.push(watcher)
        } else {
          let i = queue.length - 1
          while (i > index && queue[i].id > watcher.id) {
            i--
          }
          queue.splice(i + 1, 0, watcher)
        }
        if (!waiting) {
          waiting = true
          nextTick(flushSchedulerQueue)
        }
      }
    }
    

    这里使用了一个 has 的哈希map用来检查是否当前watcher的id是否存在,若已存在则跳过,不存在则就push到queue,队列中并标记哈希表has,用于下次检验,防止重复添加。因为执行更新队列时,是每个watcher都被执行run,如果是相同的watcher没必要重复执行,这样就算同步修改了一百次视图中用到的data,异步更新计算的时候也只会更新最后一次修改。

    nextTick(flushSchedulerQueue)把回调方法flushSchedulerQueue传递给nextTick,一次异步更新,只要传递一次异步回调函数就可以了,在这个异步回调里统一批量的处理queue中的watcher,进行更新。

    
    function flushSchedulerQueue () {
      flushing = true
      let watcher, id
    
      queue.sort((a, b) => a.id - b.id)
    
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        id = watcher.id
        has[id] = null
        watcher.run()
      }
    
      resetSchedulerState()
    
    }
    

    每次执行异步回调更新,就是循环执行队列里的watcher.run方法。

    在循环队列之前对队列进行了一次排序:

    • 组件更新的顺序是从父组件到子组件的顺序,因为父组件总是比子组件先创建。
    • 一个组件的user watchers(侦听器watcher)比render watcher先运行,因为user watchers往往比render watcher更早创建
    • 如果一个组件在父组件watcher运行期间被销毁,它的watcher执行将被跳过

    nextTick

    
    export function nextTick (cb?: Function, ctx?: Object) {
    // 这个方法里,我把关于不写回调,使用promise的情况处理去掉了,把trycatch都去掉了。
      callbacks.push(() => {
         cb.call(ctx)
      })
      if (!pending) {
        pending = true
        setTimeout(flushCallbacks, 0) // 异步任务进行了简化
      }
    }
    

    下面是异步的回调方法flushCallbacks,遍历执行callbacks里的方法,也就是遍历执行调用nextTick时传入的回调方法。

    你可能就要问了,queueWatcher的时候不是控制了只会调用一次nextTick吗,为啥要用callbacks数组来存储呢。举个例子:

    你写了一堆同步语句,改变了data等,然后又调用了一个this.$nextTick来做个异步回调,这个时候不就又会向callbacks数组里push了一个回调方法吗。

    
    function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }
    

    如何把数组处理成可观察对象

    不考虑兼容处理

    本质就是改写数组的原型方法。当数组调用methodsToPatch这些方法时,就意味者数组发生了变化,需要通知所有观察者update。

    
    
    const methodsToPatch = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    
    methodsToPatch.forEach(function (method) {
      // 保存数组的原始原型方法
      const original = arrayProto[method]
      def(arrayMethods, method, function mutator (...args) {
        const result = original.apply(this, args)
        const ob = this.__ob__
        let inserted
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = args
            break
          case 'splice':
            inserted = args.slice(2)
            break
        }
        if (inserted) ob.observeArray(inserted)
        // notify change
        ob.dep.notify()
        return result
      })
    })
    
    

    后记
    关于从数据入口initState开始解析的部分,写在一篇里篇幅太大,我放在下一篇文章了,记得去读哦,可以加深理解。

    参考文章

    剖析Vue实现原理 - 如何实现双向绑定mvvm

    vue.js源码解读系列 - 剖析observer,dep,watch三者关系 如何具体的实现数据双向绑定

    Vue源码学习笔记之Dep和Watcher

    watcher调度原理

    Vue源码阅读 - 依赖收集原理

    原文地址:https://segmentfault.com/a/1190000016922527

  • 相关阅读:
    【C#语言规范版本5.0学习】1.5类和对象(一)
    【C#语言规范版本5.0学习】1.5类和对象(二、类的方法)
    【C#语言规范版本5.0学习】1.4语句
    mac 安装docker
    Laravel 操作指令
    php 查看接口运行时间
    MySql 按日期条件查询数据
    Laravel操作上传文件的方法
    统计数据表中某个字段的值大于2条的数据
    循环中合并数组
  • 原文地址:https://www.cnblogs.com/datiangou/p/10125900.html
Copyright © 2011-2022 走看看