Vue.js 源码剖析 - 响应式原理
准备工作
Vue源码获取
这里主要分析 Vue 2.6版本的源码,使用Vue 3.0版本来开发项目还需要一段时间的过渡
-
项目地址:
- Vue 2.6 https://github.com/vuejs/vue
- Vue 3.x https://github.com/vuejs/vue-next
Fork一份到自己仓库,克隆到本地,这样可以自己写注释提交,如果直接从github clone太慢,也可以先导入gitee,再从gitee clone到本地
-
查看Vue源码的目录结构
src compiler 编译相关 core Vue 核心库 platforms 平台相关代码(web、weex) server SSR 服务端渲染 sfc 单文件组件 .vue 文件编译为 js 对象 shared 公共代码
Flow
Vue 2.6 版本中使用了Flow,用于代码的静态类型检查
Vue 3.0+ 版本已使用TypeScript进行开发,因此不再需要Flow
Flow是JavaScript的静态类型检查器
-
Flow的静态类型检查是通过静态类型推断来实现的
安装Flow以后,在要进行静态类型检查的文件头中通过
// @flow
或/* @flow */
注释的方式来声明启用-
对于变量类型的指定,与TypeScript类似
/* @flow */ function hello (s: string): string { return `hello ${s}` } hello(1) // Error
打包与调试
Vue 2.6中使用Rollup来打包
-
打包工具Rollup
- Rollup比webpack轻量
- Rollup只处理js文件,更适合用来打包工具库
- Rollup打包不会生成冗余的代码
-
调试
- 执行
yarn
安装依赖(有yarn.lock文件) - package.json文件dev script中添加
--sourcemap
参数来开启sourcemap,以便调试过程中查看代码
- 执行
Vue不同构建版本的区别
执行yarn build可以构建所有版本的打包文件
UMD | CommonJS | ES Module | |
---|---|---|---|
Full | vue.js | vue.common.js | vue.esm.js |
Runtime-only | vue.runtime.js | vue.runtime.common.js | vue.runtime.esm.js |
Full(production) | vue.min.js | ||
Runtime-only(production) | vue.runtime.min.js |
不同版本之间的区别
- 完整版:同时包含编译器和运行时的版本
- 编译器:用来将模板字符串编译为JavaScript渲染(render)函数的代码,体积大、效率低
- 运行时:用来创建Vue实例,渲染并处理虚拟DOM等的代码,体积小、效率高,即去除编译器之后的代码
- 简单来说,运行时版本不包含编译器的代码,无法直接使用template模板字符串,需要自行使用render函数
- 通过Vue Cli创建的项目,使用的是vue.runtime.esm.js版本
- 运行时版:只包含运行时的版本
- UMD:通用模块版本,支持多种模块方式。vue.js默认就是运行时+编译器的UMD版本
- CommonJS:CommonJS模块规范的版本,用来兼容老的打包工具,例如browserfy、webpack 1等
- ES Module:从2.6版本开始,Vue会提供两个esm构建文件,是为现代打包工具提供的版本
- esm格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行tree-shaking,精简调未被用到的代码
寻找入口文件
通过查看构建过程,来寻找对应构建版本的入口文件位置
-
以vue.js版本的构建为例,通过rollup进行构建,指定了配置文件
scripts/config.js
,并设置了环境变量TARGET:web-full-dev
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
-
进一步查看
scripts/config.js
配置文件module.exports导出的内容来自genConfig()函数,并接收了环境变量TARGET作为参数
// scripts/config.js if (process.env.TARGET) { module.exports = genConfig(process.env.TARGET) } else { exports.getBuild = genConfig exports.getAllBuilds = () => Object.keys(builds).map(genConfig) }
genConfig函数组装生成了config配置对象,入口文件配置
input: opts.entry
,配置项的值opts.entry
来自builds[name]
// scripts/config.js function genConfig (name) { const opts = builds[name] const config = { input: opts.entry, external: opts.external, plugins: [ flow(), alias(Object.assign({}, aliases, opts.alias)) ].concat(opts.plugins || []), output: { file: opts.dest, format: opts.format, banner: opts.banner, name: opts.moduleName || 'Vue' }, onwarn: (msg, warn) => { if (!/Circular/.test(msg)) { warn(msg) } } } ... }
通过传入环境变量TARGET的值,可以找到
web-full-dev
相应的配置,入口文件是web/entry-runtime-with-compiler.js
const builds = { // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify 'web-runtime-cjs-dev': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.common.dev.js'), format: 'cjs', env: 'development', banner }, 'web-runtime-cjs-prod': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.common.prod.js'), format: 'cjs', env: 'production', banner }, // Runtime+compiler CommonJS build (CommonJS) 'web-full-cjs-dev': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.common.dev.js'), format: 'cjs', env: 'development', alias: { he: './entity-decoder' }, banner }, // Runtime+compiler development build (Browser) 'web-full-dev': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.js'), format: 'umd', env: 'development', alias: { he: './entity-decoder' }, banner }, ... }
使用VS Code查看Vue源码的两个问题
使用VSCode查看Vue源码时通常会碰到两个问题
- 对于flow的语法显示异常报错
- 修改VSCode设置,在setting.json中增加
"javascript.validate.enable": false
配置
- 修改VSCode设置,在setting.json中增加
- 对于使用了泛型的后续代码,丢失高亮
- 通过安装Babel JavaScript插件解决
一切从入口开始
入口文件 entry-runtime-with-compiler.js 中,为Vue.prototype.$mount指定了函数实现
el可以是DOM元素,或者选择器字符串
el不能是body或html
选项中有render,则直接调用mount挂载DOM
-
选项中如果没有render,判断是否有template,没有template则将el.outerHTML作为template,并尝试将template转换成render函数
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { // 判断el是否是 DOM 元素 // 如果el是字符串,则当成选择器来查询相应的DOM元素,查询不到则创建一个div并返回 el = el && query(el) /* istanbul ignore if */ // 判断el是否是body或者html // Vue不允许直接挂载在body或html标签下 if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elements instead.` ) return this } const options = this.$options // resolve template/el and convert to render function // 判断选项中是否包含render if (!options.render) { // 如果没有render,判断是否有template let template = options.template if (template) { ... } else if (el) { // 如果没有template,则获取el的outerHTML作为template template = getOuterHTML(el) } if (template) { ... // 将template转换成render函数 const { render, staticRenderFns } = compileToFunctions(template, { outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns ... } } // 调用 mount 挂载 DOM return mount.call(this, el, hydrating) }
tips:$mount()函数在什么位置被调用?通过浏览器调试工具的Call Stack视图,可以简单且清晰的查看一个函数被哪个位置的上层代码所调用
Vue初始化
初始化相关的几个主要文件
-
src/platforms/web/entry-runtime-with-compiler.js
- 重写了平台相关的$mount()方法
- 注册了Vue.compile()方法,将HTML字符串转换成render函数
-
src/platforms/web/runtime/index.js
- 注册了全局指令:v-model、v-show
- 注册了全局组件:v-transition、v-transition-group
- 为Vue原型添加了全局方法
- _patch_:把虚拟DOM转换成真实DOM(snabbdom的patch函数)
- $mount: 挂载方法
-
src/core/index.js
- 调用
initGlobalAPI(Vue)
设置了Vue的全局静态方法
- 调用
-
src/core/instance/index.js - Vue构造函数所在位置
定义了Vue构造函数,调用了this._init(options)方法
-
给Vue中混入了常用的实例成员和方法
静态成员
通过 initGlobalAPI() 函数,实现了Vue静态成员的初始化过程
- Vue.config
- Vue.util
- 暴露了util对象,util中的工具方法不视作全局API的一部分,应当避免依赖它们
- Vue.set()
- 用来添加响应式属性
- Vue.delete()
- 用来删除响应式属性
- Vue.nextTick()
- 在下次 DOM 更新循环结束之后执行延迟回调
- Vue.observable()
- 用来将对象转换成响应式数据
- Vue.options
- Vue.options.components 保存全局组件
- Vue.options.components.KeepAlive 注册了内置的keep-alive组件到全局Vue.options.components
- Vue.options.directives 保存全局指令
- Vue.options.filters 保存全局过滤器
- Vue.options._base 保存Vue构造函数
- Vue.options.components 保存全局组件
- Vue.use()
- 用来注册插件
- Vue.mixin()
- 用来实现混入
- Vue.extend(options)
- 使用基础 Vue 构造器,创建一个子组件,参数是包含选项的对象
- Vue.component()
- 用来注册或获取全局组件
- Vue.directive()
- 用来注册或获取全局指令
- Vue.filter()
- 用来注册或获取全局过滤器
- Vue.compile()
- 将一个模板字符串编译成 render 函数
- 这个静态成员方法是在入口js文件中添加的
// src/core/global-api/index.js
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
// 初始化 Vue.config 对象
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
// 增加静态成员util对象
// util中的工具方法不视作全局API的一部分,应当避免依赖它们
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
// 增加静态方法 set/delete/nextTick
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
// 增加 observable 方法,用来将对象转换成响应式数据
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
// 初始化 options 对象
Vue.options = Object.create(null)
// ASSET_TYPES
// 'component',
// 'directive',
// 'filter'
// 为 Vue.options 初始化components/directives/filters
// 分别保存全局的组件/指令/过滤器
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
// 保存 Vue 构造函数到 options._base
Vue.options._base = Vue
// 注册内置组件 keep-alive 到全局 components
extend(Vue.options.components, builtInComponents)
// 注册 Vue.use() 用来注册插件
initUse(Vue)
// 注册 Vue.mixin() 实现混入
initMixin(Vue)
// 注册 Vue.extend() 基于传入的 options 返回一个组件的构造函数
initExtend(Vue)
// 注册 Vue.component()/Vue.directive()/Vue.filter()
initAssetRegisters(Vue)
}
实例成员
src/core/instance/index.js 中初始化了绝大部分实例成员属性和方法
- property
- vm.$data
- vm.$props
- ...
- 方法 / 数据
-
vm.$watch()
-
$watch() 没有对应的全局静态方法,因为需要用到实例对象vm
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, // 可以是函数,也可以是对象 options?: Object ): Function { // 获取 vue 实例 const vm: Component = this if (isPlainObject(cb)) { // 判断 cb 如果是对象,执行 createWatcher return createWatcher(vm, expOrFn, cb, options) } options = options || {} // 标记是用户 watcher options.user = true // 创建用户 Watcher 对象 const watcher = new Watcher(vm, expOrFn, cb, options) if (options.immediate) { // 判断 immediate 选项是 true,则立即执行 cb 回调函数 // 不确定 cb 是否能正常执行,使用 try catch 进行异常处理 try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } // 返回 unwatch 方法 return function unwatchFn () { watcher.teardown() } }
-
-
vm.$set()
- 同Vue.set()
-
vm.$delete()
- 同Vue.delete()
-
- 方法 / 事件
- vm.$on()
- 监听自定义事件
- vm.$once()
- 监听自定义事件,只触发一次
- vm.$off()
- 取消自定义事件的监听
- vm.$emit()
- 触发自定义事件
- vm.$on()
- 方法 / 生命周期
- vm.$mount()
- 挂载DOM元素
- 在
runtime/index.js
中添加,在入口js中重写
- vm.$forceUpdate()
- 强制重新渲染
- vm.$nextTick()
- 将回调延迟到下次 DOM 更新循环之后执行
- vm.$destory()
- 完全销毁一个实例
- vm.$mount()
- 其他
- vm._init()
- Vue实例初始化方法
- 在Vue构造函数中调用了该初始化方法
- vm._update()
- 会调用vm._patch_方法更新 DOM 元素
- vm._render()
- 会调用用户初始化时选项传入的render函数(或者template转换成的render函数)
- vm._patch_()
- 用于把虚拟DOM转换成真实DOM
-
runtime/index.js
中添加了该方法
- vm._init()
// src/code/instance/index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
// 注册 vm 的 _init() 方法,同时初始化 vm
initMixin(Vue)
// 注册 vm 的 $data/$props/$set()/$delete()/$watch()
stateMixin(Vue)
// 注册 vm 事件相关的成员及方法
// $on()/$off()/$once()/$emit()
eventsMixin(Vue)
// 注册 vm 生命周期相关的成员及方法
// _update()/$forceUpdate()/$destory()
lifecycleMixin(Vue)
// $nextTick()/_render()
renderMixin(Vue)
export default Vue
_init()
Vue的构造函数中会调用Vue实例的_init()方法来完成一些实例相关的初始化工作,并触发
beforeCreate
和created
生命周期钩子函数
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
// 设置实例的uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
// 给实例对象添加_isVue属性,避免被转换成响应式对象
vm._isVue = true
// merge options
// 合并构造函数中的options与用户传入的options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化生命周期相关的属性
// $children/$parent/$root/$refs
initLifecycle(vm)
// 初始化事件监听,父组件绑定在当前组件上的事件
initEvents(vm)
// 初始化render相关的属性与方法
// $slots/$scopedSlots/_c/$createElement/$attrs/$listeners
initRender(vm)
// 触发 beforeCreate 生命周期钩子函数
callHook(vm, 'beforeCreate')
// 将 inject 的成员注入到 vm
initInjections(vm) // resolve injections before data/props
// 初始化 vm 的_props/methods/_data/computed/watch
initState(vm)
// 初始化 provide
initProvide(vm) // resolve provide after data/props
// 触发 created 生命周期钩子函数
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
// 如果提供了挂载的 DOM 元素 el
// 调用$mount() 挂载 DOM元素
vm.$mount(vm.$options.el)
}
}
initState()
初始化 vm 的_props/methods/_data/computed/watch
// src/core/instance/state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
// 将 props 中成员转换成响应式的数据,并注入到 Vue 实例 vm 中
if (opts.props) initProps(vm, opts.props)
// 将 methods 中的方法注入到 Vue 的实例 vm 中
// 校验 methods 中的方法名与 props 中的属性是否重名
// 校验 methods 中的方法是否以 _ 或 $ 开头
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// 将 data 中的属性转换成响应式的数据,并注入到 Vue 实例 vm 中
// 校验 data 中的属性是否在 props 与 methods 中已经存在
initData(vm)
} else {
// 如果没有提供 data 则初始化一个响应式的空对象
observe(vm._data = {}, true /* asRootData */)
}
// 初始化 computed
if (opts.computed) initComputed(vm, opts.computed)
// 初始化 watch
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
首次渲染过程
数据响应式原理
Vue的响应式原理是基于观察者模式来实现的
响应式处理入口
Vue构造函数中调用了vm._init()
_init()函数中调用了initState()
initState()函数中如果传入的data有值,则调用initData(),并在最后调用了observe()
observe()函数会创建并返回一个Observer的实例,并将data转换成响应式数据,是响应式处理的入口
// src/core/observer/index.js
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 创建 Observer 实例
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
// 返回 Observer 实例
return ob
}
Observer
Observer是响应式处理的核心类,用来对数组或对象做响应式的处理
在它的构造函数中初始化依赖对象,并将传入的对象的所有属性转换成响应式的getter/setter,如果传入的是数组,则会遍历数组的每一个元素,并调用observe() 创建observer实例
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
export class Observer {
// 观察对象
value: any;
// 依赖对象
dep: Dep;
// 实例计数器
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
// 初始化实例计数器
this.vmCount = 0
// 使用 Object.defineProperty 将实例挂载到观察对象的 __ob__ 属性
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 数组的响应式处理
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 为数组每一个对象创建一个 observer 实例
this.observeArray(value)
} else {
// 如果 value 是一个对象
// 遍历对象所有属性并转换成 getter/setter
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
// 遍历对象所有属性
for (let i = 0; i < keys.length; i++) {
// 转换成 getter/setter
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
// 遍历数组所有元素,调用 observe
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
defineReactive
用来定义一个对象的响应式的属性,即使用Object.defineProperty来设置对象属性的 getter/setter
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
// 目标对象
obj: Object,
// 目标属性
key: string,
// 属性值
val: any,
// 自定义 setter 方法
customSetter?: ?Function,
// 是否深度观察
// 为 false 时如果 val 是对象,也将转换成响应式
shallow?: boolean
) {
// 创建依赖对象实例,用于收集依赖
const dep = new Dep()
// 获取目标对象 obj 的目标属性 key 的属性描述符对象
const property = Object.getOwnPropertyDescriptor(obj, key)
// 如果属性 key 存在,且属性描述符 configurable === false
// 则该属性无法通过 Object.defineProperty来重新定义
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
// 获取用于预定义的 getter 与 setter
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
// 如果调用时只传入了2个参数(即没传入val),且没有预定义的getter,则直接通过 obj[key] 获取 val
val = obj[key]
}
// 判断是否深度观察,并将子对象属性全部转换成 getter/setter,返回子观察对象
let childOb = !shallow && observe(val)
// 使用 Object.defineProperty 定义 obj 对象 key 属性的 getter 与 setter
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 如果存在预定义的 getter 则 value 等于 getter 调用的返回值
// 否则 value 赋值为属性值 val
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 当前存在依赖目标则建立依赖
dep.depend()
if (childOb) {
// 如果子观察目标存在,则建立子依赖
childOb.dep.depend()
if (Array.isArray(value)) {
// 如果属性是数组,则处理数组元素的依赖收集
// 调用数组元素 e.__ob__.dep.depend()
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 如果存在预定义的 getter 则 value 等于 getter 调用的返回值
// 否则 value 赋值为属性值 val
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
// 判断新值旧值是否相等
// (newVal !== newVal && value !== value) 是对 NaN 这个特殊值的判断处理(NaN !== NaN)
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
// 有预定义 getter 但没有 setter 直接返回
if (getter && !setter) return
if (setter) {
// 有预定义 setter 则调用 setter
setter.call(obj, newVal)
} else {
// 否则直接更新新值
val = newVal
}
// 判断是否深度观察,并将新赋值的子对象属性全部转换成 getter/setter,返回子观察对象
childOb = !shallow && observe(newVal)
// 触发依赖对象的 notify() 派发通知所有依赖更新
dep.notify()
}
})
}
依赖收集
依赖收集由Dep对象来完成
每个需要收集依赖的对象属性,都会创建一个相应的dep实例,并收集watchers保存到其subs数组中
对象响应式属性的依赖收集,主要是getter中的这部分代码
if (Dep.target) {
// 当前存在依赖目标则建立依赖
dep.depend()
if (childOb) {
// 如果子观察目标存在,则建立子依赖
childOb.dep.depend()
if (Array.isArray(value)) {
// 如果属性是数组,则处理数组元素的依赖收集
// 调用数组元素 e.__ob__.dep.depend()
dependArray(value)
}
}
}
这里有两个问题
Dep.target 是何时赋值的?
在mountComponent()调用时,Watcher被实例化
Watcher构造函数中调用了实例方法get(),并通过pushTarget() 将Watcher实例赋值给Dep.target
dep.depend() 的依赖收集进行了什么操作?
dep.depend()会调用Dep.target.addDep()方法,并调用dep.addSub()方法,将Watcher实例添加到观察者列表subs中
Watcher中会维护dep数组与dep.id集合,当调用addDep()方法时,会先判断dep.id是否已经在集合中,从而避免重复收集依赖
数组
数组的成员无法像对象属性一样通过Object.defineProperty()去设置 getter/setter 来监视变化,因此数组的响应式需要进行特殊的处理,通过对一系列会影响数组成员数量的原型方法进行修补,添加依赖收集与更新派发,来完成响应式处理
影响数组的待修补方法arrayMethods
- push
- pop
- shift
- unshift
- splice
- sort
- reverse
// Observer 构造函数中的处理
if (Array.isArray(value)) {
// 数组的响应式处理
if (hasProto) {
// 如果支持原型, 替换原型指向 __prototype__ 为修补后的方法
protoAugment(value, arrayMethods)
} else {
// 如果不支持原型,通过 Object.defineProperty 为数组重新定义修补后的方法
copyAugment(value, arrayMethods, arrayKeys)
}
// 为数组每一个对象创建一个 observer 实例
this.observeArray(value)
}
// src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 影响数组的待修补方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// cache original method
// 缓存数组原型上原始的处理函数
const original = arrayProto[method]
// 通过 Object.defineProperty 为新创建的数组原型对象定义修补后的数组处理方法
def(arrayMethods, method, function mutator (...args) {
// 先执行数组原型上原始的处理函数并将结果保存到 result 中
const result = original.apply(this, args)
const ob = this.__ob__
// 是否新增
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 如果新增, 调用observe()将新增的成员转化成响应式
if (inserted) ob.observeArray(inserted)
// notify change
// 调用依赖的 notify() 方法派发更新,通知观察者 Watcher 执行相应的更新操作
ob.dep.notify()
// 返回结果
return result
})
})
通过查看数组响应式处理的源码我们可以发现,除了通过修补过的七个原型方法来修改数组内容外,其他方式修改数组将不能触发响应式更新,例如通过数组下标来修改数组成员array[0] = xxx
,或者修改数组长度array.length = 1
等
Watcher
Vue中的Watcher有三种
-
Computed Watcher
- Computed Watcher是在Vue构造函数初始化调用
_init()
->initState()
->initComputed()
中创建的
- Computed Watcher是在Vue构造函数初始化调用
-
用户Watcher(侦听器)
- 用户Watcher是在Vue构造函数初始化调用
_init()
->initState()
->initWatch()
中创建的(晚于Computed Watcher)
- 用户Watcher是在Vue构造函数初始化调用
-
渲染Watcher
-
渲染Watcher是在Vue初始化调用
_init()
->vm.$mount()
->mountComponent()
的时候创建的(晚于用户Watcher)// src/core/instance/lifecycle.js // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined // 渲染 Watcher 的创建 // updateComponent 方法用于调用 render 函数并最终通过 patch 更新 DOM // isRenderWatcher 标记参数为 true new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
-
-
Watcher的实现
/** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */ export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; lazy: boolean; sync: boolean; dirty: boolean; active: boolean; deps: Array<Dep>; newDeps: Array<Dep>; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any; constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { // 如果是渲染 watcher,记录到 vm._watcher vm._watcher = this } // 记录 watcher 实例到 vm._watchers 数组中 vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { // 渲染 watcher 不传 options this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers // watcher 相关 dep 依赖对象 this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter // expOrFn 的值是函数或字符串 if (typeof expOrFn === 'function') { // 是函数时,直接赋给 getter this.getter = expOrFn } else { // 是字符串时,是侦听器中监听的属性名,例如 watch: { 'person.name': function() {}} // parsePath('person.name') 返回一个获取 person.name 的值的函数 this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } // 渲染 watcher 的 lazy 是 false, 会立即执行 get() // 计算属性 watcher 的lazy 是 true,在 render 时才会获取值 this.value = this.lazy ? undefined : this.get() } /** * Evaluate the getter, and re-collect dependencies. */ get () { // 组件 watcher 入栈 // 用于处理父子组件嵌套的情况 pushTarget(this) let value const vm = this.vm try { // 执行传入的 expOrFn // 渲染 Watcher 传入的是 updateComponent 函数,会调用 render 函数并最终通过 patch 更新 DOM value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) { traverse(value) } // 组件 watcher 实例出栈 popTarget() // 清空依赖对象相关的内容 this.cleanupDeps() } return value } ... }
总结
响应式处理过程总结
-
整个响应式处理过程是从Vue初始化
_init()
开始的- initState() 初始化vue实例的状态,并调用initData()初始化data属性
- initData() 将data属性注入vm实例,并调用observe()方法将data中的属性转换成响应式的
- observe() 是响应式处理的入口
-
observe(value)
- 判断value是否是对象,如果不是对象直接返回
- 判断value对象是否有
__ob__
属性,如果有直接返回(认为已进行过响应式转换) - 创建并返回observer对象
-
Observer
- 为value对象(通过Object.defineProperty)定义不可枚举的
__ob__
属性,用来记录当前的observer对象 - 区分是数组还是对象,并进行相应的响应式处理
- 为value对象(通过Object.defineProperty)定义不可枚举的
-
defineReactive
- 为每一个对象属性创建dep对象来收集依赖
- 如果当前属性值是对象,调用observe将其转换成响应式
- 为对象属性定义getter与setter
- getter
- 通过dep收集依赖
- 返回属性值
- setter
- 保存新值
- 调用observe()将新值转换成响应式
- 调用dep.notify()派发更新通知给watcher,调用update()更新内容到DOM
-
依赖收集
- 在watcher对象的get()方法中调用pushTarget
- 将Dep.target赋值为当前watcher实例
- 将watcher实例入栈,用来处理父子组件嵌套的情况
- 访问data中的成员的时候,即defineReactive为属性定义的getter中收集依赖
- 将属性对应的watcher添加到dep的subs数组中
- 如果有子观察对象childOb,给子观察对象收集依赖
- 在watcher对象的get()方法中调用pushTarget
-
Watcher
- 数据触发响应式更新时,dep.notify()派发更新调用watcher的update()方法
- queueWatcher()判断watcher是否被处理,如果没有的话添加queue队列中,并调用flushSchedulerQueue()
- flushSchedulerQueue()
- 触发beforeUpdate钩子函数
- 调用watcher.run()
- run() -> get() -> getter() -> updateComponent()
- 清空上一次的依赖
- 触发actived钩子函数
- 触发updated钩子函数
全局API
为一个响应式对象动态添加一个属性,该属性是否是响应式的?不是
为一个响应式对象动态添加一个响应式属性,可以使用
Vue.set()
或vm.$set()
来实现
Vue.set()
用于向一个响应式对象添加一个属性,并确保这个新属性也是响应式的,且触发视图更新
注意:对象不能是Vue实例vm,或者Vue实例的根数据对象vm.$data
-
示例
// object Vue.set(object, 'name', 'hello') // 或 vm.$set(object, 'name', 'hello') // array Vue.set(array, 0, 'world') // 或 vm.$set(array, 0, 'world')
-
定义位置
core/global-api/index.js
-
源码解析
/** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */ export function set (target: Array<any> | Object, key: any, val: any): any { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } // 判断目标对象 target 是否是数组,且参数 key 是否是合法的数组索引 if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) // 通过 splice 对 key 位置的元素进行替换 // 数组的 splice 方法已经在Vue初始化时中完成了响应式补丁处理 (array.js) target.splice(key, 1, val) return val } // 如果 key 在目标对象 target 中存在,且不是原型上的成员,则直接赋值(已经是响应式的) if (key in target && !(key in Object.prototype)) { target[key] = val return val } // 获取目标对象 target 的 __ob__ 属性 const ob = (target: any).__ob__ // 判断 target 是否是 Vue 实例,或者是否是 $data (vmCount === 1) 并抛出异常 if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } // 判断 target 是否为响应式对象 (ob是否存在) // 如果是普通对象则不做响应式处理直接返回 if (!ob) { target[key] = val return val } // 调用 defineReactive 为目标对象添加响应式属性 key 值为 val defineReactive(ob.value, key, val) // 发送通知更新视图 ob.dep.notify() return val }
Vue.delete()
用于删除对象的属性,如果对象是响应式的,确保删除能触发视图更新
主要用于避开Vue不能检测到属性被删除的限制,但是很少会使用到
注意:对象不能是Vue实例vm,或者Vue实例的根数据对象vm.$data
-
示例
Vue.delete(object, 'name') // 或 vm.$delete(object, 'name')
-
定义位置
core/global-api/index.js
-
源码解析
/** * Delete a property and trigger change if necessary. */ export function del (target: Array<any> | Object, key: any) { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`) } // 判断目标对象 target 是否是数组,且参数 key 是否是合法的数组索引 if (Array.isArray(target) && isValidArrayIndex(key)) { // 通过 splice 删除 key 位置的元素 // 数组的 splice 方法已经在Vue初始化时中完成了响应式补丁处理 (array.js) target.splice(key, 1) return } // 获取目标对象 target 的 __ob__ 属性 const ob = (target: any).__ob__ // 判断 target 是否是 Vue 实例,或者是否是 $data (vmCount === 1) 并抛出异常 if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ) return } // 判断目标对象 target 是否包含属性 key // 如果不包含则直接返回 if (!hasOwn(target, key)) { return } // 删除目标对象 target 的属性 key delete target[key] // 判断 target 是否为响应式对象 (ob是否存在) // 如果是普通对象则直接返回 if (!ob) { return } // 发送通知更新视图 ob.dep.notify() }
Vue.nextTick()
Vue更新DOM是批量异步执行的,当通过响应式方式触发DOM更新但没有完成时,无法立即获取更新后的DOM
在修改数据后立即使用nextTick()方法可以在下次DOM更新循环结束后,执行延迟回调,从而获得更新后的DOM
-
示例
Vue.nextTick(function(){}) // 或 vm.$nextTick(function(){})
-
定义位置
-
实例方法
core/instance/render.js
->core/util/next-tick.js
-
静态方法
core/global-api/index.js
->core/util/next-tick.js
-
-
源码解析
export function nextTick (cb?: Function, ctx?: Object) { // 声明 _resolve 用来保存 cb 未定义时返回新创建的 Promise 的 resolve let _resolve // 将回调函数 cb 加上 try catch 异常处理存入 callbacks 数组 callbacks.push(() => { if (cb) { // 如果 cb 有定义,则执行回调 try { cb.call(ctx) } catch (e) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { // 如果 _resolve 有定义,执行_resolve _resolve(ctx) } }) if (!pending) { pending = true // nextTick() 的核心 // 尝试在本次事件循环之后执行 flushCallbacks // 如果支持 Promise 则优先尝试使用 Promsie.then() 的方式执行微任务 // 否则非IE浏览器环境判断是否支持 MutationObserver 并使用 MutationObserver 来执行微任务 // 尝试使用 setImmediate 来执行宏任务(仅IE浏览器支持,但性能好于 setTimeout) // 最后尝试使用 setTimeout 来执行宏任务 timerFunc() } // $flow-disable-line // cb 未定义且支持 Promise 则返回一个新的 Promise,并将 resolve 保存到 _resolve 备用 if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
// timerFunc() // Here we have async deferring wrappers using microtasks. // In 2.5 we used (macro) tasks (in combination with microtasks). // However, it has subtle problems when state is changed right before repaint // (e.g. #6813, out-in transitions). // Also, using (macro) tasks in event handler would cause some weird behaviors // that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109). // So we now use microtasks everywhere, again. // A major drawback of this tradeoff is that there are some scenarios // where microtasks have too high a priority and fire in between supposedly // sequential events (e.g. #4521, #6690, which have workarounds) // or even between bubbling of the same event (#6566). let timerFunc // The nextTick behavior leverages the microtask queue, which can be accessed // via either native Promise.then or MutationObserver. // MutationObserver has wider support, however it is seriously bugged in // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It // completely stops working after triggering a few times... so, if native // Promise is available, we will use it: /* istanbul ignore next, $flow-disable-line */ if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) // In problematic UIWebViews, Promise.then doesn't completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn't being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) setTimeout(noop) } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]' )) { // Use MutationObserver where native Promise is not available, // e.g. PhantomJS, iOS7, Android 4.4 // (#6466 MutationObserver is unreliable in IE11) let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { // Fallback to setImmediate. // Technically it leverages the (macro) task queue, // but it is still a better choice than setTimeout. timerFunc = () => { setImmediate(flushCallbacks) } } else { // Fallback to setTimeout. timerFunc = () => { setTimeout(flushCallbacks, 0) } }