Vue 源码解析—环境搭建、初始化流程,数据响应原理

学习目标

  • 搭建整体开发环境
  • vue整体初始化流程
  • 数据响应式原理

学习开发环境

获取vue项目

项目地址:https://github.com/vuejs/vue

当前版本:2.6.10

文件结构

在这里插入图片描述

命名注意事项

runtime:不带编译器的版本;

common:commonjs打包规范来自node.js,require,exports常用与后端,同步的,老旧版本的打包器browsify,webpack1.0;

amd:requirejs,专用与浏览器;

esm:es module 规范,常用的规范是import export,webpack2.0及以上;

umd:universal module definition;

入口文件怎么找

打开vue 源码里的package.json文件

{
  "main": "dist/vue.runtime.common.js",
  "module": "dist/vue.runtime.esm.js",   //用webpack2.0以上
  "unpkg": "dist/vue.js",    //用浏览器
  "jsdelivr": "dist/vue.js",    
  "typings": "types/index.d.ts",
 }

调试环境

  • 安装依赖:npm - i;
  • 安装rollup:npm i -g rollup;
  • 修改dev脚本,将"dev"改为一下:
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
  • 执行开发命令npm run dev

我们如何通过调试代码看vue是怎么运行的呢?

在这里插入图片描述

我们把 <script src="../../dist/vue.min.js"></script> 改为<script src="../../dist/vue.js"></script>

我们通过浏览器打开上图所示index.html,打开浏览器的调试模式(F12),切换到Sources,在Page目录下找到src,如下图:

在这里插入图片描述

如果能看到上图所示,说明调式模式已经成功了,基本源码结构都有了,我们在调试的时候可以很方便的看代码了,我们可以依照下图打开app.js,因为它是入口文件,并且在38行打个断点。

在这里插入图片描述

我们刷新一下,页面就响应了,点击一下图的按钮,就可以进行下一步了:
在这里插入图片描述

当我们执行到下图步骤时,就走到vue的构造函数了,如果你之前对构造涵数一无所知,通过这个方式就可以开始进行了源码的学习过程:

在这里插入图片描述

我们可以通过鼠标右键定位到具体的目标文件,如下图所示:

在这里插入图片描述

如何找入口文件?

我们可以在package.JSON文件里的:
找到这一行代码

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

通过代码我们知道它的输出目标web-full-dev,我们可以config.js文件找答案,其实config.js是rollup的配置文件,我们不需要去关注它的细节,我们的关注点可以从打开文件之后从38行 builds 对象开始,它是所有创建目标的描述对象,如下图所示:

在这里插入图片描述

我们可以在这个对象里找到了web-full-dev这一项

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

通过参数的传递我们可以很明确的知道我们用的是那一项的配置文件,通过注释我们可以很清楚的知道它将来运行于浏览器,同时我们也知道它的入口文件是web/entry-runtime-with-compiler.js,出口文件是dist/vue.js,但是需要注意的是我们通过web/entry-runtime-with-compiler.js路径并没有找到web这个目录,我们可以通过resolve这个方法可以找到这串代码:

const aliases = require('./alias')
const resolve = p => {
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}

我们找找到const aliases = require('./alias')这个目录下的文件,

const path = require('path')

const resolve = p => path.resolve(__dirname, '../', p)

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

通过这个web: resolve('src/platforms/web')这段代码我们才知道,它的真正路径是怎么来的,通过一番折腾我们终于找到了src/platforms/web/entry-runtime-with-compiler.js的入口文件。

整个vue的初始化流程

我们打开entry-runtime-with-compiler.js文件,里面代码虽然很多,但关键代码不多,我们看到第7行代码:

import Vue from './runtime/index'

这是我们有点纳闷,这代码是干什么的 ?我们先不管它,我们继完下走。

el,template,render,$mount的优先级

我们在写vue的时候通常写成以下形式:

new Vue({
  el:'#app'
  template:'<app></app>'
  render:h => h(App)
}).$mount('#app')

现在问题来,这几个选项“el,template,render,$mount”同时设置,谁的优先级高?

