XingYun blog
  • JS基础

    • 图解js原型链
    • JS Event Loop
    • 对象的底层数据结构
    • 让你的JavaScript代码简单又高效
    • 函数参数按值传递
    • 判断数据类型
    • 浮点数精度问题和解决办法
    • 常用方法snippet
    • 实现Promise
    • 防抖和节流
    • 巧用sort排序
  • CSS && HTML

    • CSS也需要性能优化
    • class命名规范
    • em、px、rem、vh、vw 区别
    • CSS揭秘阅读笔记
  • 浏览器

    • 浏览器是如何渲染页面的
    • 重排和重绘
    • BOM浏览器对象模型
    • DOM事件
    • 浏览器存储
  • 数据结构

    • JS实现链表
    • JS实现栈与栈应用
    • JS实现常见排序
    • 哈夫曼编码
    • MD5算法
  • vue原理浅析

    • Vue虚拟dom与Diff算法
    • 前端打包文件的缓存机制
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
  • 前端工程化

    • 浏览器是如何渲染页面的
    • 前端打包需要gzip压缩吗
    • 前端打包文件的缓存机制
    • webpack loader和plugin
  • 轮子&&组件库

    • 实现水波浪进度球
  • 文字转语音mp3文件
  • 文件上传前后端实现
  • moment.js给定时间获取自然月、周的时间轴
  • 实现文件上传功能
  • 批量下载照片
  • leaflet改变坐标原点
  • 网络

    • 有了MAC地址 为什么还需要IP地址
    • 为什么IP地址老是变
    • 我们为什么需要IPV6
    • TCP与UDP
  • 计算机组成原理

    • ASCII、Unicode、UTF-8和UTF-16
  • VSCode

    • VSCode图片预览插件 Image preview
    • rsync:linux间的高效传输工具

XingYun

