学习笔记(十五)Vue.js源码剖析 - 响应式原理

Vue.js 源码剖析 - 响应式原理

准备工作

Vue源码获取

这里主要分析 Vue 2.6版本的源码,使用Vue 3.0版本来开发项目还需要一段时间的过渡

  • 项目地址:

  • Fork一份到自己仓库,克隆到本地,这样可以自己写注释提交,如果直接从github clone太慢,也可以先导入gitee,再从gitee clone到本地

  • 查看Vue源码的目录结构

    src
    compiler 编译相关
    core Vue 核心库
    platforms 平台相关代码(web、weex)
    server SSR 服务端渲染
    sfc 单文件组件 .vue 文件编译为 js 对象
    shared 公共代码

Flow

Vue 2.6 版本中使用了Flow,用于代码的静态类型检查

Vue 3.0+ 版本已使用TypeScript进行开发,因此不再需要Flow

  • Flow是JavaScript的静态类型检查器

  • Flow的静态类型检查是通过静态类型推断来实现的

    • 安装Flow以后,在要进行静态类型检查的文件头中通过 // @flow/* @flow */ 注释的方式来声明启用

    • 对于变量类型的指定,与TypeScript类似

      /* @flow */
      function hello (s: string): string {
          return `hello ${s}`
      }
      hello(1) // Error
      

打包与调试

Vue 2.6中使用Rollup来打包

  • 打包工具Rollup

    • Rollup比webpack轻量
    • Rollup只处理js文件,更适合用来打包工具库
    • Rollup打包不会生成冗余的代码
  • 调试

    • 执行yarn安装依赖(有yarn.lock文件)
    • package.json文件dev script中添加 --sourcemap 参数来开启sourcemap,以便调试过程中查看代码

Vue不同构建版本的区别

执行yarn build可以构建所有版本的打包文件

UMD CommonJS ES Module
Full vue.js vue.common.js vue.esm.js
Runtime-only vue.runtime.js vue.runtime.common.js vue.runtime.esm.js
Full(production) vue.min.js
Runtime-only(production) vue.runtime.min.js

不同版本之间的区别

  • 完整版:同时包含编译器运行时的版本
    • 编译器:用来将模板字符串编译为JavaScript渲染(render)函数的代码,体积大、效率低
    • 运行时:用来创建Vue实例,渲染并处理虚拟DOM等的代码,体积小、效率高,即去除编译器之后的代码
    • 简单来说,运行时版本不包含编译器的代码,无法直接使用template模板字符串,需要自行使用render函数
    • 通过Vue Cli创建的项目,使用的是vue.runtime.esm.js版本
  • 运行时版:只包含运行时的版本
  • UMD:通用模块版本,支持多种模块方式。vue.js默认就是运行时+编译器的UMD版本
  • CommonJS:CommonJS模块规范的版本,用来兼容老的打包工具,例如browserfy、webpack 1等
  • ES Module:从2.6版本开始,Vue会提供两个esm构建文件,是为现代打包工具提供的版本
    • esm格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行tree-shaking,精简调未被用到的代码

寻找入口文件

通过查看构建过程,来寻找对应构建版本的入口文件位置

  • 以vue.js版本的构建为例,通过rollup进行构建,指定了配置文件scripts/config.js,并设置了环境变量TARGET:web-full-dev

    "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",

  • 进一步查看 scripts/config.js 配置文件

    module.exports导出的内容来自genConfig()函数,并接收了环境变量TARGET作为参数

    // scripts/config.js
    if (process.env.TARGET) {
      module.exports = genConfig(process.env.TARGET)
    } else {
      exports.getBuild = genConfig
      exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
    }
    

    genConfig函数组装生成了config配置对象,入口文件配置input: opts.entry,配置项的值opts.entry来自builds[name]

    // scripts/config.js
    function genConfig (name) {
      const opts = builds[name]
      const config = {
        input: opts.entry,
        external: opts.external,
        plugins: [
          flow(),
          alias(Object.assign({}, aliases, opts.alias))
        ].concat(opts.plugins || []),
        output: {
          file: opts.dest,
          format: opts.format,
          banner: opts.banner,
          name: opts.moduleName || 'Vue'
        },
        onwarn: (msg, warn) => {
          if (!/Circular/.test(msg)) {
            warn(msg)
          }
        }
      }
      ...
    }
    

    通过传入环境变量TARGET的值,可以找到web-full-dev相应的配置,入口文件是web/entry-runtime-with-compiler.js

    const builds = {
      // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
      'web-runtime-cjs-dev': {
        entry: resolve('web/entry-runtime.js'),
        dest: resolve('dist/vue.runtime.common.dev.js'),
        format: 'cjs',
        env: 'development',
        banner
      },
      'web-runtime-cjs-prod': {
        entry: resolve('web/entry-runtime.js'),
        dest: resolve('dist/vue.runtime.common.prod.js'),
        format: 'cjs',
        env: 'production',
        banner
      },
      // Runtime+compiler CommonJS build (CommonJS)
      'web-full-cjs-dev': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.common.dev.js'),
        format: 'cjs',
        env: 'development',
        alias: { he: './entity-decoder' },
        banner
      },
      // Runtime+compiler development build (Browser)
      'web-full-dev': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.js'),
        format: 'umd',
        env: 'development',
        alias: { he: './entity-decoder' },
        banner
      },
      ...
    }
    

