关于vue.js:vue源码分析

76次阅读

共计 33314 个字符,预计需要花费 84 分钟才能阅读完成。

理解 Flow
官网:https://flow.org/
JavaScript 的动态类型查看器,在编译阶段就进行查看而不是执行时,最终也会编译成 JavaScript 来运行
为了保障代码的可维护性和可读性,所以 vue2.x 中应用 flow,使得 vue 在代码最小改变的状况下应用动态类型查看
Flow 的动态类型查看谬误是通过动态类型推断实现的
文件结尾通过 // @flow 或者 /* @flow */ 申明
/* @flow */
function square(n: number): number {return n * n;}
square("2"); // Error!
​

调试设置
咱们能够在浏览源码时通过打包调试代码来验证本人对于源码的了解是否正确
打包工具 Rollup
Vue.js 源码的打包工具应用的是 Rollup,比 Webpack 轻量
Webpack 把所有文件当做模块,Rollup 只解决 js 文件更适宜在 Vue.js 这样的库中应用
Rollup 打包不会生成冗余的代码

开发我的项目适宜应用 webpack,开发库时适宜应用 Rollup
调试流程
装置依赖
npm i

设置 sourcemap
package.json 文件中的 dev 脚本中新增加参数 –sourcemap,开启代码映射
“dev”: “rollup -w -c scripts/config.js –sourcemap –environment TARGET:web-full-dev”
执行 dev
npm run dev 执行打包,用的是 rollup,-w 参数是监听文件的变动,文件变动主动从新打包
此时生成的 vue.js 文件和 vue.js.map 文件,如果想要其余版本的 js 文件能够通过 npm run build 来执行

调试
examples 的示例中引入的 vue.min.js 改为 vue.js
关上 Chrome 的调试工具中的 source,咱们在开启 sourcemap 后就能够看到源码 src 目录了
这样做的目标是如果不开启 sourcemap 咱们只能调试打包后的 vue.js 文件,该文件中有 1w 多行代码不不便调试
而开启 sourcemap 后咱们能够间接通过模块的模式调试模块源码

Vue 的不同构建版本
执行 npm run build 从新打包所有文件
官网文档 – 对不同构建版本的解释
dist/README.md
完整版:同时蕴含编译器和运行时的版本
编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码(将 template 转换成 render 函数),体积大(3000 行代码)、效率低
运行时:用来创立 Vue 实例、渲染并解决虚构 DOM 等的代码,体积小、效率高。基本上就是除去编译器的代码
UMD:UMD 版本通用的模块版本,反对多种模块形式。vue.js 默认文件就是运行时 + 编译器的 UMD 版本,还能够把 vue 挂载到 window 对象上
CommonJS(cjs):CommonJS 版本用来配合老的打包工具比方 Browserify 或 webpack 1
ES Module
从 2.6 开始 Vue 会提供两个 ES Modules (ESM) 构建文件,为古代打包工具提供的版本,咱们脚手架就是这个版本
ESM 格局被设计为能够被动态剖析,所以打包工具能够利用这一点来进行“tree-shaking”并将用不到的代码排除出最终的包
ES6 模块与 CommonJS 模块的差别【参考阮一峰老师的文章】


Runtime+Compiler 完整版
// <script src="../../dist/vue.js"></script>
// Compiler
// 须要编译器,把 template 转换成 render 函数
const vm = new Vue({
  el: '#app',
  template: '<h1>{{msg}}</h1>',
  data: {msg: 'Hello Vue'}
})
Runtime-only
// <script src="../../dist/vue.runtime.js"></script>
// Runtime
// 不须要编译器
const vm = new Vue({
  el: '#app',
  // template: '<h1>{{msg}}</h1>', // 没有编译器后不能解析 html 语法须要应用上面的 render 函数
  render (h) {return h('h1', this.msg)
  },
  data: {msg: 'Hello Vue'}
})


应用 vue-cli 创立的我的项目是应用的 vue.runtimr.esm.js
在 vue 中查看 webpack 的配置文件(因为 vue 对 webpack 进行了深度封装,所以须要应用其余命令来查看)
应用 inspect 办法:

vue inspect  // 这样应用获取到的 webpack 信息会打印到终端,不不便查看
vue inspect > output.js     // > 的作用是把以后 webpack 配置信息打印到 output.js 文件中
​
// output.js
  resolve: {
    alias: {
      ...
      vue$: 'vue/dist/vue.runtime.esm.js'
    }
  }
留神:*.vue 文件中的模板是在构建时预编译的,曾经准换成了 render 函数,最终打包后的后果是不须要编译器,只须要运行时版本即可

寻找入口文件
查看 dist/vue.js 的构建过程来找入口文件
执行构建

npm run dev
# "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
# --environment TARGET:web-full-dev 设置环境变量 TARGET
​
# 其中 -w 示意 watch 挂起监听模式,- c 前面是配置文件,--environment 设置环境变量为 TARGET,其中 web 示意 web 端,full 示意完整版,dev 示意开发版不进行压缩


script/config.js 的执行过程(文件开端)作用:生成 rollup 构建的配置文件
应用环境变量 TARGET = web-full-dev