entry-runtime-with-compiler.js文件里的以下代码就为我们揭开答案:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,//拿到的数据有可能是两者,#app,app
  hydrating?: boolean
):): Component {
  //{1}
  el = el && query(el)
  //{2}
  const options = this.$options;
  if (!options.render) {
    let template = options.template
    if (template) {
      //{3} 解析template选项
      if (typeof template === 'string') {
      //{4}
        if (template.charAt(0) === '#') {
          //{5}
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        //{6}如果传递dom,获取其内部模板内容, 
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) { {7}
      //{8} 否则解析el选项
      template = getOuterHTML(el)
    }

    //{9}编译过程
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      //{10} 编译得到render函数
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      //{11}直到到选项中
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  //{12}挂载:生成vdom,生成dom,追加到el
  // 执行默认$mount函数
  return mount.call(this, el, hydrating)
}

核心代码从第17行开始,首先把Vue里的$mount拿下来,核心是覆盖$mount方法,function 里的参数可能有两种类型,一种是选择器,如#app,另一种是app元素,最终$mount会返回一个组件,组件:
{1}、获取dom元素;
{2}、尝试解析配置;

我们发现一个if语句都是在处理没有render的情况,如果有render就不会执行if函数了,所以我们可以得之,
render的优先级是非常高的,一旦设置了rendertemplate的都不管用了,不过el还有可能起作用。

我们继续看代码:

{3}、判断template是字符串;

{4}、当template字符串第一个字符是”#“的时候,说明template选择器;

{5}、当template是选择器的时候,掉idToTemplate方法,获取字符串模板;

{6}、如果传递dom,获取其内部模板内容;

{7}、如果用户没有设置template的情况下,el生效,所以有得到一个结论 el选项的优先级最低如果设置了template,el不生效;

{8}、当是在el的情况下,掉getOuterHTML方法,把带着这个标签全部做为这个模板;

{9}、整个模板处理完成之后,开始编译模板,进入了编译过程;

{10}、调compileToFunctions方法,把template做为参数,生成render函数;

{11}、把生成的render函数直接指定到配置项中;

{12}、执行挂载:生成vdom,生成dom,追加到el

总结
entry-runtime-with-compiler.js 文件的作用是覆盖$mount,解析template选项并编译之。

核心代码

      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      //{11}直到到选项中
      options.render = render
解读import Vue from './runtime/index

接下来我们来研究一开始的第7行代码是干什么的,我们进入runtime 目录下的index文件;

/* @flow */
import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'

import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index'

import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

// install platform patch function
//实现$mount,核心就一个mountComponent;定义一个__patch__方法
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
//{12}定义$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

// devtools global hook
/* istanbul ignore next */
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)
}
export default Vue

{12}是最重要的代码,它的核心是定义$mount;
此时我们发现Vue不是在此文件定义的,而是从以下文件导出来的:

import Vue from 'core/index'

我们继续深挖次文件,我们发现次文件已经不在platforms文件了,平台差异性的代码在这里,而核心代码在“core”里,我们在code文件夹里打开index.js,我们开始真正的进入了核心了:

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
//{13} 定义的全局api,如Vue.set/minimx
initGlobalAPI(Vue)

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

Vue.version = '__VERSION__'

export default Vue

{13}初始化全局api,我们常见的方法,如Vue.set/minimx/use/extend,都在此声明的,要想了解这些方法,这个就是我们的入口点。

所以,core/index是作用就是定义全局的api,核心代码是:

initGlobalAPI(Vue)

我们再进入initGlobalAPI方法所在的文件,代码如下:

/* @flow */

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

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.'
      )
    }
  }
  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.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  Vue.options = Object.create(null)
  {14}
  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.
  {15}
  Vue.options._base = Vue
  {16}
  extend(Vue.options.components, builtInComponents)
  {17}
  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

{14}"ASSET_TYPES"是[‘component’,‘filter’,‘directive’]

ASSET_TYPES.forEach(type => {
  Vue.options[type + 's'] = Object.create(null)
})

这断代码的意思就是初始化Vue配置项,三个方法:
{15}保存vue;
{16}把Vue.options.components和内置的builtInComponents合并起来;
{17}我们比较关心的方法都在这下面;

我们在回到我们之前的import Vue from 'core/index文件,我们发现构造函数在:

import Vue from './instance/index'

打开此文件:

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

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')
  }
  //{18}
  this._init(options)
}

initMixin(Vue)  //{19}
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

{18}核心代码就一行,我们的构造涵数就执行了一个_init方法;
{19}在{18}中我们觉得这个_init方法特别的迷获,这个方法是那来的,显然是通过这种方式,给vue的构造函数加了一个实例方法_init,所以大家想看初始化方法得去这个里看。

我们点入这个initMixin方法:

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {  //{20}
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
  

    // a flag to avoid this being observed
    vm._isVue = true
    // merge 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
      )
    }
   
    // {21} expose real self
    vm._self = vm
    initLifecycle(vm)  //{22}
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