使用VS Code查看Vue源码的两个问题

使用VSCode查看Vue源码时通常会碰到两个问题

  • 对于flow的语法显示异常报错
    • 修改VSCode设置,在setting.json中增加"javascript.validate.enable": false配置
  • 对于使用了泛型的后续代码,丢失高亮
    • 通过安装Babel JavaScript插件解决

一切从入口开始

入口文件 entry-runtime-with-compiler.js 中,为Vue.prototype.$mount指定了函数实现

  • el可以是DOM元素,或者选择器字符串

  • el不能是body或html

  • 选项中有render,则直接调用mount挂载DOM

  • 选项中如果没有render,判断是否有template,没有template则将el.outerHTML作为template,并尝试将template转换成render函数

    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      // 判断el是否是 DOM 元素
      // 如果el是字符串,则当成选择器来查询相应的DOM元素,查询不到则创建一个div并返回
      el = el && query(el)
    
      /* istanbul ignore if */
      // 判断el是否是body或者html
      // Vue不允许直接挂载在body或html标签下
      if (el === document.body || el === document.documentElement) {
        process.env.NODE_ENV !== 'production' && warn(
          `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
        )
        return this
      }
    
      const options = this.$options
      // resolve template/el and convert to render function
      // 判断选项中是否包含render
      if (!options.render) {
        // 如果没有render,判断是否有template
        let template = options.template
        if (template) {
          ...
        } else if (el) {
          // 如果没有template,则获取el的outerHTML作为template
          template = getOuterHTML(el)
        }
        if (template) {
          ...
    
          // 将template转换成render函数
          const { render, staticRenderFns } = compileToFunctions(template, {
            outputSourceRange: process.env.NODE_ENV !== 'production',
            shouldDecodeNewlines,
            shouldDecodeNewlinesForHref,
            delimiters: options.delimiters,
            comments: options.comments
          }, this)
          options.render = render
          options.staticRenderFns = staticRenderFns
    
          ...
        }
      }
      // 调用 mount 挂载 DOM
      return mount.call(this, el, hydrating)
    }
    

tips:$mount()函数在什么位置被调用?通过浏览器调试工具的Call Stack视图,可以简单且清晰的查看一个函数被哪个位置的上层代码所调用

Vue初始化

初始化相关的几个主要文件

  • src/platforms/web/entry-runtime-with-compiler.js

    • 重写了平台相关的$mount()方法
    • 注册了Vue.compile()方法,将HTML字符串转换成render函数
  • src/platforms/web/runtime/index.js

    • 注册了全局指令:v-model、v-show
    • 注册了全局组件:v-transition、v-transition-group
    • 为Vue原型添加了全局方法
      • _patch_:把虚拟DOM转换成真实DOM(snabbdom的patch函数)
      • $mount: 挂载方法
  • src/core/index.js

    • 调用initGlobalAPI(Vue)设置了Vue的全局静态方法
  • src/core/instance/index.js - Vue构造函数所在位置

    • 定义了Vue构造函数,调用了this._init(options)方法

    • 给Vue中混入了常用的实例成员和方法

静态成员

通过 initGlobalAPI() 函数,实现了Vue静态成员的初始化过程

  • Vue.config
  • Vue.util
    • 暴露了util对象,util中的工具方法不视作全局API的一部分,应当避免依赖它们
  • Vue.set()
    • 用来添加响应式属性
  • Vue.delete()
    • 用来删除响应式属性
  • Vue.nextTick()
    • 在下次 DOM 更新循环结束之后执行延迟回调
  • Vue.observable()
    • 用来将对象转换成响应式数据
  • Vue.options
    • Vue.options.components 保存全局组件
      • Vue.options.components.KeepAlive 注册了内置的keep-alive组件到全局Vue.options.components
    • Vue.options.directives 保存全局指令
    • Vue.options.filters 保存全局过滤器
    • Vue.options._base 保存Vue构造函数
  • Vue.use()
    • 用来注册插件
  • Vue.mixin()
    • 用来实现混入
  • Vue.extend(options)
    • 使用基础 Vue 构造器,创建一个子组件,参数是包含选项的对象
  • Vue.component()
    • 用来注册或获取全局组件
  • Vue.directive()
    • 用来注册或获取全局指令
  • Vue.filter()
    • 用来注册或获取全局过滤器
  • Vue.compile()
    • 将一个模板字符串编译成 render 函数
    • 这个静态成员方法是在入口js文件中添加的
// src/core/global-api/index.js
export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  // 初始化 Vue.config 对象
  Object.defineProperty(Vue, 'config', configDef)
 
  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  // 增加静态成员util对象
  // util中的工具方法不视作全局API的一部分,应当避免依赖它们
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
 
  // 增加静态方法 set/delete/nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick
 
  // 2.6 explicit observable API
  // 增加 observable 方法,用来将对象转换成响应式数据
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }
 
  // 初始化 options 对象
  Vue.options = Object.create(null)
  // ASSET_TYPES
  // 'component',
  // 'directive',
  // 'filter'
  // 为 Vue.options 初始化components/directives/filters
  // 分别保存全局的组件/指令/过滤器
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
 
  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  // 保存 Vue 构造函数到 options._base
  Vue.options._base = Vue
 
  // 注册内置组件 keep-alive 到全局 components
  extend(Vue.options.components, builtInComponents)
 
  // 注册 Vue.use() 用来注册插件
  initUse(Vue)
  // 注册 Vue.mixin() 实现混入
  initMixin(Vue)
  // 注册 Vue.extend() 基于传入的 options 返回一个组件的构造函数
  initExtend(Vue)
  // 注册 Vue.component()/Vue.directive()/Vue.filter()
  initAssetRegisters(Vue)
}

实例成员

src/core/instance/index.js 中初始化了绝大部分实例成员属性和方法

  • property
    • vm.$data
    • vm.$props
    • ...
  • 方法 / 数据
    • vm.$watch()

      • $watch() 没有对应的全局静态方法,因为需要用到实例对象vm

        Vue.prototype.$watch = function (
            expOrFn: string | Function,
            cb: any, // 可以是函数,也可以是对象
            options?: Object
          ): Function {
            // 获取 vue 实例
            const vm: Component = this
            if (isPlainObject(cb)) {
              // 判断 cb 如果是对象,执行 createWatcher
              return createWatcher(vm, expOrFn, cb, options)
            }
            options = options || {}
            // 标记是用户 watcher
            options.user = true
            // 创建用户 Watcher 对象
            const watcher = new Watcher(vm, expOrFn, cb, options)
            if (options.immediate) {
              // 判断 immediate 选项是 true,则立即执行 cb 回调函数
              // 不确定 cb 是否能正常执行,使用 try catch 进行异常处理
              try {
                cb.call(vm, watcher.value)
              } catch (error) {
                handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
              }
            }
            // 返回 unwatch 方法
            return function unwatchFn () {
              watcher.teardown()
            }
          }
        
    • vm.$set()

      • 同Vue.set()
    • vm.$delete()

      • 同Vue.delete()
  • 方法 / 事件
    • vm.$on()
      • 监听自定义事件
    • vm.$once()
      • 监听自定义事件,只触发一次
    • vm.$off()
      • 取消自定义事件的监听
    • vm.$emit()
      • 触发自定义事件
  • 方法 / 生命周期
    • vm.$mount()
      • 挂载DOM元素
      • runtime/index.js中添加,在入口js中重写
    • vm.$forceUpdate()
      • 强制重新渲染
    • vm.$nextTick()
      • 将回调延迟到下次 DOM 更新循环之后执行
    • vm.$destory()
      • 完全销毁一个实例
  • 其他
    • vm._init()
      • Vue实例初始化方法
      • 在Vue构造函数中调用了该初始化方法
    • vm._update()
      • 会调用vm._patch_方法更新 DOM 元素
    • vm._render()
      • 会调用用户初始化时选项传入的render函数(或者template转换成的render函数)
    • vm._patch_()
      • 用于把虚拟DOM转换成真实DOM
      • runtime/index.js中添加了该方法
// src/code/instance/index.js
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
 
// 注册 vm 的 _init() 方法,同时初始化 vm
initMixin(Vue)
// 注册 vm 的 $data/$props/$set()/$delete()/$watch()
stateMixin(Vue)
// 注册 vm 事件相关的成员及方法
// $on()/$off()/$once()/$emit()
eventsMixin(Vue)
// 注册 vm 生命周期相关的成员及方法
// _update()/$forceUpdate()/$destory()
lifecycleMixin(Vue)
// $nextTick()/_render()
renderMixin(Vue)
 
export default Vue
 

_init()

Vue的构造函数中会调用Vue实例的_init()方法来完成一些实例相关的初始化工作,并触发beforeCreatecreated生命周期钩子函数

// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    // 设置实例的uid
    vm._uid = uid++
 
    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }
 
    // a flag to avoid this being observed
    // 给实例对象添加_isVue属性,避免被转换成响应式对象
    vm._isVue = true
    // merge options
    // 合并构造函数中的options与用户传入的options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 初始化生命周期相关的属性
    // $children/$parent/$root/$refs
    initLifecycle(vm)
    // 初始化事件监听,父组件绑定在当前组件上的事件
    initEvents(vm)
    // 初始化render相关的属性与方法
    // $slots/$scopedSlots/_c/$createElement/$attrs/$listeners
    initRender(vm)
    // 触发 beforeCreate 生命周期钩子函数
    callHook(vm, 'beforeCreate')
    // 将 inject 的成员注入到 vm
    initInjections(vm) // resolve injections before data/props
    // 初始化 vm 的_props/methods/_data/computed/watch
    initState(vm)
    // 初始化 provide
    initProvide(vm) // resolve provide after data/props
    // 触发 created 生命周期钩子函数
    callHook(vm, 'created')
 
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
 
    if (vm.$options.el) {
      // 如果提供了挂载的 DOM 元素 el
      // 调用$mount() 挂载 DOM元素
      vm.$mount(vm.$options.el)
    }
  }

initState()

初始化 vm 的_props/methods/_data/computed/watch

// src/core/instance/state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 将 props 中成员转换成响应式的数据,并注入到 Vue 实例 vm 中
  if (opts.props) initProps(vm, opts.props)
  // 将 methods 中的方法注入到 Vue 的实例 vm 中
  // 校验 methods 中的方法名与 props 中的属性是否重名
  // 校验 methods 中的方法是否以 _ 或 $ 开头
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    // 将 data 中的属性转换成响应式的数据,并注入到 Vue 实例 vm 中
    // 校验 data 中的属性是否在 props 与 methods 中已经存在
    initData(vm)
  } else {
    // 如果没有提供 data 则初始化一个响应式的空对象
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化 computed
  if (opts.computed) initComputed(vm, opts.computed)
  // 初始化 watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

首次渲染过程

image-20201216233101117

数据响应式原理

Vue的响应式原理是基于观察者模式来实现的

响应式处理入口

Vue构造函数中调用了vm._init()

_init()函数中调用了initState()

initState()函数中如果传入的data有值,则调用initData(),并在最后调用了observe()

observe()函数会创建并返回一个Observer的实例,并将data转换成响应式数据,是响应式处理的入口

// src/core/observer/index.js
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 创建 Observer 实例
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  // 返回 Observer 实例
  return ob
}

Observer

Observer是响应式处理的核心类,用来对数组或对象做响应式的处理

在它的构造函数中初始化依赖对象,并将传入的对象的所有属性转换成响应式的getter/setter,如果传入的是数组,则会遍历数组的每一个元素,并调用observe() 创建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.
*/
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
    this.dep = new Dep()
    // 初始化实例计数器
    this.vmCount = 0
    // 使用 Object.defineProperty 将实例挂载到观察对象的 __ob__ 属性
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 数组的响应式处理
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 为数组每一个对象创建一个 observer 实例
      this.observeArray(value)
    } else {
      // 如果 value 是一个对象
      // 遍历对象所有属性并转换成 getter/setter
      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++) {
      // 转换成 getter/setter
      defineReactive(obj, keys[i])
    }
  }
 
  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    // 遍历数组所有元素,调用 observe
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

defineReactive

用来定义一个对象的响应式的属性,即使用Object.defineProperty来设置对象属性的 getter/setter

/**
* Define a reactive property on an Object.
*/
export function defineReactive (
  // 目标对象
  obj: Object,
  // 目标属性
  key: string,
  // 属性值
  val: any,
  // 自定义 setter 方法
  customSetter?: ?Function,
  // 是否深度观察
  // 为 false 时如果 val 是对象,也将转换成响应式
  shallow?: boolean
) {
  // 创建依赖对象实例,用于收集依赖
  const dep = new Dep()
 
  // 获取目标对象 obj 的目标属性 key 的属性描述符对象
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果属性 key 存在,且属性描述符 configurable === false
  // 则该属性无法通过 Object.defineProperty来重新定义
  if (property && property.configurable === false) {
    return
  }
 
  // cater for pre-defined getter/setters
  // 获取用于预定义的 getter 与 setter
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    // 如果调用时只传入了2个参数(即没传入val),且没有预定义的getter,则直接通过 obj[key] 获取 val
    val = obj[key]
  }
 
  // 判断是否深度观察,并将子对象属性全部转换成 getter/setter,返回子观察对象
  let childOb = !shallow && observe(val)
  // 使用 Object.defineProperty 定义 obj 对象 key 属性的 getter 与 setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 如果存在预定义的 getter 则 value 等于 getter 调用的返回值
      // 否则 value 赋值为属性值 val
      const value = getter ? getter.call(obj) : val     
      if (Dep.target) {
        // 当前存在依赖目标则建立依赖
        dep.depend()
        if (childOb) {
          // 如果子观察目标存在,则建立子依赖
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // 如果属性是数组,则处理数组元素的依赖收集
            // 调用数组元素 e.__ob__.dep.depend()
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 如果存在预定义的 getter 则 value 等于 getter 调用的返回值
      // 否则 value 赋值为属性值 val
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 判断新值旧值是否相等
      // (newVal !== newVal && value !== value) 是对 NaN 这个特殊值的判断处理(NaN !== NaN)
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      // 有预定义 getter 但没有 setter 直接返回
      if (getter && !setter) return
      if (setter) {
        // 有预定义 setter 则调用 setter
        setter.call(obj, newVal)
      } else {
        // 否则直接更新新值
        val = newVal
      }
      // 判断是否深度观察,并将新赋值的子对象属性全部转换成 getter/setter,返回子观察对象
      childOb = !shallow && observe(newVal)
      // 触发依赖对象的 notify() 派发通知所有依赖更新
      dep.notify()
    }
  })
}

依赖收集

依赖收集由Dep对象来完成

每个需要收集依赖的对象属性,都会创建一个相应的dep实例,并收集watchers保存到其subs数组中

对象响应式属性的依赖收集,主要是getter中的这部分代码

    if (Dep.target) {
        // 当前存在依赖目标则建立依赖
        dep.depend()
        if (childOb) {
          // 如果子观察目标存在,则建立子依赖
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // 如果属性是数组,则处理数组元素的依赖收集
            // 调用数组元素 e.__ob__.dep.depend()
            dependArray(value)
          }
        }
     }

这里有两个问题

  • Dep.target 是何时赋值的?

    在mountComponent()调用时,Watcher被实例化

    Watcher构造函数中调用了实例方法get(),并通过pushTarget() 将Watcher实例赋值给Dep.target

  • dep.depend() 的依赖收集进行了什么操作?

    dep.depend()会调用Dep.target.addDep()方法,并调用dep.addSub()方法,将Watcher实例添加到观察者列表subs中

    Watcher中会维护dep数组与dep.id集合,当调用addDep()方法时,会先判断dep.id是否已经在集合中,从而避免重复收集依赖

数组

数组的成员无法像对象属性一样通过Object.defineProperty()去设置 getter/setter 来监视变化,因此数组的响应式需要进行特殊的处理,通过对一系列会影响数组成员数量的原型方法进行修补,添加依赖收集与更新派发,来完成响应式处理

影响数组的待修补方法arrayMethods

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse
    // Observer 构造函数中的处理
    if (Array.isArray(value)) {
      // 数组的响应式处理
      if (hasProto) {
        // 如果支持原型, 替换原型指向 __prototype__ 为修补后的方法
        protoAugment(value, arrayMethods)
      } else {
        // 如果不支持原型,通过 Object.defineProperty 为数组重新定义修补后的方法
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 为数组每一个对象创建一个 observer 实例
      this.observeArray(value)
    }
// src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
 
// 影响数组的待修补方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
 
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
  // cache original method
  // 缓存数组原型上原始的处理函数
  const original = arrayProto[method]
  // 通过 Object.defineProperty 为新创建的数组原型对象定义修补后的数组处理方法
  def(arrayMethods, method, function mutator (...args) {
    // 先执行数组原型上原始的处理函数并将结果保存到 result 中
    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
    }
    // 如果新增, 调用observe()将新增的成员转化成响应式
    if (inserted) ob.observeArray(inserted)
    // notify change
    // 调用依赖的 notify() 方法派发更新,通知观察者 Watcher 执行相应的更新操作
    ob.dep.notify()
    // 返回结果
    return result
  })
})

通过查看数组响应式处理的源码我们可以发现,除了通过修补过的七个原型方法来修改数组内容外,其他方式修改数组将不能触发响应式更新,例如通过数组下标来修改数组成员array[0] = xxx,或者修改数组长度array.length = 1

Watcher

Vue中的Watcher有三种

  • Computed Watcher

    • Computed Watcher是在Vue构造函数初始化调用_init() -> initState() -> initComputed() 中创建的
  • 用户Watcher(侦听器)

    • 用户Watcher是在Vue构造函数初始化调用_init() -> initState() -> initWatch() 中创建的(晚于Computed Watcher)
  • 渲染Watcher

    • 渲染Watcher是在Vue初始化调用_init() -> vm.$mount() -> mountComponent()的时候创建的(晚于用户Watcher)

        // src/core/instance/lifecycle.js
      
        // we set this to vm._watcher inside the watcher's constructor
        // since the watcher's initial patch may call $forceUpdate (e.g. inside child
        // component's mounted hook), which relies on vm._watcher being already defined
        // 渲染 Watcher 的创建
        // updateComponent 方法用于调用 render 函数并最终通过 patch 更新 DOM
        // isRenderWatcher 标记参数为 true
        new Watcher(vm, updateComponent, noop, {
          before () {
            if (vm._isMounted && !vm._isDestroyed) {
              callHook(vm, 'beforeUpdate')
            }
          }
        }, true /* isRenderWatcher */)
      
  • Watcher的实现

    /**
     * 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.
     */
    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) {
          // 如果是渲染 watcher,记录到 vm._watcher
          vm._watcher = this
        }
        // 记录 watcher 实例到 vm._watchers 数组中
        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 {
          // 渲染 watcher 不传 options
          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
        // watcher 相关 dep 依赖对象
        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
        // expOrFn 的值是函数或字符串
        if (typeof expOrFn === 'function') {
          // 是函数时,直接赋给 getter
          this.getter = expOrFn
        } else {
          // 是字符串时,是侦听器中监听的属性名,例如 watch: { 'person.name': function() {}}
          // parsePath('person.name') 返回一个获取 person.name 的值的函数
          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
            )
          }
        }
        // 渲染 watcher 的 lazy 是 false, 会立即执行 get()
        // 计算属性 watcher 的lazy 是 true,在 render 时才会获取值
        this.value = this.lazy
          ? undefined
          : this.get()
      }
    
      /**
       * Evaluate the getter, and re-collect dependencies.
       */
      get () {
        // 组件 watcher 入栈
        // 用于处理父子组件嵌套的情况
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          // 执行传入的 expOrFn
          // 渲染 Watcher 传入的是 updateComponent 函数,会调用 render 函数并最终通过 patch 更新 DOM
          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)
          }
          // 组件 watcher 实例出栈
          popTarget()
          // 清空依赖对象相关的内容
          this.cleanupDeps()
        }
        return value
      }
      ...
    }
    

总结

响应式处理过程总结

image-20201228185240923
  • 整个响应式处理过程是从Vue初始化_init()开始的

    • initState() 初始化vue实例的状态,并调用initData()初始化data属性
    • initData() 将data属性注入vm实例,并调用observe()方法将data中的属性转换成响应式的
    • observe() 是响应式处理的入口
  • observe(value)

    • 判断value是否是对象,如果不是对象直接返回
    • 判断value对象是否有__ob__属性,如果有直接返回(认为已进行过响应式转换)
    • 创建并返回observer对象
    image-20201228185835411
  • Observer

    • 为value对象(通过Object.defineProperty)定义不可枚举的__ob__属性,用来记录当前的observer对象
    • 区分是数组还是对象,并进行相应的响应式处理
    image-20201228185941768
  • defineReactive

    • 为每一个对象属性创建dep对象来收集依赖
    • 如果当前属性值是对象,调用observe将其转换成响应式
    • 为对象属性定义getter与setter
    • getter
      • 通过dep收集依赖
      • 返回属性值
    • setter
      • 保存新值
      • 调用observe()将新值转换成响应式
      • 调用dep.notify()派发更新通知给watcher,调用update()更新内容到DOM
    image-20201228190416199
  • 依赖收集

    • 在watcher对象的get()方法中调用pushTarget
      • 将Dep.target赋值为当前watcher实例
      • 将watcher实例入栈,用来处理父子组件嵌套的情况
    • 访问data中的成员的时候,即defineReactive为属性定义的getter中收集依赖
    • 将属性对应的watcher添加到dep的subs数组中
    • 如果有子观察对象childOb,给子观察对象收集依赖
    image-20201228190600536
  • Watcher

    • 数据触发响应式更新时,dep.notify()派发更新调用watcher的update()方法
    • queueWatcher()判断watcher是否被处理,如果没有的话添加queue队列中,并调用flushSchedulerQueue()
    • flushSchedulerQueue()
      • 触发beforeUpdate钩子函数
      • 调用watcher.run()
        • run() -> get() -> getter() -> updateComponent()
      • 清空上一次的依赖
      • 触发actived钩子函数
      • 触发updated钩子函数
    QQ截图20201228205942

全局API

为一个响应式对象动态添加一个属性,该属性是否是响应式的?不是

为一个响应式对象动态添加一个响应式属性,可以使用Vue.set()vm.$set()来实现

Vue.set()

用于向一个响应式对象添加一个属性,并确保这个新属性也是响应式的,且触发视图更新

注意:对象不能是Vue实例vm,或者Vue实例的根数据对象vm.$data

  • 示例

    // object
    Vue.set(object, 'name', 'hello') 
    // 或 
    vm.$set(object, 'name', 'hello')
    
    // array
    Vue.set(array, 0, 'world') 
    // 或 
    vm.$set(array, 0, 'world')
    
  • 定义位置

    core/global-api/index.js

  • 源码解析

    /**
     * Set a property on an object. Adds the new property and
     * triggers change notification if the property doesn't
     * already exist.
     */
    export function set (target: Array<any> | Object, key: any, val: any): any {
      if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
      ) {
        warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      // 判断目标对象 target 是否是数组,且参数 key 是否是合法的数组索引
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        // 通过 splice 对 key 位置的元素进行替换
        // 数组的 splice 方法已经在Vue初始化时中完成了响应式补丁处理 (array.js)
        target.splice(key, 1, val)
        return val
      }
      // 如果 key 在目标对象 target 中存在,且不是原型上的成员,则直接赋值(已经是响应式的)
      if (key in target && !(key in Object.prototype)) {
        target[key] = val
        return val
      }
      // 获取目标对象 target 的 __ob__ 属性
      const ob = (target: any).__ob__
      // 判断 target 是否是 Vue 实例,或者是否是 $data (vmCount === 1) 并抛出异常
      if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Avoid adding reactive properties to a Vue instance or its root $data ' +
          'at runtime - declare it upfront in the data option.'
        )
        return val
      }
      // 判断 target 是否为响应式对象 (ob是否存在)
      // 如果是普通对象则不做响应式处理直接返回
      if (!ob) {
        target[key] = val
        return val
      }
      // 调用 defineReactive 为目标对象添加响应式属性 key 值为 val
      defineReactive(ob.value, key, val)
      // 发送通知更新视图
      ob.dep.notify()
      return val
    }
    

Vue.delete()

用于删除对象的属性,如果对象是响应式的,确保删除能触发视图更新

主要用于避开Vue不能检测到属性被删除的限制,但是很少会使用到

注意:对象不能是Vue实例vm,或者Vue实例的根数据对象vm.$data

  • 示例

    Vue.delete(object, 'name')
    // 或
    vm.$delete(object, 'name')
    
  • 定义位置

    core/global-api/index.js

  • 源码解析

    /**
     * Delete a property and trigger change if necessary.
     */
    export function del (target: Array<any> | Object, key: any) {
      if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
      ) {
        warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      // 判断目标对象 target 是否是数组,且参数 key 是否是合法的数组索引
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        // 通过 splice 删除 key 位置的元素
        // 数组的 splice 方法已经在Vue初始化时中完成了响应式补丁处理 (array.js)
        target.splice(key, 1)
        return
      }
      // 获取目标对象 target 的 __ob__ 属性
      const ob = (target: any).__ob__
      // 判断 target 是否是 Vue 实例,或者是否是 $data (vmCount === 1) 并抛出异常
      if (target._isVue || (ob && ob.vmCount)) {
        process.env.NODE_ENV !== 'production' && warn(
          'Avoid deleting properties on a Vue instance or its root $data ' +
          '- just set it to null.'
        )
        return
      }
      // 判断目标对象 target 是否包含属性 key
      // 如果不包含则直接返回
      if (!hasOwn(target, key)) {
        return
      }
      // 删除目标对象 target 的属性 key
      delete target[key]
      // 判断 target 是否为响应式对象 (ob是否存在)
      // 如果是普通对象则直接返回
      if (!ob) {
        return
      }
      // 发送通知更新视图
      ob.dep.notify()
    }
    

Vue.nextTick()

Vue更新DOM是批量异步执行的,当通过响应式方式触发DOM更新但没有完成时,无法立即获取更新后的DOM

在修改数据后立即使用nextTick()方法可以在下次DOM更新循环结束后,执行延迟回调,从而获得更新后的DOM

  • 示例

    Vue.nextTick(function(){})
    // 或
    vm.$nextTick(function(){})
    
  • 定义位置

    • 实例方法

      core/instance/render.js -> core/util/next-tick.js

    • 静态方法

      core/global-api/index.js -> core/util/next-tick.js

  • 源码解析

    export function nextTick (cb?: Function, ctx?: Object) {
      // 声明 _resolve 用来保存 cb 未定义时返回新创建的 Promise 的 resolve
      let _resolve
      // 将回调函数 cb 加上 try catch 异常处理存入 callbacks 数组
      callbacks.push(() => {
        if (cb) {
          // 如果 cb 有定义,则执行回调
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          // 如果 _resolve 有定义,执行_resolve
          _resolve(ctx)
        }
      })
      if (!pending) {
        pending = true
        // nextTick() 的核心
        // 尝试在本次事件循环之后执行 flushCallbacks
        // 如果支持 Promise 则优先尝试使用 Promsie.then() 的方式执行微任务
        // 否则非IE浏览器环境判断是否支持 MutationObserver 并使用 MutationObserver 来执行微任务
        // 尝试使用 setImmediate 来执行宏任务(仅IE浏览器支持,但性能好于 setTimeout)
        // 最后尝试使用 setTimeout 来执行宏任务
        timerFunc()
      }
      // $flow-disable-line
      // cb 未定义且支持 Promise 则返回一个新的 Promise,并将 resolve 保存到 _resolve 备用
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    
    
    // timerFunc()
    
    // Here we have async deferring wrappers using microtasks.
    // In 2.5 we used (macro) tasks (in combination with microtasks).
    // However, it has subtle problems when state is changed right before repaint
    // (e.g. #6813, out-in transitions).
    // Also, using (macro) tasks in event handler would cause some weird behaviors
    // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
    // So we now use microtasks everywhere, again.
    // A major drawback of this tradeoff is that there are some scenarios
    // where microtasks have too high a priority and fire in between supposedly
    // sequential events (e.g. #4521, #6690, which have workarounds)
    // or even between bubbling of the same event (#6566).
    let timerFunc
    
    // The nextTick behavior leverages the microtask queue, which can be accessed
    // via either native Promise.then or MutationObserver.
    // MutationObserver has wider support, however it is seriously bugged in
    // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
    // completely stops working after triggering a few times... so, if native
    // Promise is available, we will use it:
    /* istanbul ignore next, $flow-disable-line */
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      const p = Promise.resolve()
      timerFunc = () => {
        p.then(flushCallbacks)
        // In problematic UIWebViews, Promise.then doesn't completely break, but
        // it can get stuck in a weird state where callbacks are pushed into the
        // microtask queue but the queue isn't being flushed, until the browser
        // needs to do some other work, e.g. handle a timer. Therefore we can
        // "force" the microtask queue to be flushed by adding an empty timer.
        if (isIOS) setTimeout(noop)
      }
      isUsingMicroTask = true
    } else if (!isIE && typeof MutationObserver !== 'undefined' && (
      isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) {
      // Use MutationObserver where native Promise is not available,
      // e.g. PhantomJS, iOS7, Android 4.4
      // (#6466 MutationObserver is unreliable in IE11)
      let counter = 1
      const observer = new MutationObserver(flushCallbacks)
      const textNode = document.createTextNode(String(counter))
      observer.observe(textNode, {
        characterData: true
      })
      timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter)
      }
      isUsingMicroTask = true
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      // Fallback to setImmediate.
      // Technically it leverages the (macro) task queue,
      // but it is still a better choice than setTimeout.
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else {
      // Fallback to setTimeout.
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容