冲!
  • JS基础

    • 图解js原型链
    • JS Event Loop
    • 对象的底层数据结构
    • 让你的JavaScript代码简单又高效
    • 函数参数按值传递
    • 判断数据类型
    • 浮点数精度问题和解决办法
    • 常用方法snippet
    • 实现Promise
    • 防抖和节流
    • 巧用sort排序
  • CSS && HTML

    • CSS也需要性能优化
    • class命名规范
    • em、px、rem、vh、vw 区别
    • CSS揭秘阅读笔记
  • 浏览器

    • 浏览器是如何渲染页面的
    • 重排和重绘
    • BOM浏览器对象模型
    • DOM事件
    • 浏览器存储
  • 数据结构

    • JS实现链表
    • JS实现栈与栈应用
    • JS实现常见排序
    • 哈夫曼编码
    • MD5算法
  • vue原理浅析

    • Vue虚拟dom与Diff算法
    • 前端打包文件的缓存机制
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
  • 前端工程化

    • 浏览器是如何渲染页面的
    • 前端打包需要gzip压缩吗
    • 前端打包文件的缓存机制
    • webpack loader和plugin
  • 轮子&&组件库

    • 实现水波浪进度球
  • 文字转语音mp3文件
  • 文件上传前后端实现
  • moment.js给定时间获取自然月、周的时间轴
  • 实现文件上传功能
  • 批量下载照片
  • leaflet改变坐标原点
  • 网络

    • 有了MAC地址 为什么还需要IP地址
    • 为什么IP地址老是变
    • 我们为什么需要IPV6
    • TCP与UDP
  • 计算机组成原理

    • ASCII、Unicode、UTF-8和UTF-16
  • VSCode

    • VSCode图片预览插件 Image preview
    • rsync:linux间的高效传输工具
  • 3个提升Vue性能的写法
  • 重读Vue文档
  • moment.js给定时间获取自然月、周的时间轴
  • Vue虚拟dom与Diff算法
  • 深入响应式原理
    • Echart样例
    • 我的Vue指令库
    • 滚动到底部加载更多
    • 实现一键换肤
    • 手写Vue数据劫持
    • vue-router核心原理与手写实现
    • computed和watch
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
    • webpack loader和plugin
    • keep-alive组件原理
    • vue插槽进化
    • Vue多层嵌套组件
    • vue生命周期hook
    • vue监听dom元素的resize事件
    • 前端打包需要gzip压缩吗
    • 实现水波浪进度球
    • Vue
    XingYun
    2022-02-13
    目录

    深入响应式原理

    Vue 最独特的特性之一,是其非侵入性的响应式系统。

    数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。

    # Object.defineProperty

    当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 把这些 property 全部转为 getter/setter。

    这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。

    Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

    # 响应式原理图解

    每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

    # 源码浅析

    下面从一个组件初始化开始,了解一下响应式实现过程

    在 Vue 初始化时,会调用 initState ,它会初始化 props ,methods ,data ,computed ,watch 等.

    initState

    // src/core/instance/state.js
    export function initState(vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      if (opts.props) initProps(vm, opts.props)
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        initData(vm)
      } else {
        observe((vm._data = {}), true /* asRootData */)
      }
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    initProps

    // src/core/instance/state.js
    function initProps(vm: Component, propsOptions: Object) {
      const propsData = vm.$options.propsData || {}
      const props = (vm._props = {}) // 缓存 props 的每个 key,性能优化
      const keys = (vm.$options._propKeys = [])
      const isRoot = !vm.$parent // root instance props should be converted // 非根实例的情况
      if (!isRoot) {
        // 响应式的优化,主要优化在响应式处理的递归过程
        toggleObserving(false)
      }
      for (const key in propsOptions) {
        // 缓存 key
        keys.push(key) // 校验并返回值,主要检查传递的数据是否符合 prop 的定义规范
        const value = validateProp(
          key,
          propsOptions,
          propsData,
          vm
        ) /* istanbul ignore else */
        if (process.env.NODE_ENV !== 'production') {
          const hyphenatedKey = hyphenate(key)
          if (
            isReservedAttribute(hyphenatedKey) ||
            config.isReservedAttr(hyphenatedKey)
          ) {
            warn(
              `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
              vm
            )
          } // 为 props 的每个 key 设置响应式
          defineReactive(props, key, value, () => {
            if (!isRoot && !isUpdatingChildComponent) {
              warn(
                `Avoid mutating a prop directly since the value will be ` +
                  `overwritten whenever the parent component re-renders. ` +
                  `Instead, use a data or computed property based on the prop's ` +
                  `value. Prop being mutated: "${key}"`,
                vm
              )
            }
          })
        } else {
          // 为 props 的每个 key 设置响应式
          defineReactive(props, key, value)
        } // static props are already proxied on the component's prototype // during Vue.extend(). We only need to proxy props defined at // instantiation here.
        if (!(key in vm)) {
          proxy(vm, `_props`, key)
        }
      } // 响应式的优化,主要优化在响应式处理的递归过程
      toggleObserving(true)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51

    props 的初始化就是对其进行遍历,遍历过程主要做两件事:

    1. 调用 defineReactive 对每个值做响应式处理。
    2. 通过 proxy 把 vm._props.xxx 的访问代理到 vm.xxx 上。

    initData

    // src/core/instance/state.js
    function initData(vm: Component) {
      let data = vm.$options.data // 判断 data 是函数还是对象
      data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
      if (!isPlainObject(data)) {
        data = {}
        process.env.NODE_ENV !== 'production' &&
          warn(
            'data functions should return an object:\n' +
              'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
            vm
          )
      } // 代理数据到 vm 实例上。 // 判断去重, data 上的属性不能和 props、methods 上的属性相同。
      const keys = Object.keys(data)
      const props = vm.$options.props
      const methods = vm.$options.methods
      let i = keys.length
      while (i--) {
        const key = keys[i]
        if (process.env.NODE_ENV !== 'production') {
          if (methods && hasOwn(methods, key)) {
            warn(`Method "${key}" has already been defined as a data property.`, vm)
          }
        }
        if (props && hasOwn(props, key)) {
          process.env.NODE_ENV !== 'production' &&
            warn(
              `The data property "${key}" is already declared as a prop. ` +
                `Use prop default value instead.`,
              vm
            )
        } else if (!isReserved(key)) {
          // 代理操作
          proxy(vm, `_data`, key)
        }
      } // 响应式操作
      observe(data, true /* asRootData */)
    }
    export function getData(data: Function, vm: Component): any {
      // #7573 disable dep collection when invoking data getters
      pushTarget()
      try {
        return data.call(vm, vm)
      } catch (e) {
        handleError(e, vm, `data()`)
        return {}
      } finally {
        popTarget()
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50

    data 的初始化和 props 目的差不多,这里主要做了三件事:

    1. 检查 data 上的属性不能和 props 、methods 上的属性相同。
    2. 通过 proxy 把 vm._data.xxx 的访问代理到 vm.xxx 上。
    3. 调用 observe 把 data 上的数据变成响应式。

    proxy

    // src/core/instance/state.js
    export function proxy(target: Object, sourceKey: string, key: string) {
      sharedPropertyDefinition.get = function proxyGetter() {
        return this[sourceKey][key]
      }
      sharedPropertyDefinition.set = function proxySetter(val) {
        this[sourceKey][key] = val
      }
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    代理的作用是把 props 和 data 上的属性代理到 vm 实例上,这就是为什么我们定义了 props.xxx,却可以通过 this.xxx 进行访问。

    observe

    // src/core/observer/index.js
    export function observe(value: any, asRootData: ?boolean): Observer | void {
      // 非对象和 VNode 实例不做响应式处理
      if (!isObject(value) || value instanceof VNode) {
        return
      }
      let ob: Observer | void
      if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        // 如果 value 对象上存在 __ob__ 属性,则表示已经做过观察了,直接返回 __ob__ 属性
        ob = value.__ob__
      } else if (
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
      ) {
        // 创建观察者实例
        ob = new Observer(value)
      }
      if (asRootData && ob) {
        ob.vmCount++
      }
      return ob
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25

    observe 就是给非 VNode 的对象类型创建观察者实例 Observer,如果成功创建过,直接返回已有的观察者,否则创建新的实例。

    Observer

    // src/core/observer/index.js
    /**
     * Observer class that is attached to each observed
     * nce attached, the observer converts the target
     * object's property keys into getter/setters that
     * collect dependencies and dispatch updates.
     */
    export class Observer {
      value: any
      dep: Dep
      vmCount: number // number of vms that have this object as root $data
      constructor(value: any) {
        this.value = value // 为什么在 Observer 里面声明 Dep?
        this.dep = new Dep()
        this.vmCount = 0 // 在 value 对象上设置 __ob__ 属性,引用了当前 Observer 实例
        def(value, '__ob__', this) // 判断类型
        if (Array.isArray(value)) {
          // 覆盖数组默认的七个原型方法,以实现数组响应式
          // hasProto = '__proto__' in {}
          if (hasProto) {
            protoAugment(value, arrayMethods)
          } else {
            copyAugment(value, arrayMethods, arrayKeys)
          }
          this.observeArray(value)
        } else {
          this.walk(value)
        }
      }
      /**
       * 遍历对上的每个 key,设置响应式
       * 只有类型为 Object 时才走到这里
       */
      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])
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48

    **Observer 类会被附加到被观察的对象上,也就是说,每一个响应式对象上都会有一个 **ob****;

    然后对数据类型进行了一个判断;若是数组,则判断是否存在 proto 属性,因为要通过原型链覆盖数组的几个方法,判断有无 proto 属性,主要是一种兼容的处理方式,proto 不是标准属性,所以有些浏览器不支持,比如 IE6-10,Opera10.1。

    为什么在 Observer 里面声明 Dep? 了解过响应式的道友都知道,我们应该是一个 key 对应一个 dep 嘛,来管理依赖,当 key 的值发生变化时,触发 setter 通知更新。这里的 dep 主要是作用于 Object 属性增加和删除,Array 的变更方法。比如: { a: { b: 1 } } 使用了 $set 增加了一个属性 { a: { b: 1, c: 2 } } ,不管 a.b 还是 a.c ,增加还是删除,只要是和 a 相关的,就直接更新。

    下面来看响应式核方法

    defineReactive

    // src/core/observer/index.js
    export function defineReactive(
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      // 实例化 dep,一个 key 一个 dep
      const dep = new Dep() // 获取 obj[key] 的属性描述符,发现它是不可配置对象的话直接 return
      const property = Object.getOwnPropertyDescriptor(obj, key)
      if (property && property.configurable === false) {
        return
      } // 记录 getter 和 setter,获取 val 值
      const getter = property && property.get
      const setter = property && property.set
      if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
      } // 递归调用,处理 val 的值为对象的情况
      let childOb = !shallow && observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true, // 劫持读取操作
        get: function reactiveGetter() {
          const value = getter ? getter.call(obj) : val // Dep.target 是 Dep的一个静态属性,保存的是当前的 Watcher 实例。 // 在 new Watcher 实例化的时候(computed 除外,因为它懒执行)会触发读取造作,被劫持运行这个 get 函数,进行依赖收集。 // 在实例化 Watcher 最后,Dep.target 设置为 null,避免重复收集。
          if (Dep.target) {
            // 依赖收集,在 dep 中添加 watcher,也在 watcher 中添加 dep
            dep.depend() // childOb 表示当前的 val 还是一个复杂类型,对象或者数组。
            if (childOb) {
              // 这个 dep 是在 Observer中创建的,之前提到过。
              childOb.dep.depend()
              if (Array.isArray(value)) {
                // 处理数组内还是对象的情况
                dependArray(value)
              }
            }
          }
          return value
        }, // 劫持修改操作
        set: function reactiveSetter(newVal) {
          // 旧的 obj[key]
          const value = getter ? getter.call(obj) : val // 如果新旧值一样,则直接 return,无需更新
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          } /* eslint-enable no-self-compare */
          if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
          } // setter 不存在说明该属性是一个只读属性,直接 return
          if (getter && !setter) return // 设置新值
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          } // 对新值进行观察,让新值也是响应式的
          childOb = !shallow && observe(newVal) // 依赖通知更新
          dep.notify()
        }
      })
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    // src/core/observer/index.js
    /**
     * 遍历每个数组元素,递归处理数组项为对象的情况,为其添加依赖.
     * 因为前面的递归阶段无法为数组中的对象元素添加依赖.
     */
    function dependArray(value: Array<any>) {
      for (let e, i = 0, l = value.length; i < l; i++) {
        e = value[i]
        e && e.__ob__ && e.__ob__.dep.depend()
        if (Array.isArray(e)) {
          dependArray(e)
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    defineReactive 的作用就是利用 Object.defineProperty 对数据的读写进行劫持,给属性 key 添加 getter 和 setter ,用于依赖收集和通知更新。

    如果传进来的值依旧是一个对象,则递归调用 observe 方法,保证子属性都能变成响应式。

    # 依赖收集

    Dep

    //  src/core/observer/dep.js
    /* @flow */
    import type Watcher from './watcher'
    import { remove } from '../util/index'
    import config from '../config'
    let uid = 0
    /**
     * A dep is an observable that can have multiple
     * directives subscribing to it.
     */
    export default class Dep {
      static target: ?Watcher
      id: number
      subs: Array<Watcher>
      constructor() {
        this.id = uid++
        this.subs = []
      } // 添加订阅,把 Watcher实例,保存到 subs中
      addSub(sub: Watcher) {
        this.subs.push(sub)
      } // 移除订阅,把 Watcher实例,从 subs 中移除
      removeSub(sub: Watcher) {
        remove(this.subs, sub)
      } // 向 Watcher 中添加 dep
      depend() {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
      } // 通知更新
      notify() {
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        if (process.env.NODE_ENV !== 'production' && !config.async) {
          // subs aren't sorted in scheduler if not running async
          // we need to sort them now to make sure they fire in correct
          // order
          subs.sort((a, b) => a.id - b.id)
        } // 遍历 dep 中存储的 watcher,执行 watcher.update()
        for (let i = 0, l = subs.length; i < l; i++) {
          subs[i].update()
        }
      }
    }
    /**
     * 当前正在执行的 watcher,同一时间只会有一个 watcher 在执行
     * Dep.target = 当前正在执行的 watcher ,并推入栈中
     * 通过调用 pushTarget 方法完成赋值,调用 popTarget 方法完成重置
     */
    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]
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58

    Watcher

    // src/core/observer/watcher.js
    /* @flow */
    import {
      warn,
      remove,
      isObject,
      parsePath,
      _Set as Set,
      handleError,
      invokeWithErrorHandling,
      noop
    } from '../util/index'
    import { traverse } from './traverse'
    import { queueWatcher } from './scheduler'
    import Dep, { pushTarget, popTarget } from './dep'
    import type { SimpleSet } from '../util/index'
    let uid = 0
    /**
     * 一个组件一个 watcher(渲染 watcher)或者一个表达式一个 watcher(用户watcher)
     * 当数据更新时 watcher 会被触发,访问 this.computedProperty 时也会触发 watcher
     */
    export default class Watcher {
      vm: Component
      expression: string
      cb: Function
      id: number
      deep: boolean
      user: boolean
      lazy: boolean
      sync: boolean
      dirty: boolean
      active: boolean
      deps: Array<Dep>
      newDeps: Array<Dep>
      depIds: SimpleSet
      newDepIds: SimpleSet
      before: ?Function
      getter: Function
      value: any
      constructor(
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        this.vm = vm
        if (isRenderWatcher) {
          vm._watcher = this
        }
        vm._watchers.push(this) // options
        if (options) {
          this.deep = !!options.deep
          this.user = !!options.user
          this.lazy = !!options.lazy
          this.sync = !!options.sync
          this.before = options.before
        } else {
          this.deep = this.user = this.lazy = this.sync = false
        }
        this.cb = cb
        this.id = ++uid // uid for batching
        this.active = true
        this.dirty = this.lazy // for lazy watchers
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
        this.expression =
          process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        } else {
          // this.getter = function() { return this.xx }
          // 在 this.get 中执行 this.getter 时会触发依赖收集
          // 待后续 this.xx 更新时就会触发响应式
          this.getter = parsePath(expOrFn)
          if (!this.getter) {
            this.getter = noop
            process.env.NODE_ENV !== 'production' &&
              warn(
                `Failed watching path: "${expOrFn}" ` +
                  'Watcher only accepts simple dot-delimited paths. ' +
                  'For full control, use a function instead.',
                vm
              )
          }
        }
        this.value = this.lazy ? undefined : this.get()
      }
      /**
       * 执行 this.getter,并重新收集依赖
       * this.getter 是实例化 watcher 时传递的第二个参数,一个函数或者字符串,比如:updateComponent 或者 parsePath 返回的读取 this.xx 属性值的函数
       * 为什么要重新收集依赖?
       *   因为触发响应式数据更新时,虽然已经经过 observe 观察,但却没有进行依赖收集,
       *   所以,在更新页面时,会重新执行一次 render 函数,执行期间会触发读取操作,这时候进行依赖收集
       */
      get() {
        // 在需要进行依赖收集的时候调用
        // 设置  targetStack.push(target) 和 Dep.target = watcher
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          // 执行回调函数,比如 updateComponent,进入 patch 阶段
          value = this.getter.call(vm, vm)
        } catch (e) {
          if (this.user) {
            handleError(e, vm, `getter for watcher "${this.expression}"`)
          } else {
            throw e
          }
        } finally {
          // "touch" every property so they are all tracked as
          // dependencies for deep watching
          if (this.deep) {
            traverse(value)
          }
          // 依赖收集结束调用
          // 设置 targetStack.pop() 和 targetStack[targetStack.length - 1]
          popTarget() // 清除依赖
          this.cleanupDeps()
        }
        return value
      }
      /**
       * 添加 dep 到 watcher
       * 添加 watcher 到 dep
       */
      addDep(dep: Dep) {
        const id = dep.id
        if (!this.newDepIds.has(id)) {
          // 保存 id 用于去重
          this.newDepIds.add(id) // 添加 dep 到当前 watcher
          this.newDeps.push(dep) // 避免在 dep 中重复添加 watcher
          if (!this.depIds.has(id)) {
            // 添加当前 watcher 到 dep
            dep.addSub(this)
          }
        }
      }
      /**
       * 清除依赖,每次数据变化都会重新 render,
       * 那么 vm._render() 方法又会再次执行,并再次触发数据的 getters,所以 Watcher 在构造函数中会初始化 2 个 Dep 实例数组.
       * this.deps 表示上一次的 dep 实例数组,this.newDeps 表示新添加的 dep 实例数组。
       */
      cleanupDeps() {
        let i = this.deps.length // 遍历 deps,移除对 dep.subs 数组中 Wathcer 的订阅
        while (i--) {
          const dep = this.deps[i]
          if (!this.newDepIds.has(dep.id)) {
            dep.removeSub(this)
          }
        } // newDepIds 和 depIds 做交换,然后清空 newDepIds
        let tmp = this.depIds
        this.depIds = this.newDepIds
        this.newDepIds = tmp
        this.newDepIds.clear() // newDeps 和 deps 做交换,然后清空 newDeps
        tmp = this.deps
        this.deps = this.newDeps
        this.newDeps = tmp
        this.newDeps.length = 0
      }
    
      // ... 下面还有几个方法,暂时不看,放在别的地方一块看
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166

    依赖收集流程

    回顾一下在执行 mount 挂载过程中 mountComponent 里有这么一段逻辑。

    // src/core/instance/lifecycle.js
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
    
    new Watcher(
      vm,
      updateComponent,
      noop,
      {
        before() {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      },
      true /* isRenderWatcher */
    )
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    收集流程:

    1. 当实例化 Watcher 时,会执行 Watcher 构造函数中的 this.get()。
    2. get 中首先调用 pushTarget(this), 其实就是 Dep.target = 当前正在执行的 Watcher 并推入栈中。
    3. 然后执行 value = this.getter.call(vm, vm) 。
    4. this.getter 对应的就是 updateComponent,实际执行的是 vm._update(vm._render(), hydrating);它会先调用 vm._render()。
    5. vm._render() 的作用就是生成 VNode,过程中会触发对数据的访问,也就是触发了 getter.
    6. 每个对象的 key 都会有一个对应的 dep,在 getter 中会调用 dep.depend(),也就会调用 Dep.target.addDep(this);因为在第2步时 Dep.target = 当前正在执行的 Watcher,所以调用的应该是 当前正在执行的 Watcher.addDep(this)。
    7. addDep 中在不重复的情况下调用 dep.addSub(this),也就会执行 this.subs.push(sub) ,也就是把 Watcher 实例保存到 dep 的 subs 中。
    8. 在 vm._render() 过程中,会触发所有数据的 getter,这样实际就完成了依赖收集的过程,但是 Watcher 构造函数中的 this.get() 后续还有一些操作。
    9. if (this.deep) { traverse(value) } 要递归去访问 value,触发它所有子项的 getter 。
    10. 然后执行 popTarget() , 依赖收集结束,重新设置 Dep.target。
    11. 最后就是调用 this.cleanupDeps(),清除依赖。

    # 数组七个方法的重写

    // src/core/observer/array.js
    /*
     * 定义 arrayMethods 对象,用于增强 Array.prototype
     * 当访问 arrayMethods 对象上的那七个方法时会被劫持,以实现数组响应式
     */
    import { def } from '../util/index'
    // 备份 数组 原型对象
    const arrayProto = Array.prototype
    // 通过继承的方式创建新的 arrayMethods
    export const arrayMethods = Object.create(arrayProto)
    // 操作数组的七个方法
    const methodsToPatch = [
      'push',
      'pop',
      'shift',
      'unshift',
      'splice',
      'sort',
      'reverse'
    ]
    /**
     * 拦截方法并触发事件
     */
    methodsToPatch.forEach(function (method) {
      // 缓存原生方法,比如 push
      const original = arrayProto[method]
      // def 就是 Object.defineProperty,劫持 arrayMethods.method 的访问
      def(arrayMethods, method, function mutator (...args) {
        // 先执行原生方法,比如 push.apply(this, args)
        const result = original.apply(this, args)
        const ob = this.__ob__
        // 如果 method 是以下三个之一,说明是新插入了值
        let inserted
        switch (method) {
          case 'push':
          case 'unshift':
            inserted = args
            break
          case 'splice':
            inserted = args.slice(2)
            break
        }
        // 对新插入的值做响应式处理
        if (inserted) ob.observeArray(inserted)
        // 通知更新
        ob.dep.notify()
        return result
      })
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49

    数组方法重写主要做了这么几件事:

    1. arrayMethods 继承了 Array。
    2. 对数组中能改变数组自身的七个方法进行了劫持和重写,劫持是使其被调用时,调用的是重写后的方法。
    3. 重写后的方法首先会调用原来原型上的逻辑。
    4. 判断可以添加值的三个方法 push、unshift、splice ,获取新插入的值,进行响应处理。
    5. 最后调用 ob.dep.notify() 通知更新。

    响应式原理的核心流程代码大概就这些,还有一些watch computed $set 和 $delete等的实现都是基于响应式原理的应用

    todo

    vue源码有关响应式的代码还是比较多的,有时间了自己实现一个简易版的响应式

    #vue
    上次更新: 2023/04/05, 09:41:10
    Vue虚拟dom与Diff算法
    Echart样例

    ← Vue虚拟dom与Diff算法 Echart样例→

    最近更新
    01
    JavaScript-test
    07-20
    02
    二维码的原理
    07-20
    03
    利用ChatGPT优化代码
    07-20
    更多文章>
    Theme by Vdoing | Copyright © 2021-2023 XingYun | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式