getConfig(name)
依据环境变量 TARGET 获取配置信息
const opts = builds[name]
builds[name] 获取生成配置的信息
最终返回了 config 配置对象


const builds = {
  ...
  // 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
  },
}
resolve()

获取入口和进口文件的绝对路径



const aliases = require('./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)
  }
}
// scripts/alias
const path = require('path')
const resolve = p => path.resolve(__dirname, '../', p)
module.exports = {
  ...
  web: resolve('src/platforms/web'),
}

最终后果

把 src/platforms/web/entry-runtime-with-compiler.js(入口文件)构建成 dist/vue.js,如果设置 –sourcemap 会生成 vue.js.map
src/platform 文件夹下是 Vue 能够构建成不同平台下应用的库,目前有 weex 和 web,还有服务器端渲染的库

Vue 源码剖析
Vue 调试办法

如果同时用 render 函数和 template 模板会输入哪个?const vm = new Vue({
  el: '#app',
  template: '<h1>Hello Template</h1>',
  render(h) {return h('h1', 'Hello Render')
  }
})
依据源码可剖析:// 保留 Vue 实例的 $mount 办法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  // 非 ssr 状况下为 false,ssr 时候为 true
  hydrating?: boolean
): Component {
  // 获取 el 对象
  el = el && query(el)

  /* istanbul ignore if */
  // 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
  }

  const options = this.$options
  
  // 这个 if 的作用
  // 如果 options 外面没有 render 函数,就把 template/el 转换成 render 函数
  // 如果传入的有 render 函数就不会进入这个 if 语句,就间接将调用 mount 渲染 DOM
  if (!options.render) {...}
  // 调用 mount 办法,渲染 DOM
  return mount.call(this, el, hydrating)
}

el 不能是 body 或者 html 标签,此处会进行判断,如果设置了二者会报错
如果 options 外面没有 render 函数,就把 template/el 转换成 render 函数
如果传入的有 render 函数就不会进入这个 if 语句,就间接将调用 mount 渲染 DOM
所以会输入 render 函数中的内容

$mount 在哪里调用?被什么调用的?
通过代码调试来探索
留神:

如果应用 npm run build 来输入文件,此时的 dist/vue.js 中的最初一行是没有 sourceMap 映射的://# sourceMappingURL=vue.js.map 会被革除,所以如果想在调试过程看到 src 源码,须要从新运行 npm run dev 生成 vue.js 文件来开启代码地图。

在调试窗口 Call Stack 调用堆栈地位能够晓得,咱们能够看到办法调用的过程,以后执行的是 Vue.$mount 办法,再往下能够看到 是 vue 调用的_init,Vue._init

通过代码调试能够发现 $mount 是 _init() 调用的
并且证实了,如果 new Vue 同时设置了 template 和 render(),此时只会执行 render()

Vue 初始化过程
从导出 vue 的文件开始动手:

entry-runtime.js 文件
这个文件里是运行时的入口文件,导入了 runtime.js 文件并将其以 Vue 的名称导出

entry-runtime-with-compiler.js 文件
这个是带有编译器的入口文件,重写了 $mount 办法减少了渲染模板的性能,外围是上述的那个 if 语句,将模板编译成渲染函数
给 Vue 减少了一个 compile 静态方法,用来编译 HTML 语法转换成 render 函数
引入了 runtime 中的 index.js 即:src/platform/web/runtime/index.js
设置 Vue.config
设置平台相干的指令和组件
指令 v-model、v-show
组件 transition、transition-group

设置平台相干的__patch__ 办法(打补丁办法,比照新旧的 VNode)
设置 $mount 办法,挂载 DOM


import config from 'core/config'
...
// install platform runtime directives & components
// 设置平台相干的指令和组件(运行时)
// extend() 将第二个参数对象成员 拷贝到 第一个参数对象中去
// 指令 v-model、v-show
extend(Vue.options.directives, platformDirectives)
// 组件 transition、transition-group
extend(Vue.options.components, platformComponents)

// install platform patch function
// 设置平台相干的 __patch__ 办法 (虚构 DOM 转换成 实在 DOM)
// 判断是否是浏览器环境(是 - 间接返回,非 - 空函数 noop
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
// 设置 $mount 办法,挂载 DOM
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
src/platform/web/runtime/index.js 中又援用了 'core/index',即:src/core/index.js

platform 是平台相干的,而 core 文件夹中的代码是和平台无关的
定义了 Vue 的静态方法
定义了服务端渲染的办法啥的
initGlobalAPI(Vue) 给 vue 的构造函数减少一些静态方法
初始化 Vue.config 对象
定义 Vue.set、Vue.delete、Vue.nextTick
定义 Observerable,让对象具备响应式

src/core/index.js 中援用了 ‘./instance/index’

src/core/index.js 中援用了 ‘./instance/index’,即 src/core/instance/index.js
定义了 Vue 的构造函数
设置 Vue 实例的成员


// 此处不必 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)
}
// 注册 vm 的 _init() 办法,初始化 vm
initMixin(Vue)
// 注册 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相干办法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相干的混入办法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)

