内容乃本人学习Vue2源码的一点笔记,若有错误还望指正。

源码版本:

vue: 2.6

vue-loader: 13.x

vue-template-compiler: 2.6

相关学习笔记:

众所周知,Vue是以数据驱动视图展示的,即Vue会监听数据的变化,从而自动重新渲染页面的结构。

Vue主要通过三步走来实现这个功能:

第一步是对数据进行响应式改造,即对数据的读写操作进行劫持;

第二步是对模板依赖的数据进行收集;

第三步是在数据发生变化时,触发组件更新。

数据响应式改造

0. defineReactive

对数据进行响应式改造的核心代码

// core/observer/index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep() 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)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) { // 当前`Dep.target`不为空时,通常指向一个`watcher`实例
dep.depend() // 属性被收集到当前`watcher`实例的依赖数组中
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 */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}
})
}

通过Object.defineProperty修改对象属性的属性描述符descriptor,来实现劫持对象属性的读写操作。

前置知识,对象的属性分为data型和accessor型。

data型的属性描述符包含value和writable;accessor型的属性描述符包含getter和setter函数(两者至少存在一个)。

由上述代码可以看出,所有属性被处理成了accessor型属性,即通过getter和setter来完成读写,比如当我们读取person对象上的属性name,实际得到的是name的属性访问符中的getter函数执行后的返回值。

上述的响应式改造中,每个属性会对应一个dep实例:const dep = new Dep(),假如属性值val是对象或数组,会被列入观察对象,他的属性会被递归进行响应式改造let childOb = !shallow && observe(val)

get函数被用于收集依赖

get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},

该函数在对象属性被访问时会执行,如果Dep.target不为空,即当下有一个监听器watcher在收集依赖,就进行依赖的收集,dep实例会被收集到该watcher的依赖数组newDeps中,同时dep也会将此watcher记录到自己的subs订阅数组中,记录有谁订阅了自己的变更。

如果childOb不为空(即属性值val为数组或对象,且可扩展),就对val的__ob__属性也进行收集操作。

如果value是数组,对数组中的对象元素也进行依赖收集。

就是一层层的递归收集。

set函数被用于通知变更:

set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify()
}

如果属性的新值是属性或对象,就更新childOb

完成属性赋值操作后,调用dep.notify(),通知所有订阅了自己的watcher实例执行update操作,即下面代码中的for循环操作。

// core/observer/dep.js
export default class Dep {
// ... 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()
}
}
}

如果是同步this.sync的watcher会立即被执行,否则会插入到watcher队列queueWatcher(this)排队等待执行:

// core/observer/watcher.js
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

1. initInjections

// core/instance/inject.js
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}

通过执行resolveInject解析inject中的数据,解析结果赋值给result。result包含inject中所有的key,如果上级组件中没有对应inject数据的provide,就赋默认值,简单来说大致就是result[key] = inject[key] || default

再调用defineReactive(vm, key, result[key])将这些key加到vm实例上,即inject中的数据也会进行响应式处理。

假设存在一个inject:["person"],如果person的值是个对象,它是一个被观察对象,当前子组件的watcher会对该对象的__ob__属性依赖收集,在上级组件中更改了原始person的某个属性,就会触发子组件的更新。

2. initProps

// core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {
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.
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)
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
)
}
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 {
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)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}

propsOptions,接收的是vm.$options.props,是声明接收的props的配置,;vm.$options.propsData是实际接收到的props数据。

调用defineReactive(props, key, result[key])将propsOptions上的key加到props对象上,即vm._props上,进行响应式处理,如果是在vm上不存在的key,通过proxy(vm, '_props', key)操作,使得可以通过vm直接访问到_props的属性,而不需要通过_props对象来访问。

3. initData

// core/instance/state.js
function initData (vm: Component) {
let data = vm.$options.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]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}

data选项会被挂在vm._data上,从上述代码中可以看出,data必须是一个对象,或者返回值为对象的函数。

通过proxy(vm, '_data', key)操作,vm可以直接访问到_data的属性,而不需要通过_data对象来访问。

最后通过observe(data, true /* asRootData */)来对数据做响应式改造,可以看到这个observe方法多传了一个参数值为true,标记当前处理的数据是$options.data对象。

observe方法实际是创建一个新的ob实例,数据的__ob__属性相信在控制台打印过vue中数据的同学都不陌生,都是指向ob实例。

// core/observe/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__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
// core/observe/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()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
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])
}
}
}

从上面Observer的构造函数中可以看出,创建ob实例后,这个实例就挂载数据的__ob__属性上了,因为在iniDats时传递给构造函数的参数是个对象,所以会调用walk方法,继续看walk方法的定义,可以看出,是把这个对象的属性逐个取出,调用defineReactive(obj, keys[i])进行响应式改造。