{20}就一件事,就是给vue的构造函数加了_init方法
{21}核心代码就是以下部分,我们猜猜它们是干什么的,一眼看上去,会觉得initLifecycle(vm)是生命周期函数,其实不是,而callHook(vm, 'beforeCreate')才是真正生命周期函数,在beforeCreate之前做了三件事:
1、initLifecycle
2、initEvents初始化事件相关的事情;
3、initRender初始化渲染相关的事情;

之后是更数据相关的事情:

  • initInjections注入祖代传入的数据;
  • initState初始化组件里面的数据;
  • initProvide提供给后代需要传递的数据;

所以我们可以得出一个结论,我们想调数据只有在beforeCreate之后进行

{22}我们进入initLifecycle方法里看看,到底做了什么事情;

export function initLifecycle (vm: Component) {
  const options = vm.$options
  // locate first non-abstract parent
  let parent = options.parent //{23}
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent   //{24}
  vm.$root = parent ? parent.$root : vm  //{25}

  vm.$children = []    //{26}
  vm.$refs = {}
}

{23}我们看到了parent字眼,我们知道谁赐予自己生命,是你父母,所以和生命周期相关的事情都是和老爹相关的事情,或者是和祖代相关的事情。所以初始化就是先找到自己的老爹,在把自己塞到自己老爹肚子里面。
{24}要和自己老爹建立一个联系,保存一下老爹;
{25}保存一下自己的老祖宗;

数据响应式原理

接下就是来研究vue的数据响应式,我们从{19}的initMixin(Vue)开始,打开init.js文件,我们从以下代码开始:

 vm._self = vm
 initLifecycle(vm)
 initEvents(vm)    //{26}
 initRender(vm)    //{27}
 callHook(vm, 'beforeCreate')
 initInjections(vm) // resolve injections before data/props
 initState(vm)     //{28}
 initProvide(vm) // resolve provide after data/props
 想callHook(vm, 'created')

{26}将来如果想研究vue的事件,此处是个非常好的入口点;
{27}接下来来研究一下render,render又做了什么事情呢,其实核心的事情只做了两件;

  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  //render函数中的h就是它
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

render声明了两个函数分别是_c$createElement,将来我们执行的h其实就是$createElement,将来我们要研究vue的虚拟DOM,就研究它就可以了;

{28}initState非常的重要,我们打开它对应的文件;

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options  
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm) //{29}
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

接下来我们在浏览器里运行examples/todomvc/index.html,是浏览器的sources里找到state.js文件,打开它,在53行打上断点,并运行之,如下图:

在这里插入图片描述

我们发现53行的data并不是我们熟悉的那个data,我们在写根主见的时候我们并没有写data,而data是在初始化的某一步生成的,生成data的正是mergedInstanceDataFn函数,它执行的结果就会返回data所有的对象,接下来我们执行下一步,它就会执行{29}的initData(vm),我继续往下执行,就会进入state.js里:
文件地址:src/core/instance/state.js

我们来看看这段代码的核心作用是什么?

function initData (vm: Component) {
  let data = vm.$options.data  //{30}
  data = vm._data = typeof data === 'function'  // {31}
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)  //{32}
  const props = vm.$options.props  //{33}
  const methods = vm.$options.methods   //{34}
  let i = keys.length
  while (i--) {    //{35}
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data   将data做响应化处理
  observe(data, true /* asRootData */)  //{36}
}

{30}首先拿到vm里的$options选项,离得data
{31}对data的类型进行判断,如果是函数调getData函数进行处理,否则返回,总之就为了确保是我们想要的数据;
{32}拿到data里所有的key
{33}拿到$options选项里的props;
{34}拿到$options选项里的methods;
{35}对data里的属性进行去重校验;
{36}而整快代码的核心就是这个,调observe函数,它所做的是将data进行响应化处理;

我们打开observe函数所在的文件:
文件地址:src/core/observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void  //{37}
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { //{38}
    ob = value.__ob__  //{39}
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)  // {40}
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob   //{41}
}

{37}我们创建一个类,这个类是观察者Observer
{38}判断__ob__是否存在
{39}存在把value挂载的__ob__给返回
{40}如果不存在就创建新的Observer
{41}observe函数做的唯一件事情就是返回观察者实例;

接下来我们来研究一下Observe
地址:src/code/observer/index.js

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() //{42}
    this.vmCount = 0
    def(value, '__ob__', this) //{43}
    if (Array.isArray(value)) {  //{44}
      if (hasProto) {
        protoAugment(value, arrayMethods)  //{61}
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)  //45}
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {   //{46}
    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])
    }
  }
}