export default Vue
四个导出 Vue 的模块
src/platforms/web/entry-runtime-with-compiler.js
web 平台相干的入口
重写了平台相干的 $mount()办法
注册了 Vue.compile() 办法,传递一个 HTML 字符串返回 render 函数

src/platform/web/runtime/index.js
web 平台相干
注册和平台相干的全局指令:v-model、v-show
注册和平台相干的全局组件:v-transition、v-transition-group
全局办法:
patch:把虚构 DOM 转换成实在 DOM
$mount:挂载办法

src/core/index.js
与平台无关
设置了 Vue 的静态方法,initGlobalAPI(Vue)

src/core/instance/index.js
与平台无关
定义了构造函数,调用了 this._init(options) 办法
给 Vue 中混入了罕用的实例成员

Vue 动态成员初始化
两个问题
去掉 vscode 中的语法查看问题
因为 vscode 和 ts 都是微软开发的,默认类型查看都是在 ts 文件中能力应用,而 vue 源码应用的 flow 进行的类型查看,然而 vscode 会报错,认为只能在 ts 文件中应用接口、类型查看等语法
在设置的 json 文件中增加如下代码

"javascript.validate.enable": false,  // 不对 js 代码进行校验

当解析到上面语法是会产生谬误,导致这段代码前面的代码失去高亮显示
解决办法是在 vscode 外面装置 babel JavaScript 插件即可
尽管具备了高亮,然而会失落 ctrl+ 左击跳转的性能


  Vue.observable = <T>(obj: T): T => {observe(obj)
    return obj
  }

动态成员的初始化
通过 src/core/index.js 的 initGlobalAPI(Vue) 来到 初始化 Vue 的静态方法 所在文件:global-api/index


import {initGlobalAPI} from './global-api/index'
...
// 注册 Vue 的动态属性 / 办法
initGlobalAPI(Vue)
src/core/global-api/index.js
初始化 Vue 的静态方法
initUse() : src/core/global-api/use.js
initMixin() : src/core/global-api/mixin.js
initExtend() : src/core/global-api/extend.js
initAssetRegisters() : src/core/global-api/assets.js




export function initGlobalAPI (Vue: GlobalAPI) {
  ...
  // 初始化 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.
  // 这些工具办法不视作全局 API 的一部分,除非你曾经意识到某些危险,否则不要去依赖他们
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
  // 静态方法 set/delete/nextTick
  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 对象,并给其扩大, 全局的指令、过滤器都会存储到这里
  // components/directives/filters
  Vue.options = Object.create(null)
  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.
  // 这是用来标识 "base" 构造函数,在 Weex 的多实例计划中,用它来扩大所有一般对象组件
  Vue.options._base = Vue

  // 设置 keep-alive 组件
  extend(Vue.options.components, builtInComponents)

  // 注册 Vue.use() 用来注册插件
  initUse(Vue)
  // 注册 Vue.mixin() 实现混入
  initMixin(Vue)
  // 注册 Vue.extend() 基于传入的 options 返回一个组件的构造函数
  initExtend(Vue)
  // 注册 Vue.directive()、Vue.component()、Vue.filter() 这三个参数基本一致所以能够一起定义
  initAssetRegisters(Vue)
}

门路中不加 ./ 是在 src 目录下,如果加了./ 就代表是在同级目录下
Vue.use 办法的实现:
初始化时通过 initUse 办法来实现,initUse 办法外部定义 use 办法,而后判断以后传入的插件参数是对象还是函数
如果是对象就调用对象的 install 办法(这个是 Vue 文档的约定,插件必须具备 install 办法)
如果是个函数就间接调用这个函数
最初要把以后作为参数传递进来的插件放到 Vue 的数组 installedPlugins 外面保存起来

Vue 实例成员初始化
在文件 src/core/instance/index.js 中进行的实例化
定义 Vue 的构造函数
初始化 Vue 的实例成员

initMixin() : src/core/instance/init.js
stateMixin() : src/core/instance/state.js
eventsMixin() : src/core/instance/state.js
lifecycleMixin() : src/core/instance/state.js
renderMixin() : src/core/instance/state.js


// 此处不必 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)
}

// 注册 vm 的 _init() 办法,初始化 vm
initMixin(Vue)

// 注册 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)

// 初始化事件相干办法
// $on/$once/$off/$emit
eventsMixin(Vue)

// 初始化生命周期相干的混入办法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)

// 混入 render
// $nextTick/_render
// $nextTick/_render
renderMixin(Vue)


实例成员 - init
initMixin(Vue)
注册 vm 的 _init() 办法,初始化 vm
src/core/instance/init.js
可参考 Vue 实例 文档