4. initComputed

// core/instance/state.js
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering() for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
} if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
} // component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}

获得每个计算属性对应watcher的初始值

从上述代码中可以看出,会遍历computed所有的属性,每个属性对应配置一个watcher实例,watcher实例在创建时,会调用每个computed对应的getter获取一遍初始值,放在watcher实例的value属性上

// core/observer/watcher.js
export default class Watcher {
// ... constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// ...
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
} // ...
}
// core/observer/dep.js
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}

可以看到,执行watcher实例的get()方法时,会进行一个pushTarget(this)的操作,此操作修改了Dep.target,使它指向了当前的watcher实例,如果某个computed属性依赖了data中的某个属性,需要读取data中的某个属性值,就会触发该data属性的getter函数,使得该data属性被收集到当前watcher实例的依赖数组中。

完成computed属性的取值后,执行popTarget(),即下面的代码:

// core/observer/dep.js
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}

会使Dep.target指回上一个watcher实例。

最后清理依赖this.cleanupDeps(),将不再关联的依赖dep其订阅数组中对应的watcher移除,将newDeps赋值给deps并清空newDeps,代表该watcher实例一次依赖收集完毕。

计算属性被读取时,其对应watcher依赖的数据会被当前watcher收集为自身的依赖

如果computed某个属性的标识符不在vm实例上,就继续执行defineComputed(vm, key, userDef),会将给vm实例添加一个名为key的属性,该属性的getter函数由下述代码定义:

// core/instance/state.js
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}

即在这个计算属性被读取时,会拿到它所对应的watcher实例,如果当前Dep.target不为null时,watcher会执行实例方法depend()

export default class Watcher {
// ... /**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
} // ...
}

可以看到此watcher实例的依赖deps会被一一取出,执行dep实例的depend方法:

// core/observerdep.js
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}

即此watcher实例的依赖都会被收集到当前Dep.target指向的watcher实例的依赖数组中。

5. initWatch

// core/instance/state.js
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
} function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
} Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}

initWatch的内容比较简单,就是通过调用createWatcher(vm, key, handler),一一对应生成watcher实例,并且给watcher实例标记options.user = true,代表这个watcher是用户配置的。

每个watch通常是有一个对应的表达式(通常是vm的data数据)和一个对应的回调函数,使用场景通常是当vm中的某些数据发生改变时,用户需要做一些自定义的操作来做处理。

与computed中生成对应watcher实例类似,watcher实例在创建时,每个watch对应的表达式就会被求值一遍,即vm实例上的某些数据属性被读取,这些属性对应的dep会被收集到该watcher实例的依赖数组中,求得的值会放在watcher实例的value属性上,如果某个watch配置了immediate,就立即执行一遍watch对应的回调函数,入参为watchervalue属性值。

可以看到在执行回调函数前,执行了一个pushTarget(),此时Dep.target会指向空,所以在回调函数执行过程中,如果vm的某些数据属性被访问,这些属性不会被收集依赖,因为属性的getter函数中在属性被收集依赖前有个对Dep.target的判空检查。

6. 一些说明

__ob__是给对象加的属性,指向observer实例,ob和对象是一对一,代表这个对象被观察,该对象的属性的读写操作会被做响应式处理,即被劫持。

dep是给属性配置的用于依赖收集的,通常对象的某个属性与dep是一对一,可以被多个watcher收集,即多个watcher实例在监听这个属性的变化;__ob__是对象的特殊属性,它也有自己的dep,可以被watcher收集。

一个watcher实例只会关联一个vm,一个vm实例可以关联多个watcher,watcher实例会放在vm._watchers数组中;渲染watcher还会放在vm._watcher上,渲染watcher从字面上理解就是与组件渲染有关的watcher

