学习目标
- 搭建整体开发环境
- vue整体初始化流程
- 数据响应式原理
学习开发环境
获取vue项目
项目地址:https://github.com/vuejs/vue
当前版本:2.6.10
文件结构
命名注意事项:
runtime
:不带编译器的版本;
common
:commonjs打包规范来自node.js,require,exports常用与后端,同步的,老旧版本的打包器browsify,webpack1.0;
amd
:requirejs,专用与浏览器;
esm
:es module 规范,常用的规范是import export,webpack2.0及以上;
umd
:universal module definition;
入口文件怎么找?
打开vue 源码里的package.json
文件
{
"main": "dist/vue.runtime.common.js",
"module": "dist/vue.runtime.esm.js", //用webpack2.0以上
"unpkg": "dist/vue.js", //用浏览器
"jsdelivr": "dist/vue.js",
"typings": "types/index.d.ts",
}
调试环境
- 安装依赖:npm - i;
- 安装rollup:npm i -g rollup;
- 修改dev脚本,将"dev"改为一下:
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
- 执行开发命令npm run dev
我们如何通过调试代码看vue是怎么运行的呢?
我们把 <script src="../../dist/vue.min.js"></script>
改为<script src="../../dist/vue.js"></script>
。
我们通过浏览器打开上图所示index.html
,打开浏览器的调试模式(F12),切换到Sources,在Page目录下找到src,如下图:
如果能看到上图所示,说明调式模式已经成功了,基本源码结构都有了,我们在调试的时候可以很方便的看代码了,我们可以依照下图打开app.js,因为它是入口文件,并且在38行打个断点。
我们刷新一下,页面就响应了,点击一下图的按钮,就可以进行下一步了:
当我们执行到下图步骤时,就走到vue的构造函数了,如果你之前对构造涵数一无所知,通过这个方式就可以开始进行了源码的学习过程:
我们可以通过鼠标右键定位到具体的目标文件,如下图所示:
如何找入口文件?
我们可以在package.JSON文件里的:
找到这一行代码
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
通过代码我们知道它的输出目标web-full-dev
,我们可以config.js文件找答案,其实config.js是rollup的配置文件,我们不需要去关注它的细节,我们的关注点可以从打开文件之后从38行 builds 对象开始,它是所有创建目标的描述对象,如下图所示:
我们可以在这个对象里找到了web-full-dev
这一项
//Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
通过参数的传递我们可以很明确的知道我们用的是那一项的配置文件,通过注释我们可以很清楚的知道它将来运行于浏览器,同时我们也知道它的入口文件是web/entry-runtime-with-compiler.js
,出口文件是dist/vue.js
,但是需要注意的是我们通过web/entry-runtime-with-compiler.js
路径并没有找到web
这个目录,我们可以通过resolve
这个方法可以找到这串代码:
const aliases = require('./alias')
const resolve = p => {
const base = p.split('/')[0]
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
return path.resolve(__dirname, '../', p)
}
}
我们找找到const aliases = require('./alias')
这个目录下的文件,
const path = require('path')
const resolve = p => path.resolve(__dirname, '../', p)
module.exports = {
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
compiler: resolve('src/compiler'),
core: resolve('src/core'),
shared: resolve('src/shared'),
web: resolve('src/platforms/web'),
weex: resolve('src/platforms/weex'),
server: resolve('src/server'),
sfc: resolve('src/sfc')
}
通过这个web: resolve('src/platforms/web')
这段代码我们才知道,它的真正路径是怎么来的,通过一番折腾我们终于找到了src/platforms/web/entry-runtime-with-compiler.js
的入口文件。
整个vue的初始化流程
我们打开entry-runtime-with-compiler.js
文件,里面代码虽然很多,但关键代码不多,我们看到第7行代码:
import Vue from './runtime/index'
这是我们有点纳闷,这代码是干什么的 ?我们先不管它,我们继完下走。
el,template,render,$mount的优先级
我们在写vue的时候通常写成以下形式:
new Vue({
el:'#app'
template:'<app></app>'
render:h => h(App)
}).$mount('#app')
现在问题来,这几个选项“el,template,render,$mount”同时设置,谁的优先级高?
entry-runtime-with-compiler.js
文件里的以下代码就为我们揭开答案:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,//拿到的数据有可能是两者,#app,app
hydrating?: boolean
):): Component {
//{1}
el = el && query(el)
//{2}
const options = this.$options;
if (!options.render) {
let template = options.template
if (template) {
//{3} 解析template选项
if (typeof template === 'string') {
//{4}
if (template.charAt(0) === '#') {
//{5}
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
//{6}如果传递dom,获取其内部模板内容,
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) { {7}
//{8} 否则解析el选项
template = getOuterHTML(el)
}
//{9}编译过程
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
//{10} 编译得到render函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
//{11}直到到选项中
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
//{12}挂载:生成vdom,生成dom,追加到el
// 执行默认$mount函数
return mount.call(this, el, hydrating)
}
核心代码从第17行开始,首先把Vue里的$mount
拿下来,核心是覆盖$mount
方法,function
里的参数可能有两种类型,一种是选择器,如#app,另一种是app元素,最终$mount
会返回一个组件,组件:
{1}、获取dom元素;
{2}、尝试解析配置;
我们发现一个if语句都是在处理没有render
的情况,如果有render
就不会执行if函数了,所以我们可以得之,
render的优先级是非常高的,一旦设置了render
,template
的都不管用了,不过el
还有可能起作用。
我们继续看代码:
{3}、判断template
是字符串;
{4}、当template
字符串第一个字符是”#“的时候,说明template
选择器;
{5}、当template是选择器的时候,掉idToTemplate
方法,获取字符串模板;
{6}、如果传递dom,获取其内部模板内容;
{7}、如果用户没有设置template的
情况下,el
生效,所以有得到一个结论 el选项的优先级最低如果设置了template
,el
不生效;
{8}、当是在el
的情况下,掉getOuterHTML
方法,把带着这个标签全部做为这个模板;
{9}、整个模板处理完成之后,开始编译模板,进入了编译过程;
{10}、调compileToFunctions
方法,把template
做为参数,生成render
函数;
{11}、把生成的render
函数直接指定到配置项中;
{12}、执行挂载:生成vdom
,生成dom
,追加到el
;
总结
entry-runtime-with-compiler.js
文件的作用是覆盖$mount,
解析template
选项并编译之。
核心代码
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
//{11}直到到选项中
options.render = render
解读import Vue from './runtime/index
’
接下来我们来研究一开始的第7行代码是干什么的,我们进入runtime 目录下的index文件;
/* @flow */
import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'
import {
query,
mustUseProp,
isReservedTag,
isReservedAttr,
getTagNamespace,
isUnknownElement
} from 'web/util/index'
import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'
// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// install platform patch function
//实现$mount,核心就一个mountComponent;定义一个__patch__方法
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
//{12}定义$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// devtools global hook
/* istanbul ignore next */
if (inBrowser) {
setTimeout(() => {
if (config.devtools) {
if (devtools) {
devtools.emit('init', Vue)
} else if (
process.env.NODE_ENV !== 'production' &&
process.env.NODE_ENV !== 'test'
) {
console[console.info ? 'info' : 'log'](
'Download the Vue Devtools extension for a better development experience:\n' +
'https://github.com/vuejs/vue-devtools'
)
}
}
if (process.env.NODE_ENV !== 'production' &&
process.env.NODE_ENV !== 'test' &&
config.productionTip !== false &&
typeof console !== 'undefined'
) {
console[console.info ? 'info' : 'log'](
`You are running Vue in development mode.\n` +
`Make sure to turn on production mode when deploying for production.\n` +
`See more tips at https://vuejs.org/guide/deployment.html`
)
}
}, 0)
}
export default Vue
{12}是最重要的代码,它的核心是定义$mount;
此时我们发现Vue不是在此文件定义的,而是从以下文件导出来的:
import Vue from 'core/index'
我们继续深挖次文件,我们发现次文件已经不在platforms
文件了,平台差异性的代码在这里,而核心代码在“core”里,我们在code
文件夹里打开index.js
,我们开始真正的进入了核心了:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
//{13} 定义的全局api,如Vue.set/minimx
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue
{13}初始化全局api
,我们常见的方法,如Vue.set/minimx/use/extend
,都在此声明的,要想了解这些方法,这个就是我们的入口点。
所以,core/index
是作用就是定义全局的api,核心代码是:
initGlobalAPI(Vue)
我们再进入initGlobalAPI
方法所在的文件,代码如下:
/* @flow */
import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'
import {
warn,
extend,
nextTick,
mergeOptions,
defineReactive
} from '../util/index'
export function initGlobalAPI (Vue: GlobalAPI) {
// config
const configDef = {}
configDef.get = () => config
if (process.env.NODE_ENV !== 'production') {
configDef.set = () => {
warn(
'Do not replace the Vue.config object, set individual fields instead.'
)
}
}
Object.defineProperty(Vue, 'config', configDef)
// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on
// them unless you are aware of the risk.
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
Vue.options = Object.create(null)
{14}
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
// this is used to identify the "base" constructor to extend all plain-object
// components with in Weex's multi-instance scenarios.
{15}
Vue.options._base = Vue
{16}
extend(Vue.options.components, builtInComponents)
{17}
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
{14}"ASSET_TYPES"是[‘component’,‘filter’,‘directive’]
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
这断代码的意思就是初始化Vue配置项,三个方法:
{15}保存vue;
{16}把Vue.options.components
和内置的builtInComponents
合并起来;
{17}我们比较关心的方法都在这下面;
我们在回到我们之前的import Vue from 'core/index
文件,我们发现构造函数在:
import Vue from './instance/index'
打开此文件:
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
//{18}
this._init(options)
}
initMixin(Vue) //{19}
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
{18}核心代码就一行,我们的构造涵数就执行了一个_init方法;
{19}在{18}中我们觉得这个_init方法特别的迷获,这个方法是那来的,显然是通过这种方式,给vue
的构造函数加了一个实例方法_init
,所以大家想看初始化方法得去这个里看。
我们点入这个initMixin方法:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) { //{20}
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// {21} expose real self
vm._self = vm
initLifecycle(vm) //{22}
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
{20}就一件事,就是给vue的构造函数加了_init方法
{21}核心代码就是以下部分,我们猜猜它们是干什么的,一眼看上去,会觉得initLifecycle(vm)
是生命周期函数,其实不是,而callHook(vm, 'beforeCreate')
才是真正生命周期函数,在beforeCreate
之前做了三件事:
1、initLifecycle
;
2、initEvents
初始化事件相关的事情;
3、initRender
初始化渲染相关的事情;
之后是更数据相关的事情:
initInjections
注入祖代传入的数据;initState
初始化组件里面的数据;initProvide
提供给后代需要传递的数据;
所以我们可以得出一个结论,我们想调数据只有在beforeCreate
之后进行
{22}我们进入initLifecycle
方法里看看,到底做了什么事情;
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent //{23}
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent //{24}
vm.$root = parent ? parent.$root : vm //{25}
vm.$children = [] //{26}
vm.$refs = {}
}
{23}我们看到了parent
字眼,我们知道谁赐予自己生命,是你父母,所以和生命周期相关的事情都是和老爹相关的事情,或者是和祖代相关的事情。所以初始化就是先找到自己的老爹,在把自己塞到自己老爹肚子里面。
{24}要和自己老爹建立一个联系,保存一下老爹;
{25}保存一下自己的老祖宗;
数据响应式原理
接下就是来研究vue的数据响应式,我们从{19}的initMixin(Vue)
开始,打开init.js
文件,我们从以下代码开始:
vm._self = vm
initLifecycle(vm)
initEvents(vm) //{26}
initRender(vm) //{27}
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) //{28}
initProvide(vm) // resolve provide after data/props
想callHook(vm, 'created')
{26}将来如果想研究vue的事件,此处是个非常好的入口点;
{27}接下来来研究一下render
,render
又做了什么事情呢,其实核心的事情只做了两件;
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
//render函数中的h就是它
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
render
声明了两个函数分别是_c
和$createElement
,将来我们执行的h
其实就是$createElement
,将来我们要研究vue的虚拟DOM,就研究它就可以了;
{28}initState
非常的重要,我们打开它对应的文件;
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm) //{29}
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
接下来我们在浏览器里运行examples/todomvc/index.html
,是浏览器的sources里找到state.js
文件,打开它,在53行打上断点,并运行之,如下图:
我们发现53行的data并不是我们熟悉的那个data,我们在写根主见的时候我们并没有写data,而data是在初始化的某一步生成的,生成data的正是mergedInstanceDataFn
函数,它执行的结果就会返回data所有的对象,接下来我们执行下一步,它就会执行{29}的initData(vm)
,我继续往下执行,就会进入state.js
里:
文件地址:src/core/instance/state.js
我们来看看这段代码的核心作用是什么?
function initData (vm: Component) {
let data = vm.$options.data //{30}
data = vm._data = typeof data === 'function' // {31}
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data) //{32}
const props = vm.$options.props //{33}
const methods = vm.$options.methods //{34}
let i = keys.length
while (i--) { //{35}
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data 将data做响应化处理
observe(data, true /* asRootData */) //{36}
}
{30}首先拿到vm
里的$options
选项,离得data
;
{31}对data
的类型进行判断,如果是函数调getData
函数进行处理,否则返回,总之就为了确保是我们想要的数据;
{32}拿到data
里所有的key
;
{33}拿到$options
选项里的props
;
{34}拿到$options
选项里的methods
;
{35}对data
里的属性进行去重校验;
{36}而整快代码的核心就是这个,调observe
函数,它所做的是将data
进行响应化处理;
我们打开observe
函数所在的文件:
文件地址:src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void //{37}
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { //{38}
ob = value.__ob__ //{39}
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value) // {40}
}
if (asRootData && ob) {
ob.vmCount++
}
return ob //{41}
}
{37}我们创建一个类,这个类是观察者Observer
;
{38}判断__ob__
是否存在
{39}存在把value
挂载的__ob__
给返回
{40}如果不存在就创建新的Observer
;
{41}observe
函数做的唯一件事情就是返回观察者实例;
接下来我们来研究一下Observe
地址:src/code/observer/index.js
export class Observer {
value: any; //{}
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep() //{42}
this.vmCount = 0
def(value, '__ob__', this) //{43}
if (Array.isArray(value)) { //{44}
if (hasProto) {
protoAugment(value, arrayMethods) //{61}
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value) //45}
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) { //{46}
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
{42}当一个值传进来的时候我们立刻创建一个new Dep()
的实例;
{43}给当前的值定义一个__ob__
;
{44}判断data里是纯对象还是数组,如果是数组有该怎么做,从这里我们可知,Observer
的核心就是判断data
里是数组还是对象,不同的类型,采用不同的响应化来处理。
{45}对象走的是walk
;
{46}先对对象的所有属性,对对象的属性进行遍历,对所有key
掉defineReactive
方法进行响应化处理;
以下为defineReactive
方法的代码:
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep() //{46}
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val) //{47}
Object.defineProperty(obj, key, { //{48}
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
//{49} 添加watcher实例
if (Dep.target) {
dep.depend() //{56}
//{50}有子对象额外处理
if (childOb) {
//{51} 把watcher也添加到子对象中
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value) //{52}
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) { //{53}
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal) //{54}
dep.notify() //{55}
}
})
}
{46}每个key
对应一个dep
;
{47}如果val
是对象就向下递归;
{48}如果Key
是普通的值,就定义get
,set
;
{49}添加watcher
;
{50}有子对象额外处理;
{51}把watcher
也添加到子对象中;
这有是为什么呢?
主要是因为对象的更改一定是会影响到子对象的;
{52}如果是数组,还必须执行一下数组响应化处理;
接下来我们看看set的处理:
{53}这行代码很奇怪,自己和自己想等也就算了,还有自己和自己都不相等的情况吗?其实NAN
就是自己和自己都不相等的;
{54}递归的把自己的孩子也设定一下;
{55}通知他们去做更新;
{56}接下来,来看一下dep
方法;
一下是dep
方法代码:
import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
//{57}
Dep.target.addDep(this)
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
{57}为你的理解是,我们会主动的把addDep
添加到subs
里,但是奇怪的是还让watcher
加我自己,这是什么意图?
为你先进入watcher
里addDep
来回答这个问题:
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) { //{58}
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn //{60}
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) { //{58}
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
{58}这个操作主要是做去重的操作,确保我们传进来的dep
我们没加过,如果没加过,我们就把它添加进去,我们一定不能重复的添加dep
,先把watcher
加到dep
做引用,然后又把自己加到dep
里面。这个操作做了两件事,就是把wather
和dep
之间进行相互关注,你加我,我加你。
我们特别的疑惑,为什么要这样做,为什么watcher
里要保存一个dep
?
watcher
是一个非常关键的中间人角色,js改变对象的时候,怎么就能去通知虚拟DOM
的diff算法?
我们再来看看构造函数watcher
到底还做了什么?
{59}vue里里面有一个组件就有一个watcher
,我们称这个wathcer
为渲染watcher
;
{60}我们得一些更新函数;
接下来我们看看数组的响应式,我们来看看上面的src/code/observer/index.js
文件里的{44}部分,当data
是数组的时候,调this.observeArray(value)
函数进行处理。
数组响应化的原理
我们想一下有一个数组在data
中声明,数组发生变化的时候,它什么时候会发生变化呢?比如:
new Vue {
el:'#app',
template:'<App></App>'
component:{},
data:{
arr:[{foo:'foo'},{foo:'foo'}]
},
methods:{
modify(){
this.arr[0].foo = 'bar'
}
}
}
我们假设将来data
里的arr
会发生“增”、“删”、“改”、“查”的操作,比如我们直接在arr
里的item
里的某个属性进行赋值,如调上面的modify
方法,界面中会响应化,会更新吗?
答案是:会更新
如果这是后有会问,我们平常在项目中为什么又会去用$set
,我们通过揭晓源码来揭晓答案。
我们知道能够改变数组结构操作的有7个方法,这个七个方法分别是:
push
,
pop
,
shift
,
unshift
,
splice
,
sort
',
reverse
为了能让我们操作数组的时候,vue能直接做响应式,vue做了一个事情,vue对这七个方法做了一个拦截器,对这七个方法进行了拦截,接管了它们具体的操作,除了做这7个方法的操作之外,还要做个通知方法,让它去做更新。
我们来看看它们是怎么进行拦截的?
我们进入src/code/observer/index.js
文件里的{61}的 protoAugment(value, arrayMethods)
里:
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src //{62}
}
{62}这段代码的意思是要把数组的原型设为src
这时我们回去看protoAugment(value, arrayMethods)
,value
是数组,arrayMethods
就是我们要覆盖的原型,就是要进行数据拦截的方法,怎么拦截呢,直接把原来的原型给替换了,我们接着来看arrayMethods
方法。
arrayMethods
定义在src/core/observer/array.js
文件里,代码如下:
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from '../util/index'
const arrayProto = Array.prototype //{63}
export const arrayMethods = Object.create(arrayProto) //{64}
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) { //{65}
// cache original method
//获取原始操作方法push
const original = arrayProto[method] //{66}
//定义拦截
def(arrayMethods, method, function mutator (...args) { //{67}
const result = original.apply(this, args) //{68}
//{69}
const ob = this.__ob__ //{70}
let inserted //{71}
switch (method) { //{72}
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted) //{73}
// notify change
ob.dep.notify() //{71}
return result
})
})
通过代码我们看看,vue是如何对数组方法进行拦截的?
{63}首先我们先将Array
的原型给保存起来;
{64}接下来创建一个全新的对象arrayMethods
,将来它将做为我们数组的指定原型,我们在执行数组的七个方法,将不再是原来的七个方法,而是现在的七个方法。
{65}接下来我们对这七个方法进行遍历;
{66}获取原始的操作方法,我们以push
为例;
{67}定义拦截def
,我们给arrayMethods
对象定义method
方法,这个方法是什么呢,就是def
里具体的mutator
函数的描述器。
如下,将来的value就是我们定义的mutator
Object.defineProperty(arrayMethods,method,{value:mutator})
{68}之前的行为;
{69}定义数组方法的全新行为;
{70}拿到之前定义的观察者;
{71}拿到观察者之后,拿到它的dep
做更新,这时界面就更新了;
现在我们得道答案了,如果我们对data里的数组做push
操作,对arr
做push
操作,页面立刻就更新了,如:
new Vue {
el:'#app',
template:'<App></App>'
component:{},
data:{
arr:[{foo:'foo'},{foo:'foo'}]
},
methods:{
modify(){
this.arr.push({foo:'foo'}) //{74}
this.arr[0].bar = 'bar'; //{75}
delete this.arr[0].foo //{76}
this.$delete(...) //{77}
}
}
}
这是为什么呢?
原因就是{70}是观察者;
{71}尝试去做插入工作;
{72}插入的方式有三,push
,unshift
,splice
,不管新进来的元素,做了这三个方法的什么操作,新进来的元素必须做响应式,但它是新进来的,没做响应式,因为它是新来的,还没经历过共产主义的洗礼,必须进行在教育,但data
一开始就有的数组,早都进行教育了,早都进行过教育了;
{73}对新来的元素进行额外的教育;
{74}可以对数组进行变更;
{75}而这部分就不能进行变更;
{76}同样是这个方法也不行,因为只有经过处理的七方法才能进行拦截,而delete
没有经过处理;
{77}而这种方法是可以的;
//mutator
就是定义数组方法的全新行为。