export function initMixin (Vue: Class<Component>) {// 给 Vue 实例减少 _init() 办法
  // 合并 options / 初始化操作
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a 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
    // 如果是 Vue 实例不须要被 observe
    vm._isVue = true
    // merge 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
      
    // 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')

    /* 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)
    }
    // 调用 $mount() 挂载
    if (vm.$options.el) {vm.$mount(vm.$options.el)
    }
  }
}

咱们在 render 函数时调用的 h 函数其实就是 $createElement 办法来把虚构 DOM 转换成实在 DOM
实例成员 – initState
initState(vm)
初始化 vm 的 _props/methods/_data/computed/watch
src/core/instance/state.js
可参考 Vue 实例 文档


export function initState (vm: Component) {vm._watchers = []
  const opts = vm.$options
  // 将 props 成员转换成响应式数据,并注入到 vue 实例
  if (opts.props) initProps(vm, opts.props)
  // 初始化选项中的办法(methods)
  if (opts.methods) initMethods(vm, opts.methods)
  // 数据的初始化
  if (opts.data) {
    // 把 data 中的成员注入到 Vue 实例 并转换为响应式对象
    initData(vm)
  } else {
    // observe 数据的响应式解决
    observe(vm._data = {}, true /* asRootData */)
  } 
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)
  }
}

在初始化 data 和 props 时会进行判断,二者之间有没有重名的属性,是不容许有重名的属性的

初始化过程调试
初始化过程调试代码
设置断点


src/core/instance/index.js
initMixin(Vue)


src/core/index.js
initGlobal(Vue)


src/platforms/web/runtime/index.js
Vue.config.mustUseProp


src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount

开始调试

F5 刷新浏览器卡到断点
首先进入 core/instance/index.js,core 中的代码与平台无关的,在这里调用了 Mixin 的一些函数,这些函数外面给 Vue 的原型上减少了一些实例成员
通过 initMixin(Vue)用来初始化 Vue 的实例 vm
通过_init() 函数,初始化 vm 实例,判断以后是否为组件,合并 Vue 本身及用户提供的 options
通过 stateMixin(),新增 $data / $props / $set / $delete / $watch 几个成员。然而 $data 和 $props 此时都是 undefined,仅仅初始化了这两个属性,未来须要通过选项去赋值
通过函数 eventsMixin(),初始化事件相干的四个办法 $on / $once / $off / $emit
通过函数 lifecycleMixin(),它注册了根生命周期相干的办法 _update / $forceUpdate / $destroy。其中_updata 外部调用了 patch 办法,把 VNode 渲染成实在的 DOM
通过函数 renderMixin(),其执行过后,会给原型挂载一些 _ 结尾的办法,这些办法的作用是当咱们把模板转换成 render 函数的时候,在 render 函数中调用,除此之外还注册了 $nextTick / _render, _render 的作用是调用用户定义 或 模板渲染的 render 函数

F8 跳转到下一个导出 Vue 的文件 core/index.js,这个文件中执行了 initGlobalAPI(),给 Vue 的构造函数初始化了动态成员
进入 initGlobalAPI(),F10 执行到初始化 Vue.config 对象的中央,Vue 的构造函数新增 config 属性,这是一个对象,并且这个对象曾经初始化了很多内容,这些内容是通过给 config 对象减少一个 get 办法,在 get 办法中返回了../config 中导入的 config
持续 F10 执行三个静态方法 set / delete / nextTick
F10 初始化 observable
持续 F10 初始化 options 对象,但此时 options 对象为空,因为是通过 Object.create(null)来初始化的,没有原型,持续 F10 削减全局组件、指令以及过滤器 components / directives / filters,再 F10 初始化_base 即 Vue
F10 为 options.compents 设置 keep-alive 组件
F10 初始化静态方法 Vue.use()、Vue.mixin()、Vue.extend()。以及 Vue.directive()、Vue.component()、Vue.filter(),它们是用来注册全局的指令、组件和过滤器,咱们调用这些办法的时候,它们会把指令、组件和过滤器别离帮咱们注册到 Vue.options 中对应的属性外面来

再按 F8 进入 platforms/web/runtime/index.js,此时咱们看到的代码都是与平台相干的,它首先给 Vue.config 中注册了一些与平台相干的一些公共的办法,当它执行完过后 又注册了几个与平台相干的指令和组件
F10 将其执行完察看指令和组件的变动
持续 F10 给 Vue 原型上注册了 patch 和 $mount,其执行是在 Vue._init 中调用的

F8 进入到最初一个文件 platforms/web/runtime/entry-runtime-with-compiler.js 的断点,这个文件重写了 $mount,新增了把模板编译成 render 函数的性能
在文件最初给 Vue 构造函数挂载了 compile 办法,这个办法的作用是让咱们手共把模板转换成 render 函数