{42}当一个值传进来的时候我们立刻创建一个new Dep()的实例;
{43}给当前的值定义一个__ob__;
{44}判断data里是纯对象还是数组,如果是数组有该怎么做,从这里我们可知,Observer的核心就是判断data里是数组还是对象,不同的类型,采用不同的响应化来处理。
{45}对象走的是walk;
{46}先对对象的所有属性,对对象的属性进行遍历,对所有keydefineReactive方法进行响应化处理;

以下为defineReactive方法的代码:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()  //{46}

  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]
  }
  
  let childOb = !shallow && observe(val) //{47}
  Object.defineProperty(obj, key, {      //{48}
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      //{49} 添加watcher实例
      if (Dep.target) {
        dep.depend()  //{56}
        //{50}有子对象额外处理
        if (childOb) {
          //{51} 把watcher也添加到子对象中
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)  //{52}
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) { //{53}
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)  //{54}
      dep.notify()   //{55}
    }
  })
}

{46}每个key对应一个dep;
{47}如果val是对象就向下递归;
{48}如果Key是普通的值,就定义get,set;
{49}添加watcher;
{50}有子对象额外处理;
{51}把watcher也添加到子对象中;

这有是为什么呢?
主要是因为对象的更改一定是会影响到子对象的;

{52}如果是数组,还必须执行一下数组响应化处理;

接下来我们看看set的处理:
{53}这行代码很奇怪,自己和自己想等也就算了,还有自己和自己都不相等的情况吗?其实NAN就是自己和自己都不相等的;
{54}递归的把自己的孩子也设定一下;
{55}通知他们去做更新;
{56}接下来,来看一下dep方法;

一下是dep方法代码:

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      //{57}
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

{57}为你的理解是,我们会主动的把addDep添加到subs里,但是奇怪的是还让watcher加我自己,这是什么意图?
为你先进入watcheraddDep来回答这个问题:

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) {  //{58}
      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 //{60}
    } else {
      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()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      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)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {   //{58}
    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)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  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) {
          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)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

{58}这个操作主要是做去重的操作,确保我们传进来的dep我们没加过,如果没加过,我们就把它添加进去,我们一定不能重复的添加dep,先把watcher加到dep做引用,然后又把自己加到dep里面。这个操作做了两件事,就是把watherdep之间进行相互关注,你加我,我加你。

我们特别的疑惑,为什么要这样做,为什么watcher里要保存一个dep

watcher是一个非常关键的中间人角色,js改变对象的时候,怎么就能去通知虚拟DOM的diff算法?

我们再来看看构造函数watcher到底还做了什么?

{59}vue里里面有一个组件就有一个watcher,我们称这个wathcer为渲染watcher;
{60}我们得一些更新函数;

接下来我们看看数组的响应式,我们来看看上面的src/code/observer/index.js文件里的{44}部分,当data是数组的时候,调this.observeArray(value)函数进行处理。

数组响应化的原理

我们想一下有一个数组在data中声明,数组发生变化的时候,它什么时候会发生变化呢?比如:

new Vue {
  el:'#app',
  template:'<App></App>'
  component:{},
  data:{
    arr:[{foo:'foo'},{foo:'foo'}]
  },
  methods:{
   modify(){
     this.arr[0].foo = 'bar'
   }
  }
}

我们假设将来data里的arr会发生“增”、“删”、“改”、“查”的操作,比如我们直接在arr里的item里的某个属性进行赋值,如调上面的modify方法,界面中会响应化,会更新吗?

答案是:会更新
如果这是后有会问,我们平常在项目中为什么又会去用$set,我们通过揭晓源码来揭晓答案。

我们知道能够改变数组结构操作的有7个方法,这个七个方法分别是:
push,
pop,
shift,
unshift,
splice,
sort',
reverse
为了能让我们操作数组的时候,vue能直接做响应式,vue做了一个事情,vue对这七个方法做了一个拦截器,对这七个方法进行了拦截,接管了它们具体的操作,除了做这7个方法的操作之外,还要做个通知方法,让它去做更新。

我们来看看它们是怎么进行拦截的?
我们进入src/code/observer/index.js文件里的{61}的 protoAugment(value, arrayMethods)里:

function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src   //{62}
}

