Vue源码(未完成)
1.准备工作
1.1目标
-
vue.js的静态成员和实例的初始化过程
-
首次渲染过程的原理
-
响应式的原理
1.2准备工作
-
下载源码
git clone https://github.com/vuejs/vue.git -
安装依赖
-
开启代码地图设置
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev" -
npm run dev
1.3构建不同版本的vue
| UMD | CommonJS | ES Module | |
|---|---|---|---|
| FULL | vue.js | vue.common.js | vue.common.js |
| Runtime-only | vue.runtime.js | vue.runtime.common.js | vue.runtime.esm.js |
| Full(production) | vue.min.js | vue.common.prod.js | |
| Runtime-only(production) | vue.runtime.min.js | vue.runtime.common.prod.js |
- 名词解释
- Full:这是一个全量的包,包含编译器(
compiler)和运行时(runtime) - Compiler:编译器,负责将模版字符串(即你编写的类 html 语法的模版代码)编译为 JavaScript 语法的 render 函数。
- Runtime:负责创建 Vue 实例、渲染函数、patch 虚拟 DOM 等代码,基本上除了编译器之外的代码都属于运行时代码。
- UMD:兼容 CommonJS 和 AMD 规范,通过 CDN 引入的 vue.js 就是 UMD 规范的代码,包含编译器和运行时。
- CommonJS:典型的应用比如 nodeJS,CommonsJS 规范的包是为了给 browserify 和 webpack 1 这样旧的打包器使用的。他们默认的入口文件为
vue.runtime.common.js。 - ES Module:现代 JavaScript 规范,ES Module 规范的包是给像 webpack 2 和 rollup 这样的现代打包器使用的。这些打包器默认使用仅包含运行时的
vue.runtime.esm.js文件。
- Full:这是一个全量的包,包含编译器(
1.4寻找入口文件
-
执行构建
npm run dev # "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev" # TARGET:web-full-dev // 打包的版本 -
查看
scripts/config.js文件- 生成完整版的时候地址
src/platforms/web/entry-runtime-with-compiler.js'
- 生成完整版的时候地址
1.5目录结构
├── benchmarks 性能、基准测试
├── dist 构建打包的输出目录
├── examples 案例目录
├── flow flow 语法的类型声明
├── packages 一些额外的包,比如:负责服务端渲染的包 vue-server-renderer、配合 vue-loader 使用的的 vue-template-compiler,还有 weex 相关的
│ ├── vue-server-renderer
│ ├── vue-template-compiler
│ ├── weex-template-compiler
│ └── weex-vue-framework
├── scripts 所有的配置文件的存放位置,比如 rollup 的配置文件
├── src vue 源码目录
│ ├── compiler 编译器
│ ├── core 运行时的核心包
│ │ ├── components 全局组件,比如 keep-alive
│ │ ├── config.js 一些默认配置项
│ │ ├── global-api 全局 API,比如熟悉的:Vue.use()、Vue.component() 等
│ │ ├── instance Vue 实例相关的,比如 Vue 构造函数就在这个目录下
│ │ ├── observer 响应式原理
│ │ ├── util 工具方法
│ │ └── vdom 虚拟 DOM 相关,比如熟悉的 patch 算法就在这儿
│ ├── platforms 平台相关的编译器代码
│ │ ├── web
│ │ └── weex
│ ├── server 服务端渲染相关
├── test 测试目录
├── types TS 类型声明
2.Vue初始化过程
2.1从入口开始
-
通过查看代码判断以下输出结果
<body> <div id="app"> </div> <script src="../../dist/vue.js"></script> <script> const vm = new Vue({ el:'#app', template:'<h2>haha</h2>', render(h) { return h('h3','xixi') } }) </script> </body>//最终输出xixi -
源码笔记
-
传入的el不能Body或则html标签
-
如果没有传入render,把template转换成render函数
-
如果有render方法直接,调用mount挂载DOM
-
// $mount方法是是把把生成的dom挂载到页面上
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
// 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
if (!options.render) { // 如果没有传入render函数就把template转化成render函数
// ---------
}
// 调用mount方法渲染dom
return mount.call(this, el, hydrating)
Vue.compile = compileToFunctions
-
代码调试 从call stack中可以看到的调用位置
2.2Vue导出模块
从四个导出vue模块查看初始化流程
-
\src\platforms\web\entry-runtime-with-compiler.js
- Web平台相关入口
- 主要是处理导出完整版的代码
- 重写$mount的方法
- 挂载vue.compiler()
- 引入Vue
import Vue from './runtime/index'
-
vue\src\platforms\web\runtime\index.js
- Web平台相关入口
- 在vue.config内挂载一些内部使用的方法
- 调用extend方法,注册一些组件和指令
- 导入组件
Transition,TransitionGroup - 导入指令show,model
- 导入组件
- 在浏览器的环境下挂载patch方法,
- patch可以借鉴snabbdom的patch方法,用来对比新旧两个虚拟dom
- inBrowser判断条件,
export const inBrowser = typeof window !== 'undefined'
- 给vue中挂载$mount()
-
vue\src\core\index.js
- 通过
initGlobalAPI全局注册一些方法
- 通过
-
vue\src\core\instance\index.js
- 与平台无关
- 创建Vue搞糟函数
- 定义了构造函数,调用了
this._init(options) - 给Vue中混入了常用的实例成员
2.3 调试初始化过程
- 准备工作
<!-- 准备一份基础代码 -->
<body>
<div id="app">
{{ msg }}
</div>
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el:'#app',
data: {
msg: 'Hello Vue'
}
})
</script>
</body>
分别设置四个断点
-
开始调试
- F5刷新进入src/core/instance/index.js文件(平台无关),该文件作用式给Vue原型挂载上一些方法
- 在watch中监视Vue
- F10执行函数会添加相对应的方法
- 其中_x的方法实在模板编译的时候使用
-
初始化静态成员
-
F8进入下一个文件,调转到
initGlobalAPI(Vue)函数中,该函数作用是给vue实例上挂在一些静态成员 -
F11进入该函数
-
-
导入平台相关代码
- F8进入到
src/platforms/web/runtime/index.js文件中 - 注册平台对应的方法
- 添加了组件和指令
- 挂载了_patch和$mount方法到原型上
- F8进入到
- 入口文件
- 重写了原型上的$mount,使其可以进行模板编译
- 注入Vue.compile方法,该方法的作用手动把模板编程render函数
2.4 源码解读
2.4.1 Vue(options)
-
路径
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')
}
this._init(options)
}
// 注册vm的_init()方法初始化vm
initMixin(Vue)
// 注册vm的$data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关的方法$on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _updata/$forceUpdata/$destroy
lifecycleMixin(Vue)
// 混入render
// $nextTick/_render
renderMixin(Vue)
2.4.2 Vue.prototype._init
-
路径
src\core\instance\init.js -
作用
- 定义注vm的_init()方法
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// vm 用来记录当前vue实例
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实例,将来定义响应式数据的时候不对他经行处理
vm._isVue = true
// merge options 合并options
// 两个方法都用是吧用户传入的options和vm的构造函数的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)
// vm的编译render初始化
// $slots/$scopedSlots/_c/$createdElemnet/$attrs
initRender(vm)
// 触发声明周期的钩子,这里式beforeCreate
callHook(vm, 'beforeCreate')
// 把inject的成员注入到vm上
initInjections(vm) // resolve injections before data/props
initState(vm)
// 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上 依赖注入
initProvide(vm) // resolve provide after data/props
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)
}
}
}
2.4.3 initInternalComponent()
-
路径
src\core\instance\init.js -
作用
- 对组件进行优化处理
- 对组件选项进行打平做性能优化,当组件有嵌套很多层,避免免不了要通过原型链进行动态查找,影响执行效率。
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
// 根据vm的构造函数创建新的配置$options对象,即
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
// 把当前组件配置项打平然后赋值到 $options 对象,避免了原型链的动态查找
const vnodeComponentOptions = parentVnode.componentOptions
opts.propsData = vnodeComponentOptions.propsData
opts._parentListeners = vnodeComponentOptions.listeners
opts._renderChildren = vnodeComponentOptions.children
opts._componentTag = vnodeComponentOptions.tag
//如果当前组件配置项中存在render选项,就把它添加到 $options对象上
if (options.render) {
opts.render = options.render
opts.staticRenderFns = options.staticRenderFns
}
}
2.4.4 resolveConstructorOptions()
-
路径
src\core\instance\init.js -
作用
- 从组件构造函数中解析配置对象 options,并合并基类选项。
export function resolveConstructorOptions (Ctor: Class<Component>) {
// 配置项目
let options = Ctor.options
// 如果构造函数的super属性存在,存在基类,递归解析基类构造函数的选项
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// 说明基类构造函数选项已经发生改变,需要重新设置
Ctor.superOptions = superOptions
// 检查 Ctor.options 上是否有任何后期修改/附加的选项
const modifiedOptions = resolveModifiedOptions(Ctor)
// 如果存在被修改或增加的选项,则合并两个选项
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
// 选项合并,将合并结果赋值为 Ctor.options
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}
2.4.5 mergeOptions()
-
路径
src\core\util\options.js -
作用
- 合并两个选项,出现相同配置项时,子选项会覆盖父选项的配置
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)
}
if (typeof child === 'function') {
child = child.options
}
// 标准化 props、inject、directive 选项,入参的时候可以数组或对象
normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)
// 处理原始 child 对象上的 extends 和 mixins,分别执行 mergeOptions,将这些继承而来的选项合并到 parent
// mergeOptions 处理过的对象会含有 _base 属性
if (!child._base) {
if (child.extends) {
parent = mergeOptions(parent, child.extends, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
}
const options = {}
let key
// 遍历 父选项
for (key in parent) {
mergeField(key)
}
// 遍历 子选项,如果父选项不存在该配置,则合并,否则跳过,因为父子拥有同一个属性的情况在上面处理父选项时已经处理过了,用的子选项的值
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
// 合并选项,childVal 优先级高于 parentVal
function mergeField (key) {
// strats = Object.create(null)
const strat = strats[key] || defaultStrat
// 值为如果 childVal 存在则优先使用 childVal,否则使用 parentVal
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
2.4.6 initGlobalAPI(vue)
- 路径:
src\core\global-api\index.js - 作用
- initGlobalAPI 是初始化全局API的,它是先于new Vue执行的,提供全局方法在Vue的执行过程中执行相应的方法。
- 定义vue.options
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对象,并且扩展'component','directive','filter'三个属性
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
//extend将builtInComponents复制到Vue.options.components
// 导入keepalive组件
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)
}
2.5调试Vue.init
- 设置断点,Watch中监视vm
- 在mergeOptions中合并了 原有的options和传入的options
- 在开发环境中F11进入到initProxy
- 判断浏览器是否支持proxy给 vm._renderProxy赋值
- 设置断点vm.$mount,F11进入
- 处理得到传入的template
- 运行到
return mount.call(this, el, hydrating)F11进入函数- 这里会重在runtime/index.js中主要是重新获取el(处理运行时版本没有编译器)
- 调用mountComponent函数,F11进入该函数
- 在mountComponent中,每个组件都会创建一个new Watcher 设置断点进入到new Watcher
- 在创建的观察者中,在get中触发getter的方法的时候就会渲染数据
2.6挂载数据相关源码
2.6.1Vue.prototype.$mount()
- 位置:
src\platforms\web\entry-runtime-with-compiler.js- 作用定义Vue.prototype.$mount()
// $mount方法是是把把生成的dom挂载到页面上
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
// 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
// 获取options处理得到对应的templiate
if (!options.render) { // 如果没有传入render函数就把template转化成render函数
let template = options.template
if (template) { // 如果模存在
if (typeof template === 'string') {
if (template.charAt(0) === '#') { // 判断模板是id选择器
template = idToTemplate(template) // 把id转化成DOM元素获取里面的innerHTML作为模板
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
// 如果模板是元素,返回元素的innerHTML
template = template.innerHTML
} else { // 都没有的话就抛出警告
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 如果没有tempalte,获取el的outerHTML作为模板
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 将模板编译成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
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 调用mount方法渲染dom,注意这里在runtime/index.js中会写获取el
return mount.call(this, el, hydrating)
}
2.6.2mountComponent()
- 路径:
src\core\instance\lifecycle.js - 作用
- 先判断 vm.$options.render 是否存在,如果不存在的话就让它等于 createEmptyVNode
- 接着定义了 updateComponent 函数
- 触发beforeMount生命周期钩子
- 创建了一个渲染 Watcher
- 触发beforeMount生命周期钩子
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 判断选项中有没有render函数
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
)
}
}
}
// 触发beforeMount生命周期钩子
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 = () => {
// 调用用户传入的render或编译器生成的render,并返回一个虚拟dom并返回给_updata
// _updata就是把虚拟dom转化未真实dom
// 执行到这里后会把模板渲染到节面上
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对象
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
// 触发mounted生命周期钩子
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
2.7初始化过程思维导图
3数据响应式原理
3.1从入口文件开始
-
思考问题
- vm.msg = { count : 0 },给属性中心赋值是否时响应式
- vm.arr[0] = 4 ,给数组元素赋值,视图是否会更新
- vm.arr.length = 0 ,修改数组的length,视图是否会更新
- vm.arr.push(4),视图是否会更新
-
响应式处理入口
- 位置
core\instance\state.js
- 位置
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
- 设置断点 在
3.2调试依赖收集过程
- 准备工作
- 收集依赖过程,就是收集模板Observe中使用到对应组件的Watcher添加到Dep的subs数组中,只要数据法发生变化就通知Watcher去更新视图
<body>
<div id="app">
<h1>{{msg}}</h1>
{{msg}}
<hr>
{{count}}
</div>
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el:'#app',
data: {
msg: 'Hello Vue',
count: 100
}
})
</script>
</body>
-
设置断点
src\core\instance\lifecycle.js中mountComponent()创建的New Warcher- 其中知道关注的式这个updateComponen
- 进入Wacher,并构造数内会执行自身的get方法,右边式函数执行过程,进入
pushTarget
- 在
pushTarget中会把Watcher会把对象压入到一个栈中,且赋值给Deg.target
- 值得注意在Watcher.get.try中执行的getter指向的是updateCompent,而updateCompent内部调用了
vm._update(vm._render(), hydrating),先是生成虚拟dom然后在生成真实DOM - 进入到
vm._render()函数中这个最终会调用render()函数,render可由用传入或模板编译而成
- 模板中F11进入到hasHandler函数中,该函数是用来判断vue实例中是否有 _ c、_ v、msg等成员
- 当判断是否有msg的时候,会触发proxyGetter, 访问_data中的msg
- 然后进入_data属性中的msg的get方法
- 在这个get方法中会收集依赖,在watchet对象中已经给Dep.target赋值了所以进入Dep.depend方法内
- depend()内会调用Dep.target.addDep(this),此时Dep.target指向就是原来的Watcher对象,this指向msg的dep对象
- 在Watcher对象中的appdep就是判断,Dep.subs内是否有对应的Watcher,没有就那wancher对象添加到subs数组中,度过有就跳过
3.3源码解读
3.3.1 initState(vm)
- 路径
src\core\instance\state.js - 作用
- 初始化data,props,methods,watch,computed等
- 注意:初始化的顺序很重要,这就标志着后面初始化的选项名称不能与已初始化的重复。
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
3.3.2 initProps(vm, opts.props)
- 路径:
src\core\instance\state.js - 作用:处理 props 对象,为 props 对象的每个属性设置响应式,并将其代理到 vm 实例上
function initProps (vm: Component, propsOptions: Object) {
//存放父组件传入子组件的props
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
// 存放porps的keys
const keys = vm.$options._propKeys = []
// 是否是是根
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
// 获取每个popps[key]的对应值
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
// 为 props 的每个 key 是设置数据响应式
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
// 将props中的每个属性设为响应式
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
// 代理 key 到 vm 对象上
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
3.3.3 initMethods()
- 路径
src\core\instance\state.js - 作用
- 校验 methoss[key]必须是一个函数
- 查重,props和自身方法对比
-
遍历$options的传入methods,把每一个method绑定到当前vue实例上,并将method的this指向当前vue实例
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
// 判断类型
if (process.env.NODE_ENV !== 'production') {
if (typeof methods[key] !== 'function') {
warn(
`Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
`Did you reference the function correctly?`,
vm
)
}
//查重
// 和propskey对比
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
// 和自身方法对比
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
// noop空函数,绑定函数且改变this指向
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
3.3.4 initData(vm)
- 路径
src\core\instance\state.js - 作用
- 获取data
- 查重
- 代理 data 对象上的属性到 vm 实例
- 为 data 对象的上数据设置响应式
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中的所有属性,获取prosps和methods
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
// 判断属性名称和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)) {
// 把data中的成员注入到vue实例中
proxy(vm, `_data`, key)
}
}
// observe data
// 响应式处理, true式说明式根数据
observe(data, true /* asRootData */)
}
3.3.5 observe()
- 地址:
src\core\observer\index.js - 作用:为对象创建观察者实例,如果对象已经被观察过,则返回已有的观察者实例,否则创建新的观察者实例
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 判断Value是否式式对象或是否是Vnode,是的话就不需要处理
if (!isObject(value) || value instanceof VNode) {
return
}
// ob就是Obsever的实例
let ob: Observer | void
// 如果value中存在_ob_(observer对象)这个属性,就直接赋值给ob返回
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
}
3.3.6 Observer [观察者类]
-
位置:
src\core\observer\index.js -
作用:
- 定义观察者类
- 定义
_ob_属性,吧Observer对象记录下来,并且设置不可枚举 - 给对象中每个属性设置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()
// 实例计数器
this.vmCount = 0
// def.Object.defineProperty经行封装,设置属性不可枚举不可枚举
// 在 value 对象上设置 __ob__ 属性
def(value, '__ob__', this)
// 数组响应式处理(后面分析)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
// 为数组中每个对象创建一个observer实例
this.observeArray(value)
} else {
// 遍历每一个对象,装换成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++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
// 遍历每一个属性,设置为响应式数据
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
3.3.7defineReactive()
- 位置:
src\core\observer\index.js - 作用:
- 收集依赖,发送通知
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean // 当shallow为true的时候值监听第一层属性,false时为深度监听
) {
// 创建调度中心,用来收集所有依赖Watcher
const dep = new Dep()
// 获取obj的属性描述符,如果时不可以配置的就直接返回,应为后面会重写
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
// 提供预定义的存取器函数
// 记录 getter 和 setter,获取 val 值
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 判断是否需要递归观察子对象,并将子对象的属性转换成getter/setter,返回子观察对象
let childOb = !shallow && observe(val)
// 响应式核心
Object.defineProperty(obj, key, {
enumerable: true, // 可枚举
configurable: true, // 可配置
get: function reactiveGetter () {
// 如果预定义的getter存在则value等于getter调用的返回值
// 否则赋予属性值·
const value = getter ? getter.call(obj) : val
// 这里主要经行依赖收集,把依赖该属性的watcher对象添加搭配dep.sub数组中
// 当数据发生变化的时候通知所有watcher
// Dep.target实在Watcher内收集依赖的
if (Dep.target) {
dep.depend()
// childOb 表示对象中嵌套对象的观察者对象,如果存在也对其进行依赖收集
if (childOb) {
// 如果子观察目标存在,建立子对象的依赖关系
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
// 当新值等于就职,或者新旧值为NaN的时候直接返回,注意的式js中NaN != 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
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 如果新值也是个对象观察子对象并返回,子obsever对象
childOb = !shallow && observe(newVal)
// 派发更新
dep.notify()
}
})
}
3.3.8Dep [调度中心类]
- 位置:
src\core\observer\dep.js - 作用:
- 依赖管理,收集依赖,发送通知
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 添加
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 调用watcher中的addDep
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
// Dep.target 用来存放目前正在使用的Watcher
// 全局唯一,并且一次也只能有一个watcher被使用
Dep.target = null
const targetStack = []
// 这里要注意的的是入栈并将当前的watcher赋值给Dep.target
// 每个组件会创建一个wathce对象
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
// 出栈
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
3.3.9Wather[观察者类]
- 位置:
src\core\observer\watcher.js - 作用:
- 依赖管理,收集依赖,发送通知
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) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
const info = `callback for watcher "${this.expression}"`
invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
4.异步更新
4.1 数组响应式原理调试
- 调试响应式数据执行过程
- 数据响应式处理的核心过程和数组收集依赖的过程
- 注意数组中的属性并不是响应式
- 当属组的数据改变的时候watcher的执行过程
- 数据响应式处理的核心过程和数组收集依赖的过程
- 准备工作
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<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]
}
})
</script>
</body>
</html>
- 设置断点
- 首先创建响应式数据,传入的data是一个对象会进入Walk方法
- walk方法会遍历对象中的每个属性并执行defineReactive()
- defineReactive()内,值得注意的是初次传入的data是一个对象,所以会递归监听
- Observe类中会判断入参data是不是数组,如果是数组的化会重写数组的方法
- 执行完后还会执行observeArray()递归监听
- 下一步会进入到defineProperty
- 会对每一个属性创建一个dep,为这个属性收集依赖
- 接着调用dep.depend(),会把这个dep对象添加到subs数组中
- 注意的是这里会收集两次依赖,
- 第一个是为了当前属性收集依赖
- 对数组对象收集依赖,当数组对象发生变化的时候会通知Watcher
- 如果当前value是数组的化会进入dependArray()
- dependArray() 会对数组内的元素处理
- 数组中元素是对象被这回执行收集依赖
- 数组中元素是数组就对递归调用
- 到这个数组收集依赖的过程就完成了
- 查看数组数据发生改变时的执行过程
- 设置断点再Dep类的notify()
- 再控制到Console中改变数据,
vm.arr.push(10)
- 回车后进入sub.notify()
- 收件会拷贝一根subs,防止后续再操作中会更新这个数组
- 遍历subs数组中的watcher对象,调用Watcher.update()
-
进入到queueWatcher方法内
- 首先会记录这个Watcher的id,然后去判断id是都,再队列象内,没有的化证明数组未被处理过
- 然后判断是否再刷新队列,不是的化就把这个watcher对象压入到queue队列中
- 然后再nextTick把刷新队列的flushSecheduleQueue()传入
-
在flushSecheduleQueue方法中
- 在方法中首先会对Wacher对象进行排序
- 遍历队列
- 触发beforeUpdata()钩子函数
- 执行Wacher.run()
- 在run方法内最终回到
vm._update(vm._render(), hydrating)去更新视图 - 后续就执行有些清除和reset工作
4.2源码解读
4.2.1 dep.notify
- 路径
/src/core/observer/watcher.js - 作用
- 通知 dep 中的所有 watcher,执行 watcher.update() 方法
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
// 遍历 dep 中存储的 watcher,执行 watcher.update()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
4.2.1 wather.update
- 路径
src\core\observer\watcher.js - 作用
- 通知 dep 中的所有 watcher,执行 watcher.update() 方法
update () {
/* istanbul ignore else */
if (this.lazy) {
// 懒执行时会走导致,比如computed
// 将dirty设置为true,在组件更行之后,当响应式数据在此被更新的时候,执行computed Getter
// 重新执行computed回调函数,计算啊下新值,然后换缓存到 Watcer.value
this.dirty = true
} else if (this.sync) {
// 当同步执行会走这
// 比如this.$watch()
this.run()
} else {
// 当时渲染Wathcer的时候会进入到这里来
queueWatcher(this)
}
}
4.2.3 queueWatcher()
- 位置
src\core\observer\scheduler.js - 作用:
- 将 watcher 放入 watcher 队列
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 如果 watcher 已经存在,则跳过,不会重复入队
if (has[id] == null) {
// 缓存 watcher.id,用于判断 watcher 是否已经入队
has[id] = true
if (!flushing) {
// 当前没有处于刷新队列状态,watcher 直接入队
queue.push(watcher)
} else {
// 已经在刷新队列了
// 从队列末尾开始倒序遍历,根据当前 watcher.id 找到它大于的 watcher.id 的位置,然后将自己插入到该位置之后的下一个位置
// 即将当前 watcher 放入已排序的队列中,且队列仍是有序的
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
// 直接刷新调度队列
flushSchedulerQueue()
return
}
/**
* 熟悉的 nextTick => vm.$nextTick、Vue.nextTick
* 1、将 回调函数(flushSchedulerQueue) 放入 callbacks 数组
* 2、通过 pending 控制向浏览器任务队列中添加 flushCallbacks 函数
*/
nextTick(flushSchedulerQueue)
}
}
}
4.2.4 nextTick
- 位置
/src/core/util/next-tick.js - 作用:
- 用 try catch 包装 flushSchedulerQueue 函数,然后将其放入 callbacks 数组
- 调用timerFunc()将异步任务压入到浏览器的异步任务队列中
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 将回调函数用trycatch 包装一层,方便一场捕获
// 然后将 包装后的函数 放到这个callback数组
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// pedding是一flag 确保却列中异步任务队列中只有一个flushCallbacks函数
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
4.2.5 timerFunc
- 位置
/src/core/util/next-tick.js - 作用:
- 将 flushCallbacks 函数放入浏览器的异步任务队列中,根据浏览器的支持优雅降级
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
// 首选 Promise.resolve().then()
timerFunc = () => {
// 在 微任务队列 中放入 flushCallbacks 函数
p.then(flushCallbacks)
/**
* 在有问题的UIWebViews中,Promise.then不会完全中断,但是它可能会陷入怪异的状态,
* 在这种状态下,回调被推入微任务队列,但队列没有被刷新,直到浏览器需要执行其他工作,例如处理一个计时器。
* 因此,我们可以通过添加空计时器来“强制”刷新微任务队列。
*/
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// MutationObserver 次之
// 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)) {
// 再就是 setImmediate,它其实已经是一个宏任务了,但仍然比 setTimeout 要好
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
// 最后没办法,则使用 setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
4.2.6 flushCallbacks
- 位置
/src/core/util/next-tick.js - 作用:
- 将Pending再次职位false,表示下一个flushCallbacks函数可以进入浏览的异步任务队列
- 清空callback数组
- 执行callback数组中的函数
- flushSchedulerQueue中的传入函数
- 用户自己调用的this.$nextTick传递回调函数
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
4.2.7 flushSchedulerQueue()
- 位置
src\core\observer\scheduler.js - 作用:
- 更新 flushing 为 ture,表示正在刷新队列,在此期间往队列中 push 新的 watcher 时需要特殊处理(将其放在队列的合适位置)
- 按照队列中的 watcher.id 从小到大排序,保证先创建的 watcher 先执行,也配合 第一步
- 遍历 watcher 队列,依次执行 watcher.before、watcher.run
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// ==> 组件的更新顺序是由父组件到子组件,因为我们的创建顺序就是由父到子
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// ==> 组件的用户Watcher要在渲染Watcher之前执行
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
// ==> 如果一个组件在他执行他的父组件前被销毁了那这个Watcher应该被跳过
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
// 这里直接使用了 queue.length,动态计算队列的长度,没有缓存长度,是因为在执行现有 watcher 期间队列中可能会被 push 进新的 watcher
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
// 渲染Watcher去触发beforeDeta的钩子
watcher.before()
}
// 清除ID,下次可以被调用
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
4.2.8 wacher.run
- 位置
/src/core/observer/watcher.js - 作用:
- 由 刷新队列函数 flushSchedulerQueue 调用,如果是同步 watch,则由 this.update 直接调用,
- 执行实例化 watcher 传递的第二个参数,updateComponent 或者 获取 this.xx 的一个函数(parsePath 返回的函数)
- 更新旧值为新值
- 执行实例化 watcher 时传递的第三个参数,比如用户 watcher 的回调函数
run () {
if (this.active) {
// 调用 this.get 方法
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// 更新旧值为新值
const oldValue = this.value
this.value = value
if (this.user) {
// 如果是用户 watcher,则执行用户传递的第三个参数 —— 回调函数,参数为 val 和 oldVal
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
// 渲染 watcher,this.cb = noop,一个空函数
this.cb.call(this.vm, value, oldValue)
}
}
}
}
4.2.8 wacher.get
- 位置
/src/core/observer/watcher.js - 作用:
- 执行 this.getter,并重新收集依赖
- 因为触发更新说明有响应式数据被更新了,但是被更新的数据虽然已经经过 observe 观察了,但是却没有进行依赖收集
- 所以,在更新页面时,会重新执行一次 render 函数,执行期间会触发读取操作,这时候进行依赖收集
- this.getter 是实例化 watcher 时传递的第二个参数,一个函数或者字符串,比如:updateComponent 或者 parsePath 返回的函数
- 执行 this.getter,并重新收集依赖
get () {
pushTarget(this)
// value 为回调函数执行的结果
let value
const vm = this.vm
try {
// 执行回调函数,比如 updateComponent,进入 patch 阶段
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
5.全局API
5.1源码解读
5.1.1 global-api(入口文件)
- 参考2.4.6
5.1.2 Vue.util
- 向外暴露一些内置方法
// 这些工具方法不视为全局Api的一部分,不用然会有风险
Vue.util = {
// 日志方法
warn,
// 将A对象的属性复制到B对象上
extend,
// 合并选项
mergeOptions,
// 给对象设置getter、setter,涉及到依赖收集,更新触发依赖方法
defineReactive
}
- extend
/**
* Mix properties into target object.
*/
export function extend (to: Object, _from: ?Object): Object {
for (const key in _from) {
to[key] = _from[key]
}
return to
}
- mergeOptions, defineReactive参考之气那内容
5.1.3 Vue.set
-
位置
\src\core\observer\index.js -
作用:
- 给对象或属性设置响应式数据
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)}`)
}
// 处理数组,Vue.set(arr,idx,val)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// 利用splice去实现
target.splice(key, 1, val)
return val
}
// 处理对象的情况
// 如果属性原来存在,就直接更改值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
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
}
if (!ob) {
target[key] = val
return val
}
// 给新属性设置getter 和 setter 并设置成响应式数据
defineReactive(ob.value, key, val)
// 依赖通知更新
ob.dep.notify()
return val
}
5.1.4 Vue.delete
-
位置
\src\core\observer\index.js -
作用:
- 删除一个属性并触发响应式
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)}`)
}
// 处理数组情况
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1)
return
}
// 处理对象
const ob = (target: any).__ob__
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
}
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}
5.1.5 Vue.nextTick
- 参考上文4.2.4
5.1.6 Vue.use
- 位置
src\core\global-api\use.js - 作用 执行插件中暴露出来的install方法
/* @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
// 把参数转成数组并且把第一个参数去除
const args = toArray(arguments, 1)
// 把this(vue)插入到第一个元素中
// 这里就是如果插件是一个对象,必须提供 install 方法。如果插件是一个函数,
// 它会被作为 install 方法。install 方法调用时,会将 Vue 作为参数传入。
args.unshift(this)
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
// 把插件存储到数组中
installedPlugins.push(plugin)
return this
}
}
5.1.7 Vue.mixin
- 位置:
src\core\global-api\mixin.js - 作用:合并两个option
import { mergeOptions } from '../util/index'
// 实际就是利用mergeOptions方法
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
5.1.8Vue.extend
- 位置:
src\core\global-api\extend.js - 作用:扩展Vue子类。预设一些配置项
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
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)
}
// 定义一个Vue的子类
const Sub = function VueComponent (options) {
this._init(options)
}
// 设置子类的原型对象
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
// 合并传入的选项和基础类的选项
// 可以通过Vue.extend 方法定义一个子类,预设一些配置象,当使用Vue构造数的时候可以使用
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.
// 将props 和computed代理到子类上,在子类通过this.XX的方式访问
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
}
}