Vue 首次渲染
首次渲染过程调试
Vue 初始化结束,开始真正的执行
调用 new Vue() 之前,曾经初始化结束
通过调试 [代码,记录首次渲染过程
首次渲染过程:

数据响应式原理
响应式解决入口
整个响应式解决的过程是比较复杂的,上面咱们先从


src\core\instance\init.js
initState(vm) vm 状态的初始化
初始化 vm 的 _props、methods、_data 等




src\core\instance\state.js
export function initState (vm: Component) {
  ...
  // 数据的初始化
  if (opts.data) {
    // 把 data 中的成员进行遍历注入到 Vue 实例,最初再调用 observe 办法将 data 换为响应式对象
    initData(vm)
  } else {
    // observe 数据的响应式解决入口,创立了 observer 对象
    observe(vm._data = {}, true /* asRootData */)
  }
  ...
}
initData 代码:vm 数据的初始化
function initData (vm: Component) {
  let data = vm.$options.data
  // 初始化 _data,组件中 data 是函数,调用函数返回后果
  // 否则间接返回 data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  ...
  // proxy data on instance
  // 获取 data 中的所有属性
  const keys = Object.keys(data)
  // 获取 props / methods
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  // 判断 data 成员是否和 props/methods 重名, 如果重名开发模式会发送正告
  ...
  
  // observe data
  // 数据的响应式解决
  observe(data, true /* asRootData */)
}


src\core\observer\index.js
observe(value, asRootData)

目标是作为响应式入口,负责为每一个 Object 类型的 value 创立一个 observer 实例

此处的__ob__定义在 observer 类中,起到一个缓存的性能,如果 value 这个对象曾经存在这个__ob__,就阐明这个对象曾经进行了响应式解决,那么间接返回 ob,ob 是 value 中的一个属性,外面存在以后对象对应的 new observer 实例



/**
 * 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.
 */
// 试图为一个 value 创立一个 observer 观察者实例,// 如果创立胜利,则返回新的观察者实例
// 如果该值曾经有观察者,则返回现有的观察者
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 首先判断 value 是否是对象 是否是 VNode 虚构 DOM 的实例
  // 如果它不是对象或者是 VNode 实例,那么就不须要做响应式解决 间接返回
  if (!isObject(value) || value instanceof VNode) {return}
  let ob: Observer | void
  // 如果 value 有 __ob__(observer 对象) 属性
  // 判断 value.__ob__ 属性是否是 observer 的实例
  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++}
  return ob
}


Observer
src\core\observer\index.js
定义了三个属性
value -- 被察看的对象
dep -- dep 依赖对象
vmCount -- 计数器

定义了 walk 办法,遍历每一个属性,顶用 defineReactive 设置为响应式数据
对对象做响应化解决
对数组做响应化解决
观察者类,附加到每个被察看对象上一旦被附加,观察者就会将指标对象的属性转换为 getter/setter,以收集依赖关系并派发更新(发送告诉)


/**
 * 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.
 */
// 观察者类,附加到每个被察看对象上
// 一旦被附加,观察者就会将指标对象的属性转换为 getter/setter,// 以收集依赖关系并派发更新
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
    // def 调用 defineProperty 默认不可枚举
    // 将实例挂载到察看对象的 __ob__ 属性,这个__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.
   */
  // 遍历所有属性,并将它们转换为 getter/setter
  // 只有当值类型为 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])
    }
  }
}
walk(obj)

遍历 obj 的所有属性,为每一个属性调用 defineReactive() 办法,设置 getter/setter

为 data 对象设置的__ob__是不能够枚举的,起因是后续须要遍历 data 中的所以成员定义响应式,而 ob 的作用仅是用来存贮以后对象对应的 observer 实例对象的,不须要在遍历时定义响应式,所以定义成不可枚举,所以遍历时漠视它,不为其定义响应式
对象响应式解决 defineReactive
src\core\observer\index.js
defineReactive(obj, key, val, customSetter, shallow)
为一个对象定义一个响应式的属性,每一个属性对应一个 dep 对象
如果该属性的值是对象,持续调用 observe
如果给属性赋新值,持续调用 observe
如果数据更新发送告诉

依赖收集
在 defineReactive() 的 getter 中实例化 dep 对象,并判断 Dep.target 是否有值,这个 target 就是 watcher 对象,如果有, 调用 dep.depend(),它外部最调用 dep.addSub(this),把 watcher 对象增加到 dep.subs.push(watcher) 中,也就是把订阅者增加到 dep 的 subs 数组中,当数据变动的时候调用 watcher 对象的 update() 办法
Dep.target 的设置机会
当首次渲染时调用 mountComponent() 办法的时候,创立了渲染 watcher 对象,执行 watcher 中的 get() 办法
get() 办法外部调用 pushTarget(this),把以后 Dep.target = watcher,同时把以后 watcher 入栈,因为有父子组件嵌套的时候先把父组件对应的 watcher 入栈,再去解决子组件的 watcher,子组件的处理完毕后,再把父组件对应的 watcher 出栈,持续操作
Dep.target 用来寄存目前正在应用的 watcher。全局惟一,并且一次也只能有一个 watcher 被应用



/**
 * Define a reactive property on an Object.
 */
// 为一个对象定义一个响应式的属性
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 创立依赖对象实例
  const dep = new Dep()
  // 获取 obj 的属性描述符对象
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 通过 configurable 指定以后属性是否为可配置的
  // 如果为不可配置 象征不可用 delete 删除并且不能用 defineReactvie 从新定义 间接返回
  if (property && property.configurable === false) {return}
  // 提供预约义的存取器函数
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  // 参数为两个时 获取 value
  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) {...}
      // 返回属性值
      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()}
  })
}


Dep
依赖对象
记录 watcher 对象
depend() — watcher 记录对应的 dep
公布告诉