{62}这段代码的意思是要把数组的原型设为src
这时我们回去看protoAugment(value, arrayMethods),value是数组,arrayMethods就是我们要覆盖的原型,就是要进行数据拦截的方法,怎么拦截呢,直接把原来的原型给替换了,我们接着来看arrayMethods方法。

arrayMethods定义在src/core/observer/array.js文件里,代码如下:

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from '../util/index'

const arrayProto = Array.prototype   //{63}
export const arrayMethods = Object.create(arrayProto) //{64}

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {  //{65}
  // cache original method

  //获取原始操作方法push
  const original = arrayProto[method] //{66}

  //定义拦截
  def(arrayMethods, method, function mutator (...args) {  //{67}
    const result = original.apply(this, args)   //{68}
    
    //{69}
    const ob = this.__ob__  //{70}
    let inserted  //{71}
    switch (method) {  //{72}
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)   //{73}
    // notify change
    ob.dep.notify()   //{71}
    return result
  })
})

通过代码我们看看,vue是如何对数组方法进行拦截的?

{63}首先我们先将Array的原型给保存起来;
{64}接下来创建一个全新的对象arrayMethods,将来它将做为我们数组的指定原型,我们在执行数组的七个方法,将不再是原来的七个方法,而是现在的七个方法。
{65}接下来我们对这七个方法进行遍历;
{66}获取原始的操作方法,我们以push为例;
{67}定义拦截def,我们给arrayMethods对象定义method方法,这个方法是什么呢,就是def里具体的mutator函数的描述器。
如下,将来的value就是我们定义的mutator

Object.defineProperty(arrayMethods,method,{value:mutator})

{68}之前的行为;
{69}定义数组方法的全新行为;
{70}拿到之前定义的观察者;
{71}拿到观察者之后,拿到它的dep做更新,这时界面就更新了;

现在我们得道答案了,如果我们对data里的数组做push操作,对arrpush操作,页面立刻就更新了,如:

new Vue {
  el:'#app',
  template:'<App></App>'
  component:{},
  data:{
    arr:[{foo:'foo'},{foo:'foo'}]
  },
  methods:{
   modify(){
     this.arr.push({foo:'foo'})  //{74}
     this.arr[0].bar = 'bar'; //{75}
     delete this.arr[0].foo  //{76}
     
     this.$delete(...)  //{77}
   }
  }
}

这是为什么呢?
原因就是{70}是观察者;
{71}尝试去做插入工作;
{72}插入的方式有三,push,unshift,splice,不管新进来的元素,做了这三个方法的什么操作,新进来的元素必须做响应式,但它是新进来的,没做响应式,因为它是新来的,还没经历过共产主义的洗礼,必须进行在教育,但data一开始就有的数组,早都进行教育了,早都进行过教育了;

{73}对新来的元素进行额外的教育;
{74}可以对数组进行变更;
{75}而这部分就不能进行变更;
{76}同样是这个方法也不行,因为只有经过处理的七方法才能进行拦截,而delete没有经过处理;
{77}而这种方法是可以的;
//mutator就是定义数组方法的全新行为。

vue新项目准备: 1、安装nodejs,官网下载傻瓜安装 node -v 验证 2、npm包管理器,是集成在node中的,所以安装了node也就有了npm npm -v 验证 3、安装cnpm npm install -g cnpm --registry=http://registry.npm.taobao.org (完成之后,我们就可以用cnpm代替npm来安装依赖包了。如果想进一步了解cnpm的,查看淘宝npm镜像官网。) 4、安装vue-cli脚手架构建工具 npm install -g vue-cli vue新项目构建: 1、初始化项目模板: vue init webpack-simple yunshi-approve 或者 vue init webpack yunshi-approve 2、安装npm cd yunshi-approve 3、安装项目所需要的依赖: npm install 或 cnpm install 4、运行看效果: npm run dev 介绍一下目录及其作用: build:最终发布的代码的存放位置。 config:配置路径、端口号等一些信息,我们刚开始学习的时候选择默认配置。 node_modules:npm 加载的项目依赖模块。 src:这里是我们开发的主要目录,基本上要做的事情都在这个目录里面,里面包含了几个目录及文件: assets:放置一些图片,如logo等 components:目录里放的是一个组件文件,可以不用。 App.vue:项目入口文件,我们也可以将组件写这里,而不使用components目录。 main.js :项目的核心文件 static:静态资源目录,如图片、字体等。 test:初始测试目录,可删除 .XXXX文件:配置文件。 index.html:首页入口文件,可以添加一些meta信息或者同统计代码啥的。 package.json:项目配置文件。 README.md:项目的说明文件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值