Vue 2.x源码学习:数据响应式改造的更多相关文章

  1. 【Vue源码学习】响应式原理探秘

    最近准备开启Vue的源码学习,并且每一个Vue的重要知识点都会记录下来.我们知道Vue的核心理念是数据驱动视图,所有操作都只需要在数据层做处理,不必关心视图层的操作.这里先来学习Vue的响应式原理,V ...

  2. Vue provide/inject 部分源码分析 实现响应式数据更新

    provide/inject 数据响应式更新的坑及源码解析 下面是我自己曾经遇到 一个问题,直接以自己QA的形式来写吧 自问自答了,需要的同学也可以直接访问segmentfault地址 官网给出实例, ...

  3. vue 2.0源码学习笔记—new Vue ( Vue 初始化过程 )

    new Vue(Vue 初始化) 一个vue实例化到底经历了什么?已下是博主自己的总结,不正确的地方请指出,谢谢~ 一.简述 从使用角度来看,挂载的顺序如下 1. $slots 2. $scopedS ...

  4. faster rcnn 源码学习-------数据读入及RoIDataLayer相关模块解读

    参考博客:::https://www.cnblogs.com/Dzhen/p/6845852.html 非常全面的解读参考:::https://blog.csdn.net/DaVinciL/artic ...

  5. 读Vue源码二 (响应式对象)

    vue在init的时候会执行observer方法,如果value是对象就直接返回,如果对象上没有定义过_ob_这个属性,就 new Observer实例 export function observe ...

  6. vue虚拟DOM源码学习-vnode的挂载和更新流程

    代码如下: <div id="app"> {{someVar}} </div> <script type="text/javascript& ...

  7. vue源码解析之响应式原理

    关于defineReactive等使用细节需要自行了解 一些关键知识点 $mount时 会 new Watcher 把组件的 updateComponent 方法传给watcher 作为getter ...

  8. 【Vue源码学习】依赖收集

    前面我们学习了vue的响应式原理,我们知道了vue2底层是通过Object.defineProperty来实现数据响应式的,但是单有这个还不够,我们在data中定义的数据可能没有用于模版渲染,修改这些 ...

  9. Vue.js 源码学习笔记

    最近饶有兴致的又把最新版 Vue.js 的源码学习了一下,觉得真心不错,个人觉得 Vue.js 的代码非常之优雅而且精辟,作者本身可能无 (bu) 意 (xie) 提及这些.那么,就让我来吧:) 程序 ...

  10. 源码学习VUE之Observe

    在文章 源码学习VUE之响应式原理我们大概描述了响应式的实现流程,主要写了observe,dep和wather的简易实现,以及推导思路.但相应代码逻辑并不完善,今天我们再来填之前的一些坑. Obser ...

随机推荐

  1. PlayWright(二十二)- allure插件(一)

    在上文中,我们介绍并使用了pytest-html插件,总之并不复杂,但是今天我们要讲一个比pytest-html插件强很多的插件allure报告,我们要掌握他并且灵活使用,之后的框架就不需要考虑其他的 ...

  2. .net core 因路径原因导致的JSON解析错误

    因解析json配置文件导致的错误: JsonReaderException: '0xEF' is an invalid escapable character within a JSON string ...

  3. 使用gulp.js打包layuiAdmin

    安装nvm 在nvm目录下,找到settings.txt,追加以下两行加速nvm(淘宝镜像)node_mirror: https://npm.taobao.org/mirrors/node/npm_m ...

  4. 从一道面试题来谈谈Golang中的 ==

    写这篇文章的时候,已经离我找工作有一段时间了,但是觉得这道题不管是面试还是日常的工作中,都会经常遇到,所以还是特意写一篇文章,记录下自己对Golang中==的理解.如文章中出现不对的地方,请不吝赐教, ...

  5. 基于Prometheus搭建监控平台

    目录 前言 配置server单节点 prometheus.service 配置node节点 配置mysql监控 在数据库中添加exporter账户 修改mysql_exporter的配置 添加serv ...

  6. gitlab与LDAP 联调

    gitlab整理 目录 gitlab整理 1.安装Gitlab依赖包 2.下载,安装 3.配置,访问域名及邮箱 4.初始化,启动 5.访问,以及邮箱测试 5.1汉化 6.问题总结处理 6.1安装时出现 ...

  7. pentaho(keetle)使用手册

    pentaho使用 先展示一下用途和效果 1. 环境准备 1.1 pentaho是什么? pentaho可读作"彭塔湖",原名keetle 在keetle被pentaho公司收购后 ...

  8. EXE一机一码打包加密大师1.4.0更新-支持导出注册机

    EXE一机一码打包加密大师可以对EXE文件进行加密处理,可以让EXE支持一机一码功能,也可以支持静态打开密码功能, 方便开发人员想用户收费. 详细软件使用说明可以查看下面的说明文档: EXE一机一码打 ...

  9. 再聊Java Stream的一些实战技能与注意点

    大家好,又见面了. 在此前我的文章中,曾分2篇详细探讨了下JAVA中Stream流的相关操作,2篇文章收获了累计 10w+阅读.2k+点赞以及 5k+收藏的记录.能够得到众多小伙伴的认可,是技术分享过 ...

  10. 织梦DEDEBIZ调用全站文章数量

    织梦DEDEBIZ如何调用全站文章数量{dede:sql sql="select count(*) as c from biz_archives} 共有文章:[field:c/] 篇 {/d ...