let uid = 0
// dep 是个可察看对象,能够有多个指令订阅它
/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  // 动态属性,watcher 对象
  static target: ?Watcher;
  // dep 实例 Id
  id: number;
  // dep 实例对应的 watcher 对象 / 订阅者数组
  subs: Array<Watcher>;

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

  // 增加新的订阅者 watcher 对象
  addSub (sub: Watcher) {this.subs.push(sub)
  }

  // 移除订阅者
  removeSub (sub: Watcher) {remove(this.subs, sub)
  }

  // 将察看对象和 watcher 建设依赖
  depend () {if (Dep.target) {
      // 如果 target 存在,把 dep 对象增加到 watcher 的依赖中
      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)
    }
    // 调用每个订阅者的 update 办法实现更新
    for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}
  }
}
// Dep.target 用来寄存目前正在应用的 watcher
// 全局惟一,并且一次也只能有一个 watcher 被应用
// 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 = []
// 将 watcher 放入栈中,入栈并将以后 watcher 赋值给 Dep.target
// 父子组件嵌套的时候先把父组件对应的 watcher 入栈,// 再去解决子组件的 watcher,子组件的处理完毕后,再把父组件对应的 watcher 出栈,持续操作
export function pushTarget (target: ?Watcher) {targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  // 出栈操作
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

数组响应式解决

src\core\observer\index.js
Observer 的构造函数中
// 获取 arrayMethods 特有的成员 返回的是蕴含名字的数组
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

export class Observer {
  ...
  constructor (value: any) {
    ...
    // 数组的响应式解决
    if (Array.isArray(value)) {
      // 判断以后浏览器是否反对对象的原型属性
      if (hasProto) {protoAugment(value, arrayMethods)
      } else {copyAugment(value, arrayMethods, arrayKeys)
      }
      // 为数组中的每一个对象创立一个 observer 实例
      this.observeArray(value)
    } else {...}
  }
  /**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 */
  // 通过应用__proto__拦挡原型链来加强指标对象或数组
  function protoAugment (target, src: Object) {
    /* eslint-disable no-proto */
    target.__proto__ = src
    /* eslint-enable no-proto */
  }

  /**
   * Augment a target Object or Array by defining
   * hidden properties.
   */
  // 通过定义暗藏属性来加强指标对象或数组 
  /* istanbul ignore next */
  function copyAugment (target: Object, src: Object, keys: Array<string>) {for (let i = 0, l = keys.length; i < l; i++) {const key = keys[i]
      def(target, key, src[key])
    }
  }
}

对数组办法进行修补
数组中的某些办法会扭转原数组中的元素,数组默认的办法时不会触发 setter 的,所以须要进行修补,当数组调用了这些办法时调用时就调用 dep.notify 来告诉 watcher 来更新视图
还有就是调用 observeArray 办法,遍历数组中的元素将数组中的元素转换成响应式的

能使页面中视图更新的办法有:
vm.arr.push(100) 这个能够,因为曾经重写了 vue 组件中的数组办法
vm.arr[0] = 100 这个不能够(数据产生了变动,然而视图不会同步更新),因为源码并没有解决数组对象自身的属性(arr[0] 其实就是调用 arr 的非凡属性名 0),因为数组对象自身有十分多的属性,如果都解决了可能会带来性能上的适度开销
vm.arr.length = 0 这个也不能够更新视图,起因同上
如果要清空数组或者更改数组中的某一个元素的话,能够应用 splice 办法来实现
splice(0) — 示意清空数组
splice (0, 1, 100) — 示意在索引为 0 的地位,删除 1 个元素,替换成 100

Watcher
Watcher 分为三种,Computed Watcher(计算属性)、用户 Watcher (侦听器)、渲染 Watcher,前两种是在 initState 阶段初始化的
渲染 Watcher 的创立机会
src/core/instance/lifecycle.js


export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  ...
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {...} else {updateComponent = () => {vm._update(vm._render(), hydrating)
    }
  }

  // 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 的构造函数中设置为 vm._watcher,// 因为 watcher 的初始补丁可能会调用 $forceUpdate(例如在子组件的挂载钩子中),// 这依赖于 vm._watcher 曾经被定义  
  new Watcher(vm, updateComponent, noop, {before () {if (vm._isMounted && !vm._isDestroyed) {callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

整个流程概述:
当数据发生变化后,通过 setter 调用 dep 的 notify 办法去告诉 watcher
会先把 watcher 放到一个队列外面,而后遍历这个队列调用每个 watcher 的 run 办法
最初 run 办法会调用渲染 watcher 的 updateComponent 办法

响应式处理过程总结
详见脑图

实例办法
vm.$set
性能
向响应式对象中增加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上增加新属性,因为 Vue 无奈探测一般的新增属性 (比方 this.myObject.newProperty = ‘hi’)

实例
// 应用 vm.$set 或者 Vue.set 都能够,一个是静态方法,一个是实例办法
vm.$set(obj, ‘foo’, ‘test’) // 参数:对象,属性名,属性值
留神:不能给 Vue 实例,或者 Vue 实例的根数据对象 ($data) 增加响应式对象,这样会报错,并且实现不了响应式。


Vue.set()
src/core/global-api/index.js


  // 静态方法 set/delete/nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick
vm.$set()
src/core/instance/index.js
src/core/instance/state.js


// instance/index.js
// 注册 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)

// instance/state.js
Vue.prototype.$set = set
Vue.prototype.$delete = del

源码

set()办法

src/core/observer/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)) {
    // 判断以后 key 和数组 length 的最大值给 length
    // 当咱们调用 $set 传递的索引有可能超过数组的 length 属性
    target.length = Math.max(target.length, key)
    // 通过 splice 对 key 地位的元素进行替换
    // splice 在 array.js 进行了响应化的解决
    target.splice(key, 1, val)
    return val
  }
  // 如果 key 在对象中曾经存在且不是原型成员 间接赋值
  if (key in target && !(key in Object.prototype)) {target[key] = val
    return val
  }
  // 获取 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
  }
  // 如果 ob 不存在,target 不是响应式对象间接赋值
  if (!ob) {target[key] = val
    return val
  }
  // 如果 ob 存在,把 key 设置为响应式属性
  defineReactive(ob.value, key, val)
  // 发送告诉
  ob.dep.notify()
  return val
}

set 办法会解决数组的响应式也会解决对象的响应式
给数组设置值的时候会调用 splice 办法
给对象减少新的成员时会调用 defineReactive 办法,最终调用 ob.dep.notify 办法发送告诉

vm.$delete
性能
删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个办法次要用于避开 Vue 不能检测到属性被删除的限度,然而你应该很少会应用它。

留神:对象不能是 Vue 实例,或者 Vue 实例的根数据对象。


实例
 vm.$delete(vm.obj, 'msg')        // 删除 obj 对象中的 msg,并更新到视图上


定义地位

Vue.delete()
src/core/global-api/index.j


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


vm.$delete()
src/core/instance/index.js
src/core/instance/state.js


// instance/index.js
// 注册 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)

// instance/state.js
Vue.prototype.$set = set
Vue.prototype.$delete = del


源码

delete()办法
src/core/observer/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)}`)
  }
  // 判断是否是数组,以及 key 是否非法
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 如果是数组通过 splice 删除
    // splice 做过响应式解决
    target.splice(key, 1)
    return
  }
  // 获取 target 的 ob 对象
  const ob = (target: any).__ob__
  // target 如果是 Vue 实例或者 $data 对象,间接返回
  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}
  // 删除属性
  delete target[key]
  // 判断是否是响应式的
  if (!ob) {return}
  // 通过 ob 发送告诉
  ob.dep.notify()}

vm.$watch
vm.$watch(expOrFn, callback, [options] ) // expOrFn 是 data 中的属性,callback 是 data 中属性变动时的回调,[options]是配置对象
性能
察看 Vue 实例变动的一个表达式或计算属性函数。回调函数失去的参数为新值和旧值。表达式只承受监督的键门路。对于更简单的表达式,用一个函数取代。

参数
expOrFn:要监督的 $data 中的属性,能够是表达式或函数
callback:数据变动后执行的函数
函数:回调函数
对象:具备 handler 属性(字符串或者函数),如果该属性为字符串则 methods 中相应的定义

options:可选的选项
deep:布尔类型,深度监听,能够监听子属性的变动
immediate:布尔类型,是否立刻执行一次回调函数

示例


const vm = new Vue({
  el: '#app',
  data: {
      a: '1',
      b: '2',
      msg: 'Hello vue',
    user: {
      firstName: '诸葛',
      lastName: '亮'
    }
  }
})
// expOrFn 是表达式
vm.$watch('msg', function (newVal, oldVal) {onsole.log(newVal, oldVal)
})
vm.$watch('user.firstName', function (newVal, oldVal) {console.log(newVal)
})
// expOrFn 是函数
vm.$watch(function () {return this.a + this.b}, function (newVal, oldVal) {console.log(newVal)
})
// deep 是 true,耗费性能
vm.$watch('user', function (newVal, oldVal) {
  // 此时的 newVal 是 user 对象
  console.log(newVal === vm.user)
}, {
  deep: true  
 // 如果不设置这个 deep,那么将会导致只有当 user 对象变动时才会更新视图(比方将 user 置空),user 对象中的属性变动不会更新视图
})
// immediate 是 true
vm.$watch('msg', function (newVal, oldVal) {console.log(newVal)
}, {immediate: true})

三种类型的 Watcher 对象

没有静态方法,因为 $watch 办法中要应用 Vue 的实例
Watcher 分三种:计算属性 Watcher、用户 Watcher (侦听器)、渲染 Watcher
创立程序:计算属性 Watcher(id:1)、用户 Watcher (侦听器 id:2)、渲染 Watcher(id:3)
执行程序:依照 id 从小到大排序,与创立程序雷同
watcher 实例

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>watcher</title>
</head>
<body>
  <div id="app">
    {{reversedMessage}}
    <hr>
    {{user.fullName}}
  </div>

  <script src="../../dist/vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        message: 'Hello Vue',
        user: {
          firstName: '诸葛',
          lastName: '亮',
          fullName: ''
        }
      },
      computed: { // 计算属性
        reversedMessage: function () {return this.message.split('').reverse().join('')
        }
      },
      watch: {
        // 侦听器写法 1:// 这种写法不容易设置配置对象
        // 'user.firstName': function (newValue, oldValue) {
        //   this.user.fullName = this.user.firstName + ' ' + this.user.lastName
        // },
        // 'user.lastName': function (newValue, oldValue) {
        //   this.user.fullName = this.user.firstName + ' ' + this.user.lastName
        // },
        // 侦听器写法 2:'user': {handler: function (newValue, oldValue) {this.user.fullName = this.user.firstName + ' ' + this.user.lastName},
          deep: true,
          immediate: true
        }
      }
    })
  </script>
</body>
</html>


vm.$watch()
src\core\instance\state.js


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

异步更新队列 nextTick()
是在数据变动后,数据更新到 DOM 上之后才会去执行 nextTick 中传递的这个回调函数
因为 Vue 更新 DOM 是异步执行的、批量的,所以当批改 DOM 中的数据后立刻紧接着就去获取或者操作刚刚批改的数据是获取不到的,还是之前的老数据。
为了解决这个问题,所有引入了 $nextTick 办法,来实现在下次 DOM 更新循环完结之后执行提早执行 $nextTick 中的回调。使得在批改数据之后立刻应用这个办法,也能获取更新后的 DOM

vm.$nextTick(function () {/* 操作 DOM */}) / Vue.nextTick(function () {})
示例代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>nextTick</title>
</head>
<body>
  <div id="app">
    <p id="p" ref="p1">{{msg}}</p>
    {{name}}<br>
    {{title}}<br>
  </div>
  <script src="../../dist/vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello nextTick',
        name: 'Vue.js',
        title: 'Title'
      },
      // 当属性的值被扭转之后不会立刻更新 DOM
      // 这个更新是一个异步的过程,// 此时如果立刻获取 p 标签的内容是获取不到的
      mounted() {
        this.msg = 'Hello World'
        this.name = 'Hello snabbdom'
        this.title = 'Vue.js'
  
        // 如果想获取界面上最新的值
        Vue.nextTick(() => {console.log(this.$refs.p1.textContent)
        })
      }
    })

  </script>
</body>
</html>


定义地位

src\core\instance\render.js
  Vue.prototype.$nextTick = function (fn: Function) {return nextTick(fn, this)
  }

源码

手动调用 vm.$nextTick()
在 Watcher 的 queueWatcher 中执行 nextTick()

src\core\util\next-tick.js
const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  // 遍历回到函数数组 顺次调用
  for (let i = 0; i < copies.length; i++) {copies[i]()}
}

// 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).
// 在这里,咱们有应用微工作的异步提早包装器。// 在 2.5 中,咱们应用了 (宏) 工作(与微工作相结合)。// 然而,当状态在重绘之前就被扭转时,它有奥妙的问题。// 另外,在事件处理程序中应用 (宏) 工作会导致一些奇怪的行为。// 另外,在事件处理程序中应用(宏)工作会导致一些奇怪的行为。// 所以咱们当初又到处应用微工作。// 这种衡量的一个次要毛病是,有些状况下,// 微工作的优先级太高,在所谓的程序事件之间停火,甚至在同一事件的冒泡之间停火
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:
// nextTick 行为利用了微工作队列,// 能够通过原生的 Promise.then 或 MutationObserver 拜访
// MutationObserver 有更宽泛的反对,然而在 iOS >= 9.3.3 的 UIWebView 中,// 当在触摸事件处理程序中触发时,它有重大的 bug。// 触发几次后就齐全进行工作了...... 所以,如果原生 Promise 可用,咱们会应用它。/* 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.
    // 在有问题的 UIWebViews 中,Promise.then 并没有齐全 break,// 但它可能会卡在一个奇怪的状态,即回调被推送到微工作队列中,// 但队列并没有被刷新,直到浏览器须要做一些其余工作,例如解决一个计时器
    // 因而,咱们能够通过增加一个空的定时器来 "强制" 微工作队列被刷新。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)
  // 在没有本地 Promise 的中央应用 MutationObserver
  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.
  // 降级到 setImmediate
  // 从技术上讲,它利用了(宏)工作队列,// 但它依然是比 setTimeout 更好的抉择
  timerFunc = () => {setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  // 降级到 setTimeout
  timerFunc = () => {setTimeout(flushCallbacks, 0)
  }
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // callbacks 存储所有的回调函数
  // 把 cb 加上异样解决存入 callbacks 数组中
  callbacks.push(() => {if (cb) {
      try {// 调用 cb()
        cb.call(ctx)
      } catch (e) {handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {_resolve(ctx)
    }
  })
  // 判断队列是否正在被解决
  if (!pending) {
    pending = true
    // 调用
    timerFunc()}
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    // 返回 promise 对象
    return new Promise(resolve => {_resolve = resolve})
  }
}

正文完
 0