Vue2.x源码解读-响应式原理剖析

准备工作

  1. 下载vue源码,可以先将vue项目fork到自己的github仓库,然后在clone自己仓库的vue,这样在解读源码的时候可以随时添加注释,并将注释提交到自己的仓库。

  2. 源码代码主要结构说明:

    1. dist:打包生成的文件
    2. examples:实例代码目录
    3. src:源码文件目录
      1. compiler:编译器相关代码,把template模板转化成render函数
      2. core:核心代码
        1. components:定义vue自带的keep-alive组件
        2. global-api:定义vue中的静态方法,包括mixinextenduse
        3. instance:创建vue实例成员,包括构造函数、初始化和生命周期函数。
        4. observer:响应式实现
        5. util:工具方法
        6. vdom:虚拟DOM实现,重写了Snabbdom,增加了组件的机制。
      3. platforms:平台相关处理
        1. webweb平台
        2. weex:基于vue的移动端框架
      4. server:服务器端渲染
      5. sfc:单文件组件,将单文件组件转换成js模块
  3. Vue2.x使用了Flow来进行代码静态类型检查。

  4. Vue使用rollup进行打包,相对于webpack来说,rollup打包不会生成冗余代码。rollup命令行参数说明:

    1. -w:开启监视模式

    2. -c:指定配置文件

    3. --sourcemap:开启 sourceMap 功能,之后能在浏览器中的源码中看到源码对应的src文件夹
      Vue2.x源码解读-响应式原理剖析

    4. --environment:指定运行环境参数,TARGET指定打包生成的版本。

      "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
      
  5. Vue构建版本之间的差别
    Vue2.x源码解读-响应式原理剖析
    术语说明:
    完整版(Full): 同时包含编译器运行时的版本
    编译器: 将模板字符串编译成 javascript 渲染函数的代码
    运行时(Runtime-only): 用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码。基本上就是除去编译器的其它一切。运行时版本体积小,运行效率更高。
    UMD: 可以通过 <script> 标签直接用在浏览器中。
    CommonJS: CommonJS 版本用来配合老的打包工具比如 Browserifywebpack 1。这些打包工具的默认文件 (pkg.main) 是只包含运行时的 CommonJS 版本
    ES Module: 会提供两个 ES Modules (ESM) 构建文件:

    1. 为打包工具提供的 ESM:为诸如 webpack 2Rollup 提供的现代打包工具。ESM 格式被设计为可以被静态分析(编译时解析模块依赖),所以打包工具可以利用这一点来进行tree-shaking并将用不到的代码排除出最终的包。为这些打包工具提供的默认文件 pkg.module 是只有运行时的 ES Module 构建 (vue.runtime.esm.js)
    2. 为浏览器提供的 ESM (2.6+):用于在现代浏览器中通过 <script type="module"> 直接导入。
  6. 寻找入口文件(以 dev 命令为例)

    1. 根据package.json中的命令行找到配置文件
      Vue2.x源码解读-响应式原理剖析

    2. 找到配置文件中最后的导出语句,然后向前解析找到最后导出的内容

      // 判断环境变量是否有 TARGET
      // 如果有的话 使用 genConfig() 生成 rollup 配置文件
      if (process.env.TARGET) { // 如果有 TARGET 则打包生成指定的目标版本
        module.exports = genConfig(process.env.TARGET)
      } else { // 如果没有则打包生成所有版本
        exports.getBuild = genConfig
        exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
      }
      

      这里关键是是genConfig方法,查看方法,定位到buildsbuilds存储了不同TARGET对应的配置。

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

      resolve是用来解析模块路径中可能存在的alias别名,并返回一个绝对路径。

      const resolve = p => {
        // 根据路径中的前半部分去alias中找别名
        const base = p.split('/')[0]
        if (aliases[base]) {
          return path.resolve(aliases[base], p.slice(base.length + 1))
        } else {
          return path.resolve(__dirname, '../', p)
        }
      }
      

      alias别名配置

      module.exports = {
        vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
        compiler: resolve('src/compiler'),
        core: resolve('src/core'),
        shared: resolve('src/shared'),
        web: resolve('src/platforms/web'),
        weex: resolve('src/platforms/weex'),
        server: resolve('src/server'),
        sfc: resolve('src/sfc')
      }
      
    3. 根据配置找到入口文件,从入口文件开始解读。

  7. 按照上面的方法找到入口文件entry-runtime-with-compiler.js,通过阅读入口文件了解到该文件的主要功能是重写Vue$mount方法,用来渲染DOM。通过源码的阅读可以知道以下几处注意事项:

    1. Vue实例创建时传入的el不能是bodyhtml

      // el 不能是 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
      }
      
    2. 创建Vue实例时如果同时传入了templaterender,则会忽略template
      Vue2.x源码解读-响应式原理剖析

    3. 通过调试确定$mount是在什么时候使用的。编译时开启sourceMap,直接在浏览器中定位到入口文件,并打下断点,刷新浏览器,运行到断点处,可以看到右侧Call Stack调用栈中当前正在执行Vue.$mount,下面一行则是调用Vue.$mount的运行环境,即在Vue.init中,一直向下可以追溯到Vue实例的创建的执行环境。在调用栈中可以查看函数的调用过程。
      Vue2.x源码解读-响应式原理剖析

  8. 根据导入的Vue查看runtime/index.js

    1. vue.config定义一些属性。

      // 判断是否是关键属性(表单元素的 input/checked/selected/muted)
      // 如果是这些属性,设置el.props属性(属性不设置到标签上)
      Vue.config.mustUseProp = mustUseProp
      Vue.config.isReservedTag = isReservedTag
      Vue.config.isReservedAttr = isReservedAttr
      Vue.config.getTagNamespace = getTagNamespace
      Vue.config.isUnknownElement = isUnknownElement
      
    2. 定义平台环境上的指令(v-show、v-modal)和组件(Transition,TransitionGroup)

      // install platform runtime directives & components
      extend(Vue.options.directives, platformDirectives)
      extend(Vue.options.components, platformComponents)
      
    3. 定义patch方法

      Vue.prototype.__patch__ = inBrowser ? patch : noop
      
    4. 定义原型上的$mount方法

      // public mount method
      Vue.prototype.$mount = function (
        el?: string | Element,
        hydrating?: boolean
      ): Component {
        el = el && inBrowser ? query(el) : undefined
        return mountComponent(this, el, hydrating) // 渲染组件
      }
      
    5. 执行devtoolsinit钩子,并对运行环境的不足做出提示。

      if (inBrowser) {
        setTimeout(() => {
          if (config.devtools) {
            if (devtools) {
              devtools.emit('init', Vue)
            } else if (
              process.env.NODE_ENV !== 'production' &&
              process.env.NODE_ENV !== 'test'
            ) {
              console[console.info ? 'info' : 'log'](
                'Download the Vue Devtools extension for a better development experience:\n' +
                'https://github.com/vuejs/vue-devtools'
              )
            }
          }
          if (process.env.NODE_ENV !== 'production' &&
            process.env.NODE_ENV !== 'test' &&
            config.productionTip !== false &&
            typeof console !== 'undefined'
          ) {
            console[console.info ? 'info' : 'log'](
              `You are running Vue in development mode.\n` +
              `Make sure to turn on production mode when deploying for production.\n` +
              `See more tips at https://vuejs.org/guide/deployment.html`
            )
          }
        }, 0)
      }
      
  9. Vue的初始化:通过runtime/index.js中依赖的追溯,找到core/index.js,这个文件中定义了Vue的初始化的过程。

    1. 初始化Vue的静态成员。

      initGlobalAPI(Vue)
      
      1. 初始化Vue.config对象

          // 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)
        
      2. 暴露util方法,但不将其作为Vue API的一部分,尽量不要依赖他们

          // exposed util methods.
          // NOTE: these are not considered part of the public API - avoid relying on
          // them unless you are aware of the risk.
          // 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们
          Vue.util = {
            warn,
            extend,
            mergeOptions,
            defineReactive
          }
        
      3. 定义静态 setdelnextTick方法

          // 静态方法 set/delete/nextTick
          Vue.set = set
          Vue.delete = del
          Vue.nextTick = nextTick
        

        set为对象设置为响应式的属性,实现原理解析:

        function set (target: Array<any> | Object, key: any, val: any): any {}
        
        1. 确保 target 是对象或数组

          if (process.env.NODE_ENV !== 'production' &&
              (isUndef(target) || isPrimitive(target))
            ) {
              warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
            }
          
        2. target是数组时,确保key是有效索引,并扩展数组长度,使数组包含key索引,通过splice方法设置值。这里的splice方法是经过处理的方法,在方法内部会调用dep.notify()发送通知。

          // 判断 target 是否是数组,key 是否是合法的索引
            if (Array.isArray(target) && isValidArrayIndex(key)) {
              target.length = Math.max(target.length, key) // 扩展数组长度
              // 通过 splice 对key位置的元素进行替换
              // splice 在 array.js 进行了响应化的处理
              target.splice(key, 1, val)
              return val
            }
          
        3. keytarget对象中的属性,直接赋值。

          // 如果 key 在对象中已经存在直接赋值
          if (key in target && !(key in Object.prototype)) {
            target[key] = val
            return val
          }
          
        4. 如果targetvue实例或$data,直接返回

          // 获取 target 中的 observer 对象
            const ob = (target: any).__ob__
            // 如果 target 是 vue 实例或者 $data 直接返回
            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
            }
          
        5. target不是响应式的对象,直接赋值

          // 如果 ob 不存在,target 不是响应式对象直接赋值
            if (!ob) {
              target[key] = val
              return val
            }
          
        6. target是响应式的对象,将key设置为响应式的属性,并发送通知,返回val

            // 把 key 设置为响应式属性
            defineReactive(ob.value, key, val)
            // 发送通知
            ob.dep.notify()
            return val
          

        del方法删除对象或数组的属性,并发送通知更新视图,对象不能是Vue实例或Vue实例的根数据对象。实现和set方法类似就不在赘述了。
        nextTickDOM更新完之后调用传入的回调函数,代码解析:

        function nextTick (cb?: Function, ctx?: Object) {}
        
        1. cb做异步处理,并保存到回调数组中
          callbacks.push(() => {
              if (cb) {
                try {
                  // 调用 cb()
                  cb.call(ctx)
                } catch (e) {
                  handleError(e, ctx, 'nextTick')
                }
              } else if (_resolve) {
                _resolve(ctx)
              }
            })
          
        2. 如果不是pending状态(即正在执行回调),这挨个调用回调数组中的函数。
            if (!pending) {
              pending = true
              // 调用
              timerFunc()
            }
          
        3. 回调数组中的函数的调用是放在Promise、MutationObserver、setImmediate或setTimeout中执行的,首先会优先考虑使用微任务执行,如果不能使用则降级到宏任务中执行。保证在DOM更新完成之后执行。浏览器视图更新和DOM树上的数据更新是两个步骤,DOM树的数据更新是同步进行了,而视图更新则是异步的。
          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
            // MutationObserver用于监听DOM树的变化,并执行回调,也是微任务。
            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) //setimemediate性能比setTimeout好,会在setTimeout之前执行。
            }
          } else {
            // Fallback to setTimeout.
            timerFunc = () => {
              setTimeout(flushCallbacks, 0)
            }
          } 
          
        4. 如果没有传入cb,则返回一个Promise,该Promise将在回调数组中的函数执行完成之后变为resolve状态。
            // $flow-disable-line
            if (!cb && typeof Promise !== 'undefined') {
              // 返回 promise 对象
              return new Promise(resolve => {
                _resolve = resolve
              })
            }
          
      4. 定义observable方法

          // 2.6 explicit observable API
          // 让一个对象可响应
          Vue.observable = <T>(obj: T): T => {
            observe(obj)
            return obj
          }
        
      5. 初始化options对象,定义并初始化其中的componentsdirectivesfilters属性

          // 初始化 Vue.options 对象,并给其扩展
          // components/directives/filters
          Vue.options = Object.create(null)
          ASSET_TYPES.forEach(type => {
            Vue.options[type + 's'] = Object.create(null)
          })
        
      6. 保存Vue构造函数,

          // 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
        
      7. 设置Vuekeep-alive组件

        // 设置 keep-alive 组件
        extend(Vue.options.components, builtInComponents)
        
      8. 注册Vue.use()
        Vue.use是用来注册第三方插件的,第三方插件要求必须是对象或函数,如果是对象必须具有install方法。

        1. 首先需要初始化保存插件的列表,并判断是否已经注册该插件。
          // 保存安装的组件
          const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
          if (installedPlugins.indexOf(plugin) > -1) {
            return this
          }
          
        2. 执行插件提供的install方法或插件函数。 这里需要对参数进行处理,install执行时第一个参数是Vue的构造函数。
          // additional parameters
          // 如果有多个参数,把数组中的第一个元素(plugin)去除,第一个参数是plugin,剩余的参数是plugin执行时需要传入的参数。
          const args = toArray(arguments, 1)
          // 把this(Vue)插入第一个元素的位置,这里的this也是Vue的构造函数
          args.unshift(this)
          if (typeof plugin.install === 'function') {
            plugin.install.apply(plugin, args) // install 执行时第一个参数是Vue的构造函数。
          } else if (typeof plugin === 'function') {
            plugin.apply(null, args)
          }
          installedPlugins.push(plugin)
          return this
          
      9. 注册Vue.mixin()

      10. 注册Vue.extend()
        Vue.extend() 返回一个Vue的子类,接受一个包含组件选项对象的参数。代码解析:

        1. 尝试从缓存中加载组件的构造函数

          // Vue 构造函数
          const Super = this
          const SuperId = Super.cid
          // 从缓存中加载组件的构造函数
          const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
          if (cachedCtors[SuperId]) {
            return cachedCtors[SuperId]
          }
          
          const name = extendOptions.name || Super.options.name
          if (process.env.NODE_ENV !== 'production' && name) {
            // 如果是开发环境验证组件的名称
            validateComponentName(name)
          }
          
        2. 定义子类的构造函数,设置原型继承Vue

          const Sub = function VueComponent (options) {
          // 调用 _init() 初始化
          this._init(options)
          }
          // 原型继承自 Vue
          Sub.prototype = Object.create(Super.prototype)
          Sub.prototype.constructor = Sub
          Sub.cid = cid++
          
        3. 合并options选项

          // 合并 options
          Sub.options = mergeOptions(
            Super.options,
            extendOptions
          )
          Sub['super'] = Super
          
        4. options中的propscomputed转化为响应式。

          if (Sub.options.props) {
            initProps(Sub)
          }
          if (Sub.options.computed) {
            initComputed(Sub)
          }
          
        5. 复制Vue中的extendmixinusecomponentdirectivefilter属性到子类

          // allow further extension/mixin/plugin usage
          Sub.extend = Super.extend
          Sub.mixin = Super.mixin
          Sub.use = Super.use
          
          // create asset registers, so extended classes
          // can have their private assets too.
          ASSET_TYPES.forEach(function (type) {
            Sub[type] = Super[type]
          })
          
        6. 保存子类的构造函数

          if (name) {
           Sub.options.components[name] = Sub
          }
          
        7. 保存配置选项副本,方便后续配置修改之后检查

          // keep a reference to the super options at extension time.
          // later at instantiation we can check if Super's options have
          // been updated.
          Sub.superOptions = Super.options
          Sub.extendOptions = extendOptions
          Sub.sealedOptions = extend({}, Sub.options)
          
        8. 将组件的构造函数缓存到 options._Ctor

          // cache constructor
          // 把组件的构造函数缓存到 options._Ctor
          cachedCtors[SuperId] = Sub
          return Sub
          
      11. 注册Vue.directive()Vue.component()Vue.filter()
        注册或返回全局的指令、组件和过滤器,这三个方法的功能和实现是类似的,所以放在一起定义。这里主要两部分

        1. 如果只传入了id,则返回对应的指令、组件或过滤器
        2. 如果传入了第二个参数,则将第二个参数转换成合适的格式保存到Vue.directivesVue.componentsVue.filters中,并返回。
          // 遍历 ASSET_TYPES 数组,为 Vue 定义相应方法
            // ASSET_TYPES 包括了directive、 component、filter
            ASSET_TYPES.forEach(type => {
              Vue[type] = function (
                id: string,
                definition: Function | Object
              ): Function | Object | void {
                if (!definition) {
                  return this.options[type + 's'][id]
                } else {
                  /* istanbul ignore if */
                  if (process.env.NODE_ENV !== 'production' && type === 'component') {
                    validateComponentName(id)
                  }
                  // Vue.component('comp', { template: '' })
                  if (type === 'component' && isPlainObject(definition)) {
                    definition.name = definition.name || id
                    // 把组件配置转换为组件的构造函数 this.options._base = Vue
                    definition = this.options._base.extend(definition)
                  }
                  if (type === 'directive' && typeof definition === 'function') {
                    definition = { bind: definition, update: definition }
                  }
                  // 全局注册,存储资源并赋值
                  // this.options['components']['comp'] = definition
                  this.options[type + 's'][id] = definition
                  return definition
                }
              }
            })
          
        // 注册 Vue.use() 用来注册插件
        initUse(Vue)
         // 注册 Vue.mixin() 实现混入
         initMixin(Vue)
         // 注册 Vue.extend() 基于传入的options返回一个组件的构造函数
         initExtend(Vue)
         // 注册 Vue.directive()、 Vue.component()、Vue.filter()
         initAssetRegisters(Vue)
        
    2. 定义服务端渲染的属性

      // 服务端渲染的处理
      Object.defineProperty(Vue.prototype, '$isServer', {
        get: isServerRendering
      })
      
      Object.defineProperty(Vue.prototype, '$ssrContext', {
        get () {
          /* istanbul ignore next */
          return this.$vnode && this.$vnode.ssrContext
        }
      })
      
      // expose FunctionalRenderContext for ssr runtime helper installation
      Object.defineProperty(Vue, 'FunctionalRenderContext', {
        value: FunctionalRenderContext
      })
      
  10. core/index.js依赖instance/index.js,在此处定义Vue的构造函数,并定义定义Vue的实例成员。

    1. 定义构造函数,在构造函数中调用_init()方法。 这里定义构造函数而不是class是为了方便下面通过混入定义实例成员。

      // 此处不用 class 的原因是因为方便后续给 Vue 实例混入实例成员
      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')
        }
        // 调用 _init() 方法
        this._init(options)
      }
      
    2. 调用initMixin(Vue)定义_init方法。

      1. 合并选项参数,将传入的optionsVue构造函数的options(即runtime/index.js中定义的options)合并。

      2. 调用beforeCreatecreated生命周期函数,并进行一系列初始化。

        vm._self = vm
         // vm 的生命周期相关变量初始化
         // $children/$parent/$root/$refs
         initLifecycle(vm)
         // vm 的事件监听初始化, 父组件绑定在当前组件上的事件
         initEvents(vm)
         // 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')
        
      3. 调用$mount挂载元素。

    3. 调用stateMixin(Vue)定义state相关的实例成员,包括$data$props$set$delete$watch

    4. 调用eventsMixin(Vue)初始化事件相关的方法,包括$on$once$off$emit

    5. 调用lifecycleMixin(Vue)初始化生命周期相关的方法,包括_update$forceUpdate$destroy

    6. 调用renderMixin(Vue)定义render相关的方法,包括$nextTick_render

