Vue源码解析-响应式原理
课程目标
- Vue.js的静态成员和实例成员初始化过程
vue.use()
、vue.set()
、vue.extened()等这些全局成员的创建过程- vm.$el、
vm.$data
、vm.$on、vm.$mount
等这些实例成员的创建过程
- 首次渲染的过程
- 创建完vue实例,并把数据传递给vue之后,vue内部是如何把数据渲染到页面的,后续在分析源码过程中都是基于这个过程的
- 数据响应式原理(核心)
准备工作
Vue源码的获取
- 项目地址:https://github.com/vuejs/vue
- Fork一份到自己仓库,克隆到本地,可以自己写注释提交到github
- 为什么分析Vue2.6
- 到目前为止Vue3.0的正式版本还没有发布
- 新版本发布后,现有项目不会升级到3.0,2.x患有很长一段过渡期
- 3.0项目地址:https://github.com/vuejs/vue-next
源码目录结构
vue
├─dist 打包之后的结果,包含不同版本
├─examples 示例
├─flow
├─packages
├─scripts
├─src
├─compiler 编译相关(把模板转换成render函数,render函数会创建虚拟DOM)
├─core Vue 核心库
├─components 定义vue自带的keep-alive组件
├─global-api 定义vue中的静态方法,包含vue.component()、vue.filter()、vue.extend()等
├─instance 创建vue示例的位置,定义vue的构造函数以及vue的初始化、生命周期的响应函数
├─observer 响应式机制实现的位置
├─utils 公共成员
├─vdom 虚拟DOM,vue中的虚拟DOM重写了snabbdom,增加了组件的形式
├─platforms 平台相关代码
├─web web平台下相关代码
├─compiler
├─runtime
├─server
├─util
├─entry-compiler.js 打包入口文件
├─entry-runtime.js 打包入口文件
├─entry-runtime-with-compiler.js 打包入口文件
├─entry-server-basic-renderer.js 打包入口文件
├─entry-server-renderer.js 打包入口文件
├─weex week平台下相关代码(week是vue基于移动端下开发的框架)
├─server SSR,服务端渲染
├─sfc .vue 文件编译为 js 对象(Single File Component单文件组件)
└─shared 公共的代码
了解Flow
- 官网:https://flow.org/
- JavaScript的静态类型检查器
- Flow的静态类型检车错误是通过静态类型推断实现的
- 文件开头通过
// @flow
或者/* @flow */
声明
- 文件开头通过
调试设置
打包
打包工具Rollup
- vue.js源码的打包工具使用的是Rollup,比webpack清凉
- Webpack把所有的文件当做模块,Rollup只处理js文件,更适合在Vue.js这样的库中使用
- Rollup打包不会生成冗余的代码
安装依赖
yarn
设置sourcemap
package.json文件中的script的dev脚本中添加参数
--sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:webfull-dev"
执行dev
执行打包前先删除dist目录,rollup会自动生成dist目录
yarn dev执行打包,用的是rollup,-w参数是坚挺稳健的变化,文件变化自动重新打包
结果
调试
examples的实例中引入的是vue.min.js,将其改为vue.js
打开Chrome的调试工具中的source
Vue的不同构件版本
运行
yarn build
重新打包所有文件官方文档 - 对不同构建版本的解释
dist\README.md
UMD CommonJS ES Module Full vue.js vue.common.js vue.esm.js Runtime-only vue.runtime.js vue.common.min.js vue.esm.min.js Full(Production) vue.min.js Runtime-only(Production) vue.runtime.min.js
术语
- 完整版:同时包含编译器和运行时的版本
- 编译器:用来将模板字符串编译成为JavaScript渲染函数的代码,体积大、效率低
- 运行时:用来创建Vue实例、渲染并处理虚拟DOM等的代码,体积小、效率高。基本上就是除去编译器的代码
- UMD:UMD版本通用的模块版本,支持多种模块方式。vue.js默认文件就是运行时+编译器的UMD版本
- CommonJS(cjs):CommonJS版本用来配合老的打包工具比如Browserify或webpack 1
- ES Module:从2.6开始Vue会提供两个ES Module(ESM)构建稳健,为现代打包工具提供的版本
- ESM格式被设计为可以被静态分析,所以打包工具可以利用这一点来进行“Tree-Shaking”并将用不到的代码排除最终的包
- ES6模块与CommonJS模块的差异
Runtime + Compiler VS Runtime-only
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
Hello World
</div>
<script src="../../dist/vue.js"></script>
<script>
// Compiler
// 需要编译器,把template转换成render函数
const vm = new Vue({
el: '#app',
template: '<h1>{{ msg }}</h1>',
data: {
msg: "Hello Vue"
}
})
</script>
</body>
</html>
将vue版本改为vue.runtime.js
,发现浏览器报错,提示更改为render函数或者用compiler-included build。
更改template如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
Hello World
</div>
<script src="../../dist/vue.runtime.js"></script>
<script>
// Compiler
// 需要编译器,把template转换成render函数
const vm = new Vue({
el: '#app',
// template: '<h1>{{ msg }}</h1>',
render(h) {
return h('h1', this.msg)
},
data: {
msg: "Hello Vue"
}
})
</script>
</body>
</html>
使用vue-cli创建项目时查看vue构件版本
在使用vue create projectName
创建的项目中,查看vue
的构建版本。由于Vue对webpack.config.js
进行了深度封装,所以在目录中无法看到其配置文件,但是Vue提供了命令行来查看配置文件。
vue inspect # 直接输出到控制台
vue inspect > output.js # 将执行vue inspect命令后的结果输出到output.js文件中
output.js不是一个有效的
webpack
配置文件,不能拿来直接使用。
可以看到在resolve
中的alias
中,vue-cli使用了vue.runtime.esm.js
(运行时版本,模块化为ES Module)作为构建版本,vue$
中的$
符号为精确匹配,在使用时直接使用import Vue from vue
。
runtime+compiler与runtime对比(ast:抽象语法树),由下面过程可见runtime-only性能更高。
- runtime+compiler
template -> ast -> render -> vdom ->UI
- runtime-only
render -> vdom -> UI
以上内容来自coderwhy哔哩哔哩动画第96节视频
寻找入口文件
- 查看dist/vue.js的构建过程
执行构建
yarn dev
# "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
# --environment TARGET:web-full-dev 设置环境变量TARGET
script/config.js
的执行过程- 作用:生成
rollup
构建的配置文件 - 使用环境变量
TARGET=web-full-dev
// 判断环境变量中是否有TARGET // 如果有的话,使用genConfig()生成rullup配置文件 if (process.env.TARGET) { module.exports = genConfig(process.env.TARGET) } else { // 否则获取全部配置 exports.getBuild = getConfig exports.getAllBuilds = () => Objet.keys(builds).map(genconfig) }
- 作用:生成
genConfig(name)
- 根据环境变量TARGET获取配置信息
- builds[name]获取生成配置的信息
// 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.splice(base.length + 1)) } else { return path.resolve(__dirname, '../', p) } }
结果
- 把
src/platforms/web/entry-runtime-with-compiler.js
构建成dist/vue.js
,如果设置--sourcemap
,则会生成vue.js.map
文件 src/platform
文件夹下是Vue可以构建成不同平台下使用的库,目前有weex
和web
,还有服务端渲染SSR
的库
从入口开始
- src/platform/web/entry-runtime-with-compiler.js
通过查看源码解决下面问题
- 观察以下代码,通过阅读源码,回答在页面上输出的结果
const vm = new Vue({ el: '#app', template: '<h3>Hello Template</h3>', render(h) { return h('h4', 'Hello Render') }})
阅读源码记录
- el不能是body或者html标签
- 如果没有render,把template转换成render函数
- 如果有render方法,直接调用mount挂载DOM
// 1. el 不能是 body 或者 htmlif (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to <html> or <body> - mount to normal elementsinstead.` ) return this}const options = this.$optionsif (!options.render) {// 2. 把 template/el 转换成 render 函数……}// 3. 调用 mount 方法,挂载 DOMreturn mount.call(this, el, hydrating)
- 调试代码
- 调试的方法
const vm = new Vue({ el: '#app', template: '<h3>Hello template</h3>', render (h) { return h('h4', 'Hello render') }})
Vue的构造函数在哪里
Vue的构造函数在哪?
Vue实例的成员/Vue的静态成员从哪里来的?
src/platform/web/entry-runtime-with-compiler.js
中引用了’./runtime/index
‘src/platform/web/runtime/index.js
- 设置Vue.config
- 设置平台相关的指令和组件
- 指令v-model、v-show
- 组件transition、transition-group
- 设置平台相关的
__patch__
方法(打补丁方法,对比新旧的VNode) - 设置
$mount
方法,挂载DOM
/ install platform runtime directives & components// 注册跟平台相关指令和组件extend(Vue.options.directives, platformDirectives) // 注册指令v-model、v-showextend(Vue.options.components, platformComponents) // 注册组件v-transition、v-TransitionGroup// install platform patch function// 如果为浏览器环境则返回patch,否则返回noop空函数Vue.prototype.__patch__ = inBrowser ? patch : noop// public mount methodVue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) // 渲染DOM}
src/platform/web/runtime/index.js
中引用了’core/index
‘src/core/index.js
- 定义了Vue的静态方法
initGlobalAPI(Vue)
src/core/index.js
中引用了’./instance/index
‘src/core/instance/index.js
- 定义了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()方法,初始化vminitMixin(Vue)// 注册vm的$data/$props/$set/$delete/$watchstateMixin(Vue)// 初始化事件相关方法// $on/$once/$off/$emiteventsMixin(Vue)// 初始化生命周期相关的混入方法// _update/$forceUpdate/$destroylifecycleMixin(Vue)// 混入render// $nextTick/_renderrenderMixin(Vue)
四个导出Vue的模块
src/platform/web/entry-runtime-with-compiler.js
(核心作用:增加了编译的功能)- web平台相关的入口
- 重写了平台相关的
$mount()
方法 - 除了使用
$mount
方法可以将模板字符串转换成render()
函数,还定义了Vue.compile()
方法可以将模板字符串转换成render()
函数 - 注册了
Vue.compile()
方法,传递了一个HTML字符串返回render
函数
src/platform/web/runtime/index.js
- web平台相关
- 注册和平台相关的全局指令:
v-model
、v-show
- 注册和平台相关的全局组件:
v-transition
、v-transition-group
- 全局方法:
__patch__
:把虚拟DOM转换成真实DOM$mount
:挂载方法,把DOM渲染到界面上
src/core/index.js
- 与平台无关
- 设置了Vue的静态方法,
initGlobalAPI(Vue)
- src/core/instance/index.js
- 与平台无关
- 定义了Vue构造函数,调用了
this._init(options)
方法 - 给Vue中混入了常用的实例成员
Vue的初始化
src/core/global-api/index.js
- 初始化Vue的静态方法
/* @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.'
)
}
}
// 初始化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对象,并给其扩展
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.
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/core/global-api/use.js
/* @flow */import { toArray } from '../util/index'export function initUse (Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { const installedPlugins = (this._installedPlugins || (this._installedPlugins = [])) if (installedPlugins.indexOf(plugin) > -1) { return this } // additional parameters // 把数组中的第一个元素(plugin)去除,后面的是install方法或plugin的参数的参数 const args = toArray(arguments, 1) args.unshift(this) // 把this(Vue)插入第一个元素的位置 if (typeof plugin.install === 'function') { plugin.install.apply(plugin, args) } else if (typeof plugin === 'function') { plugin.apply(null, args) } installedPlugins.push(plugin) return this }}
- src/core/global-api/mixin.js
/* @flow */import { mergeOptions } from '../util/index'export function initMixin (Vue: GlobalAPI) { Vue.mixin = function (mixin: Object) { this.options = mergeOptions(this.options, mixin) return this }}
- src/core/global-api/extend.js
/* @flow */
import { ASSET_TYPES } from 'shared/constants'
import { defineComputed, proxy } from '../instance/state'
import { extend, mergeOptions, validateComponentName } from '../util/index'
export function initExtend (Vue: GlobalAPI) {
/**
* Each instance constructor, including Vue, has a unique
* cid. This enables us to create wrapped "child
* constructors" for prototypal inheritance and cache them.
*/
Vue.cid = 0
let cid = 1
/**
* Class inheritance
*/
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
// Vue的构造函数
const Super = this
const SuperId = Super.cid
// 从缓存中加载组件的构造函数
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name
if (process.env.NODE_ENV !== 'production' && name) {
// 如果是开发环境验证组件的名称
validateComponentName(name)
}
// 组件对应的构造函数
const Sub = function VueComponent (options) {
// 调用——init()初始化
this._init(options)
}
// 原型继承自Vue
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 合并options
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated.
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor
cachedCtors[SuperId] = Sub
return Sub
}
}
- src/core/global-api/extend.js
/* @flow */
import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
// 遍历ASSET_TYPE数组,为Vue定义响应方法
// ASSET_TYPE包括了directive、component、filter
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
// Vue.component('comp', { template: '' })
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
// 把组件配置转换为组件的构造函数
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
// 全局注册,存储资源并赋值
// this.options['components']['comp'] = definition
this.options[type + 's'][id] = definition
return definition
}
}
})
}
src/core/instance/index.js
- 定义Vue的构造函数
- 初始化Vue的实例成员
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'
// 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
initMixin(Vue)—-(src/core/instance/init.js)
初始化
_init()
方法/* @flow */ import config from '../config' import { initProxy } from './proxy' import { initState } from './state' import { initRender } from './render' import { initEvents } from './events' import { mark, measure } from '../util/perf' import { initLifecycle, callHook } from './lifecycle' import { initProvide, initInjections } from './inject' import { extend, mergeOptions, formatComponentName } from '../util/index' let uid = 0 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 // 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 // create生命钩子的回调 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) { vm.$mount(vm.$options.el) } } } export function initInternalComponent (vm: Component, options: InternalComponentOptions) { const opts = vm.$options = Object.create(vm.constructor.options) // doing this because it's faster than dynamic enumeration. const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } } export function resolveConstructorOptions (Ctor: Class<Component>) { let options = Ctor.options if (Ctor.super) { const superOptions = resolveConstructorOptions(Ctor.super) const cachedSuperOptions = Ctor.superOptions if (superOptions !== cachedSuperOptions) { // super option changed, // need to resolve new options. Ctor.superOptions = superOptions // check if there are any late-modified/attached options (#4976) const modifiedOptions = resolveModifiedOptions(Ctor) // update base extend options if (modifiedOptions) { extend(Ctor.extendOptions, modifiedOptions) } options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions) if (options.name) { options.components[options.name] = Ctor } } } return options } function resolveModifiedOptions (Ctor: Class<Component>): ?Object { let modified const latest = Ctor.options const sealed = Ctor.sealedOptions for (const key in latest) { if (latest[key] !== sealed[key]) { if (!modified) modified = {} modified[key] = latest[key] } } return modified }
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
- renderMixin(Vue)
首次渲染过程
- Vue初始化完毕,开始真正的执行
- 调用new Vue()之前,已经初始化完毕
- 通过调试代码,记录首次渲染过程
数据响应式原理
参考之前的文章:模拟Vue.js响应式原理,文章中自己模拟了响应式原理,实现了简易版的响应式机制,其中的思想与方法与Vue.js源码吻合,可对照查看。
通过查看源码解决下面问题
vm.msg = { count: 0 }
,重新给属性赋值,是否是响应式的?vm.arr[0] = 4
,给数组元素赋值,视图是否会更新?vm.arr.length = 0
, 修改数组的length,视图是否会更新?vm.arr.push(4)
,视图是否会更新?
响应式处理的入口
整个响应式处理的过程是比较复杂的,下面我们先从
src/core/instance/init.js
initState(vm)
vm状态的初始化- 初始化了
_data、_props、methods
等
src/core/instance/state.js
// 数据的初始化 if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) }
initData(vm)
vm数据的初始化function initData (vm: Component) { let data = vm.$options.data // 初始化_data,组件中data是函数,调用函数返回结果 // 否则直接返回data data = vm._data = typeof data === 'function' ? 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 // 获取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重名 while (i--) { 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 // 响应式处理 observe(data, true /* asRootData */) }
src/core/observer/index.js
observe(value, asRootData)
负责为每一个Object类型的value创建一个
observer
实例export function observe (value: any, asRootData: ?boolean): Observer | void { // 判断 value 是否是对象 if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // 如果 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
对对象做响应化处理
对数组做响应化处理
export class Observer { // 观测对象 value: any; // 依赖对象 dep: Dep; // 实例计数器 vmCount: number; // number of vms that have this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() // 初始化实例的 vmCount 为0 this.vmCount = 0 // 将实例挂载到观察对象的 __ob__ 属性 def(value, '__ob__', this) // 数组的响应式处理 if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } // 为数组中的每一个对象创建一个 observer 实例 this.observeArray(value) } else { // 遍历对象中的每一个属性,转换成 setter/getter this.walk(value) } } /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk (obj: Object) { // 获取观察对象的每一个属性 const keys = Object.keys(obj) // 遍历每一个属性,设置为响应式数据 for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } /** * Observe a list of Array items. */ observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } }}
wakl(obj)
- 遍历obj的所有属性,为每一个属性调用
defineReactive()
方法,设置getter/setter
- 遍历obj的所有属性,为每一个属性调用
defineReactive()
src/core/observer/index.js
defineReactive(obj, key, val, customSetter, shallow)
- 为一个对象定义一个响应式的属性,每一个属性对应一个
dep
对象 - 如果该属性的值是对象,继续调用
observe
- 如果给属性赋新值,继续调用
observe
- 如果数据更新发送通知
- 为一个对象定义一个响应式的属性,每一个属性对应一个
对象响应式处理
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 1.为每一个属性,创建依赖对象实例
const dep = new Dep()
// 获取obj的属性描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// 提供预定义的存取器函数
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 2.判断是否递归观察子对象,并将子对象属性都转换成getter/setter,返回子观察对象
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 如果预定义的getter存在,则value等于getter调用的返回值
// 否则直接赋予属性值
const value = getter ? getter.call(obj) : val
// 如果存在当前依赖目标,即watcher对象,则建立依赖
if (Dep.target) {
// dep()添加相互的依赖
// 一个组件对应一个watcher对象
// 一个watcher会对应多个dep(要观察的属性很多)
// 我们可以手动创建多个watcher监听一个属性的变化,一个dep可以对应多个watcher
dep.depend()
// 如果子观察对象目标存在,建立子对象的依赖关系
if (childOb) {
childOb.dep.depend()
// 如果属性是数组,则特殊处理收集数组对象依赖
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// 返回属性值
return value
},
set: function reactiveSetter (newVal) {
// 如果预定义的getter存在则value等于getter调用的返回值
// 否则直接赋予属性值
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
// 如果新值等于旧值或者新旧值为NaN则不执行
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
}
// 3.如果新值是对象,观察子对象并返回子对象的observer对象
childOb = !shallow && observe(newVal)
// 4.派发更新(发布更改通知)
dep.notify()
}
})
}
数组的响应式处理
Observer的构造函数中
// 数组的响应式处理 if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } // 为数组中的每一个对象创建一个 observer 实例 this.observeArray(value) } else { // 遍历对象中的每一个属性,转换成 setter/getter this.walk(value) } // helpers /** * Augment a target Object or Array by intercepting * the prototype chain using __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]) } }
处理数组修改数据的方法
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 // 使用数组的原型创建一个新的对象(克隆数组的原型) export const arrayMethods = Object.create(arrayProto) // 修改数组元素的方法 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function (method) { // cache original method // 保存数组原方法 const original = arrayProto[method] // 调用Object.defineProperty() 重新定义修改数组的方法 def(arrayMethods, method, function mutator (...args) { // 执行数组的原始方法 const result = original.apply(this, args) // 获取数组对象的ob对象 const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } // 对插入的新元素,重新遍历数组元素设置为响应式数据 if (inserted) ob.observeArray(inserted) // notify change // 调用了修改数组的方法,调用数组的ob对象发送通知 ob.dep.notify() return result }) })
def
方法/** * Define a property. */ export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) }
Dep类
- src/core/observer/dep.js
- 依赖对象
- 记录watcher对象
- depend() —- watcher记录对应的dep
- 发布通知
- 在
defineReactive()
中的getter
中创建dep
对象,并判断Dep.target
是否有值(一会再来看看有什么时候有值得),调用dep.depend()
dep.depend()
内部调用Dep.target.addDep(this)
,也就是watcher
的addDep()
方法,它内部最后调用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
被使用
/* @flow */
import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'
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 赋值给 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]
}
Watcher类
Watcher分为三种,
Comouted Watcher
、用户Watcher
(侦听器)、渲染Watcher
渲染
Watcher
的创建时机src/core/instance/lifecycle.js
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el if (!vm.$options.render) { vm.$options.render = createEmptyVNode if (process.env.NODE_ENV !== 'production') { /* istanbul ignore if */ if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || vm.$options.el || el) { warn( 'You are using the runtime-only build of Vue where the template ' + 'compiler is not available. Either pre-compile the templates into ' + 'render functions, or use the compiler-included build.', vm ) } else { warn( 'Failed to mount component: template or render function not defined.', vm ) } } } callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { const name = vm._name const id = vm._uid const startTag = `vue-perf-start:${id}` const endTag = `vue-perf-end:${id}` mark(startTag) const vnode = vm._render() mark(endTag) measure(`vue ${name} render`, startTag, endTag) mark(startTag) vm._update(vnode, hydrating) mark(endTag) measure(`vue ${name} patch`, startTag, endTag) } } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } // 创建渲染Watcher,exOrFn为updateComponent // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) 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 }
渲染
watcher
创建的位置:lifecycle.js的mountComponent
函数中Watcher
的构造函数初始化,处理exOrFn
(渲染watcher
和侦听器处理不同,渲染watcher
为updateComponent
,对比新旧vdom
并渲染到页面上)调用
this.get()
,他里面调用pushTarget()
,然后this.getter.call(vm,vm)
(对于渲染watcher
调用updateComponent
),如果是用户watcher
会回去属性的值(触发get
操作)当数据更新时,
dep
中调用notify()
方法,notify()
中调用watcher
的update()
方法update()
中调用queueWatcher()
queueWatcher()
是一个核心方法,去除重复操作,调用flushSchedulerQueue()
刷新队列并执行watcher
flushSchedulerQueue()
中对watcher
排序,遍历所有watcher
,如果有before
,触发生命周期的钩子函数beforeUpdate
,执行wacher.run()
,它内部调用this.get()
,然后调用this.cb()
(渲染wacher
的cb
是noop
,侦听器的function
)- 组件更新的顺序是从父组件到子组件(因为先创建父组件,后创建子组件)
- 组件的用户
watcher
在渲染watcher之前运行(因为用户watcher
(initState)是在渲染watcher
(mountComponent)之前创建的) - 如果一个组件在父组件执行之前被销毁,那他应该被跳过
整个流程结束
调试响应式数据执行过程
数组响应式处理的核心过程和数组收集依赖的过程
当数组的数据改变的时候watcher的执行过程
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>observe</title> </head> <body> <div id="app"> {{ arr }} </div> <script src="../../dist/vue.js"></script> <script> const vm = new Vue({ el: '#app', data: { arr: [2, 3, 5] } }) // vm.arr.push(8) // vm.arr[0] = 100 // vm.arr.length = 0 </script> </body> </html>
回答以下问题
检测变化的注意事项
methods: { handler() { this.obj.count = 555 this.arr[0] = 1 this.arr.length = 0 this.arr.push(4) } }
转换成响应式数据
methods: { handler() { this.$set(this.obj, 'count', 555) this.$set(this.arr, 0, 1) this.arr.splice(0) } }
数据响应式原理总结
动态添加一个响应式属性
当我们给一个响应式对象,动态增加一个对象,这个属性是否为响应式?
示例代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>set</title>
</head>
<body>
<div id="app">
{{ obj.title }}
<hr>
{{ obj.name }}
<hr>
{{ arr }}
</div>
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
obj: {
title: 'Hello Vue'
},
arr: [1, 2, 3]
}
})
</script>
</body>
</html>
打开浏览器开发者模式,分别键入如下内容:
vm.obj.name = 'abc'
可以发现,动态给
obj
增加name
属性,视图并未更新,说明此时name
属性不是响应式的vm.$set(vm.obj, 'name', 'zhangsan')
可以使用
vm.$set(vm.obj, 'name', 'zhangsan')
来给响应式对象动态添加响应式属性(或者使用Vue.set())。使用vm.$set()方法改变数组的第一个元素的值:
vm.$set(vm.arr, 0, 100)
。vm.$set()官方文档不能给Vue实例或者Vue实例的跟数组对象动态的添加响应式属性。
实例方法/数据
vm.$set
定义位置
Vue.set()
- global-api/index.js
// 静态方法 set/delete/nextTick Vue.set = set Vue.delete = del Vue.nextTick = nextTick
vm.$set()
- instance/index.js
// 注册vm的$data/$props/$set/$delete/$watch // instance/state.js stateMixin(Vue) // instance/state.js Vue.prototype.$set = set Vue.prototype.$delete = del
源码
set()
方法- 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)) { target.length = Math.max(target.length, key) // 通过splice对key位置的元素进行替换 // splice在array.js进行了响应化的处理,此处的splice已经不是原生的splice方法 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直接返回,如果是$data的话其observe对象中的vmCount为1,否则为0 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 } // 把key设置为响应式属性 defineReactive(ob.value, key, val) // 发送通知 ob.dep.notify() return val }
调试
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>set</title>
</head>
<body>
<div id="app">
{{ obj.title }}
<hr>
{{ obj.name }}
<hr>
{{ arr }}
</div>
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
obj: {
title: 'Hello Vue'
},
arr: [1, 2, 3]
}
})
</script>
</body>
</html>
回顾
defineReactive
中的childOb
,给每一个响应式对象设置一个ob调用
$set
的时候,会获取ob
对象,并通过ob.dep.notify()
发送通知
vm.$delete
功能
删除对象的属性。如果对象是响应式的,确保删除能触发更新视图。这个方法主要用于避开Vue不能检测到属性被删除的限制,但是你应该很少会使用它。
注意:目标不能是一个Vue实例或者Vue实例的跟数据对象
示例
vm.$delete(vm.obj, 'title')
定义位置
Vue.delete()
- global-api/index.js
// 静态方法 set/delete/nextTick Vue.set = set Vue.delete = del Vue.nextTick = nextTick
vm.$delete()
- instance/index.js
// 注册 vm 的 $data/$props/$set/$delete/$watch stateMixin(Vue) // instance/state.js Vue.prototype.$set = set Vue.prototype.$delete = del
源码
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属性直接返回,判断依据是:key是否直接属于target属性,而不是继承来的 // 如果是继承来的或者没有这个属性,直接返回 if (!hasOwn(target, key)) { return } // 删除属性 delete target[key] if (!ob) { return } // 通过ob发送通知 ob.dep.notify() }
vm.$watch
vm.$watch(expOrFn, callback, [options])
,官方文档
功能
观察Vue实例变化的一个表达式或计算属性函数。回调函数得到的参数为新值和旧值。表达式只接受监督的键路径。对于更负责的表达式,用一个函数取代
参数
- expOrFn:要监视的$data中的属性,可以是表达式或函数
- callback:数据变化后执行的函数
- 函数:回调函数
- 对象:具有handler属性(字符串或者函数),如果该属性为字符串则methods中相应的定义
- options:可选的选项
- deep:布尔类型,深度监听
- immediate:布尔类型,是否立即执行一次回调函数
示例1
<!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"> {{ user.fullName }} </div> <script src="../../dist/vue.js"></script> <script> const vm = new Vue({ el: '#app', data: { user: { firstName: '诸葛', lastName: '亮', fullName: '' } } }) vm.$watch('user', function (newValue, oldValue) { this.user.fullName = newValue.firstName + ' ' + newValue.lastName } ) </script> </body> </html>
打开浏览器,发现并没有立即显示
诸葛亮
,此时需要添加第三个参数options,内容为:{immediate: true}
,其意味着立即执行。再次刷新页面,发现页面显示了诸葛亮。当我们需要监听vm.$data.user.firstName时,发现后续可能还要监听vm.$data.user.lastName,所以此时写多个watch是非常不方便的,此时在options中添加:deep: true,即为深度监听。不但监听user对象的变化,而且监听其内部属性的变化。此时修改firstName,发现视图也会更新。
示例2
const vm = new Vue({ el: '#app', data: { a: '1', b: '2', msg: 'Hello Vue', user: { firstName: '诸葛', lastName: '亮' } } }) // expOrFn是表达式 vm.$watch('msg', function (newVal, oldVal) { congole.log(newVal) }) vm.$watch('user.firstName', function (newVal, oldVal) { congole.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) { console.log(newVal) }, { deep: true }) // immediate是true vm.$watch('msg', function (newVal, oldVal) { console.log(newVal) }, {immediate: true})
三种类型的Watcher对象
- 没有静态方法,因为$watch方法中要使用vue的实例
- Watcher分三种:计算属性Watcher、用户Watcher(侦听器)、渲染Watcher
- 创建顺序:计算属性Watcher、用户Watcher(侦听器)、渲染Watcher
- 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()
}
}
调试
查看watcher的创建顺序
测试代码
- ```html
watcher
将断点打在src/core/observer/watcher.js中的watcher构造函数中 - 计算属性watcher ![](http://5coder.cn/img/image-20210707055015275.png) - 用户watcher(侦听器) ![](http://5coder.cn/img/image-20210707055122029.png) ![](http://5coder.cn/img/image-20210707055217763.png) - 渲染watcher ![](http://5coder.cn/img/image-20210707055333356.png) ![](http://5coder.cn/img/image-20210707055459252.png) - 查看渲染`watcher`的执行过程 - 当数据更新,`defineReactive`的`set`方法中调用`dep.notify()` - 调用`watcher`的`update()` - 调用`ququeWatcher()`,把`watcher`存入队列,如果已经存在,不重复添加 - 循环调用`flushSchedulerQueue()` - 通过nextTick(),在消息循环结束之前时候调用`flushShedulerQueue()` - 调用`watcher.run()` - 调用`watcher.get()`获取最新值 - 如果是渲染`wacher`结束 - 如果 用户`watcher`,调用`this.cb()` ## 异步更新队列-nextTick() - Vue更新DOM是异步执行的,批量的 - 在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个办法,获取更新后的DOM - `vm.$nextTick(function() { /* 操作DOM */ }` / `Vue.$nextTick(function () {})` ### vm.$nextTick()代码演示 ```html <!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' }, mounted() { this.msg = 'Hello World' this.name = 'Hello snabbdom' this.title = 'Vue.js' Vue.nextTick(() => { console.log(this.$refs.p1.textContent) }) } }) </script> </body> </html>
- ```html
vm.$nextTick()代码演示
定义位置
- 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
$nextTick()实例方法
$nextTick()静态方法
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 把 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
})
}
}
- timerFunc()
/* @flow */
/* globals MutationObserver */
import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'
export let isUsingMicroTask = false
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).
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)
}
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 把 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
})
}
}