整个vue架构看下来,主要分为三个部分:响应式原理,编译器的实现 以及 patch算法。
以下是对响应式原理的个人理解,看下来,大致是利用 发布-订阅模式 + 异步更新。
当我们初始化一个vue实例并把它绑定到相应的DOM节点时,其实已经完成了属性响应式的设定。
所以我们从VUE的构造函数入手。
打开 core/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'
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
发现构造函数的根,其实在core/instance/index.js
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')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
我们不细化到每一个函数,一句话,core/instance/index.js 丰满 Vue.prototype ;而 initGlobalAPI(Vue) 丰满Vue构造函数,即全局API。
以下是丰满后的Vue.prototype和Vue构造函数:
// initMixin(Vue) src/core/instance/init.js **************************************************
Vue.prototype._init = function (options?: Object) {}
// stateMixin(Vue) src/core/instance/state.js **************************************************
Vue.prototype.$data
Vue.prototype.$props
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {}
// eventsMixin(Vue) src/core/instance/events.js **************************************************
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
// lifecycleMixin(Vue) src/core/instance/lifecycle.js **************************************************
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
// renderMixin(Vue) src/core/instance/render.js **************************************************
// installRenderHelpers 函数中
Vue.prototype._o = markOnce
Vue.prototype._n = toNumber
Vue.prototype._s = toString
Vue.prototype._l = renderList
Vue.prototype._t = renderSlot
Vue.prototype._q = looseEqual
Vue.prototype._i = looseIndexOf
Vue.prototype._m = renderStatic
Vue.prototype._f = resolveFilter
Vue.prototype._k = checkKeyCodes
Vue.prototype._b = bindObjectProps
Vue.prototype._v = createTextVNode
Vue.prototype._e = createEmptyVNode
Vue.prototype._u = resolveScopedSlots
Vue.prototype._g = bindObjectListeners
Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}
// core/index.js 文件中
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// 在 runtime/index.js 文件中
Vue.prototype.__patch__ = inBrowser ? patch : noop
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
// 在入口文件 entry-runtime-with-compiler.js 中重写了 Vue.prototype.$mount 方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// ... 函数体
}
// initGlobalAPI
Vue.config
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
Vue.options = {
components: {
KeepAlive
// Transition 和 TransitionGroup 组件在 runtime/index.js 文件中被添加
// Transition,
// TransitionGroup
},
directives: Object.create(null),
// 在 runtime/index.js 文件中,为 directives 添加了两个平台化的指令 model 和 show
// directives:{
// model,
// show
// },
filters: Object.create(null),
_base: Vue
}
// initUse ***************** global-api/use.js
Vue.use = function (plugin: Function | Object) {}
// initMixin ***************** global-api/mixin.js
Vue.mixin = function (mixin: Object) {}
// initExtend ***************** global-api/extend.js
Vue.cid = 0
Vue.extend = function (extendOptions: Object): Function {}
// initAssetRegisters ***************** global-api/assets.js
Vue.component =
Vue.directive =
Vue.filter = function (
id: string,
definition: Function | Object
): Function | Object | void {}
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
// entry-runtime-with-compiler.js
Vue.compile = compileToFunctions
以一个例子为引
<div id="app">{{test}}</div>
var vm = new Vue({
el: '#app',
data: {
test: 1
}
})
我们知道,实例化一个vue实例的关键,在this._init(options)上,所以让我们走进这个函数。
core/instance/init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._uid = uid++// vue实例ID
// 开发环境下打开性能追踪-init,compile,render,patch
let startTag, endTag
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// 以下就是被追踪性能的代码
vm._isVue = true// 标志该组件是一个vue实例
// merge options
if (options && options._isComponent) {// 不存在_isComponent属性,走else分支
initInternalComponent(vm, options)
} else {
// 初始化并丰满$options属性
vm.$options = mergeOptions(// 1.规范化属性名 2.合并对象产生新对象
resolveConstructorOptions(vm.constructor),// 解析构造函数options
options || {},
vm
)
}
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)// 对vue实例的渲染做一个代理过滤
} else {
vm._renderProxy = vm
}
vm._self = vm
// 预处理-绑定未来混入生命周期等事件所需得标志位(属性)
initLifecycle(vm)
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')
/* 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)
}
}
这个函数一共做了几件事:
1.给实例添加ID和标识为VUE实例的标志位。
2.初始化并丰满实例的$options属性。
vm.$options = mergeOptions(// 1.规范化属性名 2.合并对象产生新对象
resolveConstructorOptions(vm.constructor),// 解析构造函数options
options || {},
vm
)
相当于
vm.$options = mergeOptions(
// resolveConstructorOptions(vm.constructor)
{
components: {
KeepAlive
Transition,
TransitionGroup
},
directives:{
model,
show
},
filters: Object.create(null),
_base: Vue
},
// options || {}
{
el: '#app',
data: {
test: 1
}
},
vm
)
所以我们得去了解mergeOptions函数的实现(core/util/options.js)
主要做了两件事:
(1)规范化属性。
(2)利用相应的合并策略函数合并属性。
export function mergeOptions (
parent: Object,// 构造函数的options
child: Object,// 初始化vue实例时传入options
vm?: Component// vue实例
): Object {
if (process.env.NODE_ENV !== 'production') {
checkComponents(child)// 检验options.components组件名称合法性
}
// 处理VUE.extend的情况,合并子类构造函数的options
if (typeof child === 'function') {
child = child.options
}
// 规范化属性,因为开发者有多种定义方式,需要统一
normalizeProps(child, vm)// 统一成对象的形式
normalizeInject(child, vm)// 规范化为对象语法 inject和provide配合使用
normalizeDirectives(child)// directive-注册局部指令
const extendsFrom = child.extends
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, 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)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat// 调用属性相对应的策略函数,不存在则调用默认策略
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
需要了解引用的strats策略对象(const strats = config.optionMergeStrategies,来自于 core/config.js ),引入在当前文件core/util/options.js中,该strats策略对象是空对象,需要在该文件中慢慢丰满自己。
我们只分析两个策略函数,其他便不再赘述了。
(1) 默认合并策略函数
const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}
其实很简单,同一个属性,只要子选项不是 undefined 那么就是用子选项,否则使用父选项。
(2) data属性的合并策略函数
// 定义data属性的策略函数-最终把data属性定义成一个函数,执行该函数才能得到真正的数据对象
strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
if (!vm) { // 当前处理的是子组件-说明当前处理的是VUE.extend的情况
if (childVal && typeof childVal !== 'function') {// 子组件的data属性必须存在且为函数
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
}
发现mergeDataOrFn的返回值便是data属性策略函数,所以进入该函数。
export function mergeDataOrFn (
parentVal: any,// Vue.options 的data对象
childVal: any,// 参数 options 的data对象
vm?: Component
): ?Function {
if (!vm) {// 处理VUE.extend的情况
if (!childVal) {// 子类不存在data对象,直接返回父类的data对象
return parentVal
}
if (!parentVal) {// 父类不存在data对象,直接返回子类的data对象
return childVal
}
return function mergedDataFn () {
return mergeData(// 返回真正的数据对象,去除重复属性
typeof childVal === 'function' ? childVal.call(this, this) : childVal,// 调用子类的data函数
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal// 调用父类的data函数
)
}
} else {// 初始化实例走该分支
return function mergedInstanceDataFn () {// 返回一个函数,执行后就是真正的data数据对象
const instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal
const defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal
if (instanceData) {
return mergeData(instanceData, defaultData)// 返回真正的数据对象,去除重复属性
} else {
return defaultData
}
}
}
}
到此我们发现,data属性的策略函数,执行后会将data属性定义成一个函数,只有执行该函数才能得到真正的数据对象。
已下是vm.$options的截图
3.对vue实例的渲染做一个代理过滤。
4.预处理+调用钩子函数。
在这一步,包含着的initState(vm)函数,是实现响应式的根。
进入core/instance/state.js
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)
}
}
我们以initData(vm)为切入点,看看vue是如何对data属性实现响应式的。
function initData (vm: Component) {
let data = vm.$options.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
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
// props优先级 > data优先级 > methods优先级
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {// 避免method和data具有同名属性
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {// 避免props和data具有同名属性
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)) {// 键名不为保留字
// 在vue实例对象上添加代理访问数据对象的同名属性
proxy(vm, `_data`, key)// vm._data.x => vm.x
}
}
// observe data
observe(data, true /* asRootData */)
}
这个函数主要做了以下几件事:
(1)获取真正的data数据对象,因为vm.$options.data是一个函数。
(2)避免data中的属性与props和methods同名。
(3)对vue实例的属性添加一层基本代理,使vm.key指向vm._data.key。
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
(4)监测data对象,使之成为响应式。
进入observe函数(core/observer/index.js)
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 避免重复观测一个数据对象
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__// 被观测过的对象都会带有__ob__属性
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue// 避免vue实例被监测
) {
ob = new Observer(value)// 为data数据对象建立一个监测对象
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
这个函数的核心,是new Observer(value),所以进入Observer的构造函数。
export class Observer {
value: any;// data数据对象
dep: Dep;// 属于该数据对象的依赖(watcher)收集筐
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)//给数据对象定义一个__ob__属性,指向当前的observer实例,且该属性不可枚举
if (Array.isArray(value)) {// 处理数组对象
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {// 处理纯对象
this.walk(value)
}
}
根据数据对象data的类型,分为两个分支(处理纯对象和处理数组)。
结合当前的例子,我们先只看处理纯对象的情况:
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
对data数据对象中的每一个属性,调用defineReactive()方法,将数据对象的数据属性转换为访问器属性,该方法是整个响应式的核心。
// 让属性成为响应式
export function defineReactive (
obj: Object,// data
key: string,// 属性名
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 一个key 对应一个 dep
//每一个数据字段都通过闭包引用着属于自己的 dep 常量
const dep = new Dep()
// 获取属性描述对象-之前设置的第一层基本代理
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {// 判断属性是否是可配置的
return
}
// 缓存原来设置的get set函数
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {// 如果val本身拥有get函数但没有set,就不会执行深度监测
val = obj[key]
}
let childOb = !shallow && observe(val)// 默认深度观察-递归
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val// 调用缓存的基本get函数 vm.x=>vm._data.x
if (Dep.target) {// 要被收集的依赖-观察者watcher
dep.depend()// 将依赖收集到闭包dep的筐中
if (childOb) {//val.__ob__
childOb.dep.depend()//在使用 $set 或 Vue.set 给数据对象添加新属性时触发
if (Array.isArray(value)) {
dependArray(value)
}
}
}
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)) {//NaN
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)// vm._data.x => vm.x
} else {
val = newVal
}
childOb = !shallow && observe(newVal)//监测新值
dep.notify()// 触发dep筐中依赖
}
})
}
主要做了两件事
(1)声明一个属于属性自己的dep收集依赖筐(闭包)。
(2)改进属性的get set代理。
get代理:返回值+收集依赖
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val// 调用缓存的基本get函数 vm.x=>vm._data.x
if (Dep.target) {// 要被收集的依赖-观察者watcher
dep.depend()// 将依赖收集到闭包dep的筐中
if (childOb) {// 指向val.__ob__,处理深度监测的问题
childOb.dep.depend()// 在使用 $set 或 Vue.set 给数据对象添加新属性时触发
if (Array.isArray(value)) {// 处理属性为数组的情况
dependArray(value)
}
}
}
return value
}
当获取test属性时,先判断当前存不存在要被收集的依赖(watcher对象),如果有,调用自己对应dep的depend()方法来收集依赖。(下面两个if分支在此例中不涉及)
进入core/obsever/dep.js
export default class Dep {
static target: ?Watcher;// 静态对象,全局只有一个
id: number;// dep唯一标识
subs: Array<Watcher>;// 监测此dep实例的观察者们
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {// 收集依赖(watcher)
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {// 触发依赖
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()// 调用每一个观察者的update方法
}
}
}
dep.depend()就是把当前的存储在全局的Dep.target对象(watcher)添加到dep的观察者数组中。
再看看watcher对象的addDep方法,进入core/obsever/watcher.js
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {// 避免重复收集dep
this.newDepIds.add(id)
this.newDeps.push(dep)// 将当前dep对象添加到watcher实例自己的newDeps数组中
if (!this.depIds.has(id)) {
dep.addSub(this)// 将当前watcher实例添加到dep实例的观察者数组中
}
}
}
暂时不用去考虑if分支的作用,无非是做一些性能的优化,该方法最终的效果,就是让当前的dep实例和当前的watcher实例都彼此包含。换句话说,dep实例在收集依赖(观察者)的同时,依赖也保存了dep实例。
set代理:设置新值+触发依赖
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val// 获取原有值
/* eslint-disable no-self-compare */
// 只有当原有值与新值不等时才触发set代理
if (newVal === value || (newVal !== newVal && value !== value)) {// NaN
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)// 调用缓存的基本set函数 vm.x => vm._data.x
} else {
val = newVal
}
childOb = !shallow && observe(newVal)//监测新值
dep.notify()// 触发dep筐中依赖
}
但修改test属性时,会先判断是否等于旧值,若不等,则设置新值且调用自己对应dep的notify()方法来触发依赖。
进入core/obsever/dep.js
notify () {// 触发依赖
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()// 调用每一个观察者的update方法
}
}
发现无非是调用该dep实例存储的每一个依赖(watcher)的update方法。
update () {
/* istanbul ignore else */
if (this.computed) {// 处理计算属性
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {// 同步更新
this.run()
} else {
queueWatcher(this)// 将当前观察者对象放到一个异步更新队列
}
}
在此例中,会将当前依赖(watcher)放入一个异步更新队列中。但这块并不是我们响应式流程的重点,无非是对触发依赖的性能优化,通过上一个if分支我们知道,最终所有的依赖(watcher)都会执行自己的run方法。
run () {
if (this.active) {// 当前依赖(watcher)为激活状态
this.getAndInvoke(this.cb)
}
}
进入getAndInvoke函数,这也是我们依赖触发的尽头。
getAndInvoke (cb: Function) {
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// 深度检测标志位,默认true
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)// 调用watcher构造函数中设置的回调
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}
进入watcher.get()方法
get () {
pushTarget(this)// 将此watcher实例设置为Dep.Target
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)// 获取组件中观察的属性值-获取时同时会触发属性的get
} 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()// 还原Dep.Target
this.cleanupDeps()// 清空newDeps
}
return value
}
发现无非是把当前watcher设置为Dep.Target,再重新收集一次依赖。
所以getAndInvoke函数,无非做了两件事:重新收集依赖(其实还包含了视图更新)以及触发相应回调函数。
现在,我们了解了响应式原理的实现机制,但我们发现,在initData()阶段,只是对每个属性配置了相应的代理,那么在哪调用get,set? 初始的Dep.Target又是谁? 响应式修改数据后又是怎样自动更新视图的?
答案在_init()方法的最后一句代码:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
接下来,我们将解析vue.prototype.$mount()方法,即整个响应式的起点。
进入platforms/web/runtime/index.js
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined// 获取真实DOM节点
return mountComponent(this, el, hydrating)// 真正的挂载工作
}
这是vue.prototype.$mount()方法的第一层封装,我们还得到src/platforms/web/entry-runtime-with-compiler.js,在这一层封装,我们加入了编译模板的功能。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
/* istanbul ignore if */
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
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
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) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(template, {
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')
}
}
}
return mount.call(this, el, hydrating)
}
看下来,在这层封装中,无非是对没有render函数的vue实例,通过template编译出render函数。
最终还是要调用 mountComponent(this, el, hydrating)。即这个函数才是真正的挂载函数,之前的操作无非是为了给它提供渲染函数。
进入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)
}
}
// 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) {
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
}
在该函数中,总共做了几件事:
1.判断渲染函数是否存在,不存在定义为一个空VNode节点。
2.定义并初始化updateComponent函数。
3.为updateComponent函数声明一个观察者对象。
我们先看updateComponent函数,发现无论是执行 if 语句块还是执行 else 语句块,最终 updateComponent 函数的功能是不变的。都是以 vm._render() 函数的返回值作为第一个参数调用 vm._update() 函数,即把渲染函数生成的虚拟DOM渲染成真正的DOM。
不深究那两个子函数,可以简单地认为:
vm._render 函数的作用是调用 vm.$options.render 函数并返回生成的虚拟节点(vnode)。
vm._update 函数的作用是把 vm._render 函数生成的虚拟节点渲染成真正的 DOM。
接着,我们看创建观察者的部分,这也是真正触发响应式的关键。
创建 Watcher 观察者实例将对 updateComponent 函数求值,我们知道 updateComponent 函数的执行会间接触发渲染函数(vm.$options.render)的执行,而渲染函数的执行则会触发数据属性的 get 拦截器函数,从而将依赖(观察者)收集,当数据变化时将重新执行 updateComponent 函数,这就完成了重新渲染。同时我们把上面代码中实例化的观察者对象称为渲染函数的观察者。
最后,我们进入core/observer/watcher.js做最后的探索。
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
computed: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
dep: Dep;
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.computed = !!options.computed
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.computed = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.computed // for computed 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 = function () {}
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
)
}
}
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
当前的expOfFn指向updateComponent函数,此时被赋值到this.getter。关注到最后一行的this.value=this.get();
get () {
pushTarget(this)// 将此watcher实例设置为Dep.Target
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)// 获取组件中观察的属性值-获取时同时会触发属性的get
} 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()// 还原Dep.Target
this.cleanupDeps()// 清空newDeps
}
return value
}
发现当前的渲染函数观察者被设置到Dep.Target上了,且触发this.getter就是执行updateComponent函数,执行时会触发相应属性的get代理。
现在一切都捋顺了,我们来梳理一下流程。
我们new一个vue实例的同时,会触发_init函数,这个函数做了几件事:
(1)初始化并丰满vm.$options属性。
(2)调用相应的初始化函数,让数据成为响应式。其中关键的initState(vm),会让设置的data,methods,props对象,成为响应式。
例如其中的initData(vm),会调用observe(data),即为data数据对象建立一个监测对象observer,具体的就是为纯对象调用walk(data)(数组对象递归调用observe()),该函数内部就是为data对象中的每一个属性调用defineReactive(obj,key)函数,使该属性成为响应式。而属性成为响应式的关键,就是
1)为每个属性设置对应的dep,由它来做发布信息的工作。
2)设置get代理来收集依赖。
3)设置set代理来触发依赖。
(3)将vue实例挂载到dom节点上。我们在这一步为渲染函数设置了一个观察者watcher,这也是我们整个响应式的起点。我们在挂载的时候,会调用渲染函数,从而触发相关数据属性的 get 拦截器函数,从而将当前依赖(渲染函数观察者)收集,此时,每一个属性都收集着当前渲染函数观察者。未来,当数据变化时将触发依赖的update()函数->run()->getAndInvoke(),其中的this.get(),会重新执行 updateComponent 函数,这就完成了重新渲染。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。