数据响应式原理

  1. 入口:在调用_init方法进行初始化的时候,会调用initState方法进行state相关的初始化,在其中对data进行初始化时,主要是调用observe方法将data中的属性转换为响应式的属性,observe方法的核心这是创建一个Observer类。

  2. Observer类: 将对象的所有属性转化为setter/getter,并且收集依赖,派发更新(即发送消息,通知更新)。

    1. 初始化依赖对象,将实例挂载到__ob__属性上
    2. 判断是否是数组,如果是数组,则对数组中的每个元素调用observe方法,将其转化为响应式对象。
    3. 如果是对象,则遍历对象属性,调用defineReactive方法将属性转化为setter/getter
    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()
        // 初始化实例的 vmCount 为0
        this.vmCount = 0
        // 将实例挂载到观察对象的 __ob__ 属性
        def(value, '__ob__', this)
        // 数组的响应式处理
        if (Array.isArray(value)) {
          if (hasProto) {
            protoAugment(value, arrayMethods)
          } else {
            copyAugment(value, arrayMethods, arrayKeys)
          }
          // 为数组中的每一个对象创建一个 observer 实例
          this.observeArray(value)
        } else {
          // 遍历对象中的每一个属性,转换成 setter/getter
          this.walk(value)
        }
      }
    
      /**
       * Walk through all properties and convert them into
       * getter/setters. This method should only be called when
       * value type is Object.
       */
      walk (obj: Object) {
        // 获取观察对象的每一个属性
        const keys = Object.keys(obj)
        // 遍历每一个属性,设置为响应式数据
        for (let i = 0; i < keys.length; i++) {
          defineReactive(obj, keys[i])
        }
      }
    
      /**
       * Observe a list of Array items.
       */
      observeArray (items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
          observe(items[i])
        }
      }
    }
    
  3. defineReactive:为对象定义一个响应式的属性,在get中收集依赖,在set中派发更新。

    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      // 创建依赖对象实例
      const dep = new Dep()
      // 获取 obj 的属性描述符对象
      const property = Object.getOwnPropertyDescriptor(obj, key)
      if (property && property.configurable === false) {
        return
      }
      // 提供预定义的存取器函数
      // cater for pre-defined getter/setters
      const getter = property && property.get
      const setter = property && property.set
      if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
      }
      // 判断是否递归观察子对象,并将子对象属性都转换成 getter/setter,返回子观察对象
      let childOb = !shallow && observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          // 如果预定义的 getter 存在则 value 等于getter 调用的返回值
          // 否则直接赋予属性值
          const value = getter ? getter.call(obj) : val
          // 如果存在当前依赖目标,即 watcher 对象,则建立依赖
          if (Dep.target) {
            dep.depend()
            // 如果子观察目标存在,建立子对象的依赖关系
            if (childOb) {
              childOb.dep.depend()
              // 如果属性是数组,则特殊处理收集数组对象依赖
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          // 返回属性值
          return value
        },
        set: function reactiveSetter (newVal) {
          // 如果预定义的 getter 存在则 value 等于getter 调用的返回值
          // 否则直接赋予属性值
          const value = getter ? getter.call(obj) : val
          // 如果新值等于旧值或者新值旧值为NaN则不执行
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          /* eslint-enable no-self-compare */
          if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
          }
          // 如果没有 setter 直接返回
          // #7981: for accessor properties without setter
          if (getter && !setter) return
          // 如果预定义setter存在则调用,否则直接更新新值
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          // 如果新值是对象,观察子对象并返回 子的 observer 对象
          childOb = !shallow && observe(newVal)
          // 派发更新(发布更改通知)
          dep.notify()
        }
      })
    }
    
    1. Dep.target 保存的是观察者(Watcher),是在创建Watcher实例时保存的,而Watcher实例则是发生在lifecycle.js下的mountComponent被调用时创建的。
    2. 依赖收集就是将Watcher实例watcher保存到Dep实例depsubs中,并在此过程中将dep保存到watcherdepIds中。依赖收集调用过程:Dep.depend --> Watcher.addDep(dep) --> dep.addSub(watcher)
      //Dep
      // 将观察对象和 watcher 建立依赖
      depend () {
        if (Dep.target) {
          // 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
          Dep.target.addDep(this)
        }
      }
      // 添加新的订阅者 watcher 对象
      addSub (sub: Watcher) {
        this.subs.push(sub)
      }
      //Watcher
      addDep (dep: Dep) {
        const id = dep.id
        if (!this.newDepIds.has(id)) {
          this.newDepIds.add(id)
          this.newDeps.push(dep)
          if (!this.depIds.has(id)) {
            dep.addSub(this)
          }
        }
      }
      
  4. 数组响应式原理

    1. 将改变数组的方法进行二次封装+,然后重新赋值给数组。

      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) {
          // 执行数组的原始方法
          const result = original.apply(this, args)
          // 获取数组对象的 ob 对象
          const ob = this.__ob__
          let inserted
          switch (method) {
            case 'push':
            case 'unshift':
              inserted = args
              break
            case 'splice':
              inserted = args.slice(2)
              break
          }
          // 对插入的新元素,重新遍历数组元素设置为响应式数据
          if (inserted) ob.observeArray(inserted)
          // notify change
          // 调用了修改数组的方法,调用数组的ob对象发送通知
          ob.dep.notify()
          return result
        })
      })
      
      1. 首先调用数组原始的方法
      2. 记录新增的元素,并将其中的对象元素转化为响应式的对象
      3. 发送通知,更新视图。
    2. 为数组中的每个对象元素创建Observer实例。即将数组中的对象转化成响应式的。

        observeArray (items: Array<any>) {
          for (let i = 0, l = items.length; i < l; i++) {
            observe(items[i])
          }
        }
      
  5. Watcher

    1. Watcher分三种:Computed Watcher(计算属性的Watcher)、用户Watcher(即Watcher侦听器和$watcher)、Render Watcher ,前两种都是在initState时初始化的。

    2. 首次渲染时Watcher的执行过程。首次渲染时会调用lifecycle.js中的mountComponent方法,方法内容会创建Watcher的实例。
      Vue2.x源码解读-响应式原理剖析

      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 {
          // expOrFn 是字符串的时候,例如 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
            )
          }
        }
        this.value = this.lazy
          ? undefined
          : this.get()
      }
      

      创建Watcher实例时主要完成了下面几件事:

      1. 记录当前的Watcher实例。
      2. 初始化变量,并记录选项参数
      3. 然后调用get方法,在get方法中首先会去设置Dep.target的值,然后调用getter方法获取value,最后清理Dep.target
        /**
           * Evaluate the getter, and re-collect dependencies.
           */
          get () {
            // 设置Dep.target
            pushTarget(this)
            let value
            const vm = this.vm
            try {
              value = this.getter.call(vm, vm)// 如果是renderWatcher,这里将调用updateComponent挂载虚拟DOM
            } 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)
              }
              popTarget()
              this.cleanupDeps()
            }
            return value
          }
        
    3. 数据更新之后Watcher的执行过程:

      1. 数据更新时会调用绑定的Dep实例的notify方法,挨个调用watcherupdate方法。

        // 发布通知
        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
            // 保证Watcher按照新建的顺序执行
            subs.sort((a, b) => a.id - b.id)
          }
          // 调用每个订阅者的update方法实现更新
          for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update()
          }
        }
        
      2. update中将调用queueWatcherwatcher 加入到队列,并调用flushSchedulerQueue方法处理watcher队列。

      3. flushSchedulerQueue方法里将挨个调用队列里watcherbefore以及run方法,之后初始化queue调度的状态,然后去触发组件的activated钩子和updated钩子。

        /**
         * Flush both queues and run the watchers.
         */
        function flushSchedulerQueue () {
          currentFlushTimestamp = getNow()
          flushing = true
          let watcher, id
        
          // Sort queue before flush.
          // This ensures that:
          // 1. Components are updated from parent to child. (because parent is always
          //    created before the child)
          // 2. A component's user watchers are run before its render watcher (because
          //    user watchers are created before the render watcher)
          // 3. If a component is destroyed during a parent component's watcher run,
          //    its watchers can be skipped.
          queue.sort((a, b) => a.id - b.id)
        
          // do not cache length because more watchers might be pushed
          // as we run existing watchers
          // queue在执行过程中可能还会发生变化
          for (index = 0; index < queue.length; index++) {
            watcher = queue[index]
            // 执行传入的before方法
            if (watcher.before) {
              watcher.before()
            }
            id = watcher.id
            has[id] = null
            watcher.run()
            // in dev build, check and stop circular updates.
            if (process.env.NODE_ENV !== 'production' && has[id] != null) {
              circular[id] = (circular[id] || 0) + 1
              if (circular[id] > MAX_UPDATE_COUNT) {
                warn(
                  'You may have an infinite update loop ' + (
                    watcher.user
                      ? `in watcher with expression "${watcher.expression}"`
                      : `in a component render function.`
                  ),
                  watcher.vm
                )
                break
              }
            }
          }
        
          // keep copies of post queues before resetting state
          const activatedQueue = activatedChildren.slice()
          const updatedQueue = queue.slice()
        
          resetSchedulerState()
        
          // call component updated and activated hooks
          callActivatedHooks(activatedQueue)
          callUpdatedHooks(updatedQueue)
        
          // devtool hook
          /* istanbul ignore if */
          if (devtools && config.devtools) {
            devtools.emit('flush')
          }
        }
        
        1. before方法是创建Watcher实例是传入的,RenderWatcherbefore中将触发组件的beforeUpdate钩子。

          // 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
          new Watcher(vm, updateComponent, noop, {
            before () {
              if (vm._isMounted && !vm._isDestroyed) {
                callHook(vm, 'beforeUpdate')
              }
            }
          }, true /* isRenderWatcher */)
          
        2. run方法则是调用get方法获取值,更新value的值,然后调用传入的cb更新数据和视图。

          run () {
            if (this.active) {
              const value = this.get()
              if (
                value !== this.value ||
                // Deep watchers and watchers on Object/Arrays should fire even
                // when the value is the same, because the value may
                // have mutated.
                isObject(value) ||
                this.deep
              ) {
                // set new value
                const oldValue = this.value
                this.value = value
                if (this.user) { // 如果是用户Watcher
                  try {
                    this.cb.call(this.vm, value, oldValue) //更新数据
                  } catch (e) {
                    handleError(e, this.vm, `callback for watcher "${this.expression}"`)
                  }
                } else {
                  this.cb.call(this.vm, value, oldValue)
                }
              }
            }
          }
          
  6. $watcher

    1. 三种类型的 Watcher 的创建过程:
      1. Computed Watcher:计算属性的Watcher实例是在创建Vue实例执行_init方法时创建的,在初始化state时,会判断选项参数中是否存在computed属性,如果存在则执行initComputed方法为computed中的属性创建Watcher实例,并将属性转化为响应式的数据,添加到Vue实例中。

        const computedWatcherOptions = { lazy: true }
        
        function initComputed (vm: Component, computed: Object) {
          // $flow-disable-line
          const watchers = vm._computedWatchers = Object.create(null)
          // computed properties are just getters during SSR
          const isSSR = isServerRendering()
        
          for (const key in computed) {
            const userDef = computed[key]
            const getter = typeof userDef === 'function' ? userDef : userDef.get
            if (process.env.NODE_ENV !== 'production' && getter == null) {
              warn(
                `Getter is missing for computed property "${key}".`,
                vm
              )
            }
        
            if (!isSSR) {
              // create internal watcher for the computed property.
              watchers[key] = new Watcher(
                vm,
                getter || noop,
                noop,
                computedWatcherOptions
              )
            }
        
            // component-defined computed properties are already defined on the
            // component prototype. We only need to define computed properties defined
            // at instantiation here.
            if (!(key in vm)) {
              defineComputed(vm, key, userDef)
            } else if (process.env.NODE_ENV !== 'production') {
              if (key in vm.$data) {
                warn(`The computed property "${key}" is already defined in data.`, vm)
              } else if (vm.$options.props && key in vm.$options.props) {
                warn(`The computed property "${key}" is already defined as a prop.`, vm)
              }
            }
          }
        }
        
        export function defineComputed (
          target: any,
          key: string,
          userDef: Object | Function
        ) {
          const shouldCache = !isServerRendering()
          if (typeof userDef === 'function') {
            sharedPropertyDefinition.get = shouldCache
              ? createComputedGetter(key)
              : createGetterInvoker(userDef)
            sharedPropertyDefinition.set = noop
          } else {
            sharedPropertyDefinition.get = userDef.get
              ? shouldCache && userDef.cache !== false
                ? createComputedGetter(key)
                : createGetterInvoker(userDef.get)
              : noop
            sharedPropertyDefinition.set = userDef.set || noop
          }
          if (process.env.NODE_ENV !== 'production' &&
              sharedPropertyDefinition.set === noop) {
            sharedPropertyDefinition.set = function () {
              warn(
                `Computed property "${key}" was assigned to but it has no setter.`,
                this
              )
            }
          }
          Object.defineProperty(target, key, sharedPropertyDefinition)
        }
        
        1. 创建 Computed Watcher 时选项参数中的lazytrue,创建时并不会直接调用Watcherget方法获取值。因为计算属性的Watcherget是在模板中调用的。
      2. User Watcher:用户Watcher是和计算属性的Watcher一起创建的,都是在创建Vue实例执行_init方法时创建的,在初始化state时,如果选项参数中的watch属性存在且不为空对象,则执行initWatchwatch属性对象中的每一项创建Watcher实例。

        function initWatch (vm: Component, watch: Object) {
          for (const key in watch) {
            const handler = watch[key]
            if (Array.isArray(handler)) {
              for (let i = 0; i < handler.length; i++) {
                createWatcher(vm, key, handler[i])
              }
            } else {
              createWatcher(vm, key, handler)
            }
          }
        }
        
        function createWatcher (
          vm: Component,
          expOrFn: string | Function,
          handler: any,
          options?: Object
        ) {
          if (isPlainObject(handler)) {
            options = handler
            handler = handler.handler
          }
          if (typeof handler === 'string') { // methods中对应的函数
            handler = vm[handler]
          }
          return vm.$watch(expOrFn, handler, options)
        }
        
        1. watch中属性对应的值如果是数组,则会为该属性创建多个Watcher实例。
        2. watch中属性对应的值是一个对象时,对象中handler指定数据改变时的回调函数,也可以指定deepimmediate等参数。如果指定了deep选项,则会递归将对象中的属性都转化为响应式的对象,如果指定了immediate,则会立即执行一次handler函数。
      3. Render Watcher:在web环境下,Render Watcher是在$mount中调用mountComponent时创建的,mountComponent是在lifecycle.js中定义的。Render Watcher在绑定到视图的数据发生改变时会触发视图更新,即updateComponent,并触发beforeUpdate的组件钩子。

        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
          updateComponent = () => {
            const name = vm._name
            const id = vm._uid
            const startTag = `vue-perf-start:${id}`
            const endTag = `vue-perf-end:${id}`
        
            mark(startTag)
            const vnode = vm._render()
            mark(endTag)
            measure(`vue ${name} render`, startTag, endTag)
        
            mark(startTag)
            vm._update(vnode, hydrating)
            mark(endTag)
            measure(`vue ${name} patch`, startTag, endTag)
          }
        } else {
          updateComponent = () => {
            vm._update(vm._render(), hydrating)
          }
        }
        new Watcher(vm, updateComponent, noop, {
         before () {
            if (vm._isMounted && !vm._isDestroyed) {
              callHook(vm, 'beforeUpdate')
            }
          }
        }, true /* isRenderWatcher */)
        
      4. 三种Watcher的创建顺序:Computed Watcher --> User Watcher --> RenderWatcher,执行顺序和创建顺序一致。

      5. User Watcher是最终是通过调用$watcher实例方法实现的,所以方法内部会标记WatcherUser Watcher,然后创建Watcher实例,如果用户传入了immediate且为true,则会立即执行一次数据变化之后的回调函数。最后返回一个取消监听的方法。

        Vue.prototype.$watch = function (
         expOrFn: string | Function,
          cb: any,
          options?: Object
        ): Function {
          // 获取 Vue 实例 this
          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)
          // 判断 immediate 如果为 true
          if (options.immediate) {
            // 立即执行一次 cb 回调,并且把当前值传入
            try {
              cb.call(vm, watcher.value)
            } catch (error) {
              handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
            }
          }
          // 返回取消监听的方法
          return function unwatchFn () {
            watcher.teardown()
          }
        }
        
上一篇:如何修改已释放的请求


下一篇:vue2.x源码学习