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

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

作者: 彪悍de文艺青年 | 来源:发表于2020-12-28 22:59 被阅读0次

    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)
        }
      }
      

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/qckxoktx.html