Vue 源码解析:深入响应式原理

前端迷 | 凡事预则立,不预则废 2019/12/05 10:05

本文来自《Vue.js 权威指南》源码篇的一个章节,现在分享出来给大家

Vue.js 最显著的功能就是响应式系统,它是一个典型的 MVVM 框架,模型(Model)只是普通的 JavaScript 对象,修改它则视图(View)会自动更新。这种设计让状态管理变得非常简单而直观,不过理解它的原理也很重要,可以避免一些常见问题。下面让我们深挖 Vue.js 响应式系统的细节,来看一看 Vue.js 是如何把模型和视图建立起关联关系的。

如何追踪变化

我们先来看一个简单的例子。代码示例如下:

<div id="main">

<h1>count: {{times}}</h1>

</div>

<script src="vue.js"></script>

<script>

var vm = new Vue({

el: '#main',

data: function () {

return {

times: 1

};

},

created: function () {

var me = this;

setInterval(function () {

me.times++;

}, 1000);

}

});

</script>

运行后,我们可以从页面中看到,count 后面的 times 每隔 1s 递增 1,视图一直在更新。在代码中仅仅是通过 setInterval 方法每隔 1s 来修改 vm.times 的值,并没有任何 DOM 操作。那么 Vue.js 是如何实现这个过程的呢?我们可以通过一张图来看一下,如下图所示:

新知图谱, Vue 源码解析:深入响应式原理

图中的模型(Model)就是 data 方法返回的{times:1},视图(View)是最终在浏览器中显示的DOM。模型通过Observer、Dep、Watcher、Directive等一系列对象的关联,最终和视图建立起关系。归纳起来,Vue.js在这里主要做了三件事:

  • 通过 Observer 对 data 做监听,并且提供了订阅某个数据项变化的能力。

  • 把 template 编译成一段 document fragment,然后解析其中的 Directive,得到每一个 Directive 所依赖的数据项和update方法。

  • 通过Watcher把上述两部分结合起来,即把Directive中的数据依赖通过Watcher订阅在对应数据的 Observer 的 Dep 上。当数据变化时,就会触发 Observer 的 Dep 上的 notify 方法通知对应的 Watcher 的 update,进而触发 Directive 的 update 方法来更新 DOM 视图,最后达到模型和视图关联起来。

接下来我们就结合 Vue.js 的源码来详细介绍这三个过程。

Observer

首先来看一下 Vue.js 是如何给 data 对象添加 Observer 的。我们知道,Vue 实例创建的过程会有一个生命周期,其中有一个过程就是调用 vm.initData 方法处理 data 选项。initData 方法的源码定义如下:

<!-源码目录:src/instance/internal/state.js-->

Vue.prototype._initData = function () {

var dataFn = this.$options.data

var data = this._data = dataFn ? dataFn() : {}

if (!isPlainObject(data)) {

data = {}

process.env.NODE_ENV !== 'production' && warn(

'data functions should return an object.',

this

)

}

var props = this._props

// proxy data on instance

var keys = Object.keys(data)

var i, key

i = keys.length

while (i--) {

key = keys[i]

// there are two scenarios where we can proxy a data key:

// 1. it's not already defined as a prop

// 2. it's provided via a instantiation option AND there are no

// template prop present

if (!props || !hasOwn(props, key)) {

this._proxy(key)

} else if (process.env.NODE_ENV !== 'production') {

warn(

'Data field "' + key + '" is already defined ' +

'as a prop. To provide default value for a prop, use the "default" ' +

'prop option; if you want to pass prop values to an instantiation ' +

'call, use the "propsData" option.',

this

)

}

}

// observe data

observe(data, this)

}

在 initData 中我们要特别注意 proxy 方法,它的功能就是遍历 data 的 key,把 data 上的属性代理到 vm 实例上。_proxy 方法的源码定义如下:

<!-源码目录:src/instance/internal/state.js-->

Vue.prototype._proxy = function (key) {

if (!isReserved(key)) {

// need to store ref to self here

// because these getter/setters might

// be called by child scopes via

// prototype inheritance.

var self = this

Object.defineProperty(self, key, {

configurable: true,

enumerable: true,

get: function proxyGetter () {

return self._data[key]

},

set: function proxySetter (val) {

self._data[key] = val

}

})

}

}

proxy 方法主要通过 Object.defineProperty 的 getter 和 setter 方法实现了代理。在前面的例子中,我们调用 vm.times 就相当于访问了 vm.data.times。

在 _initData 方法的最后,我们调用了 observe(data, this) 方法来对 data 做监听。observe 方法的源码定义如下:

<!-源码目录:src/observer/index.js-->

export function observe (value, vm) {

if (!value || typeof value !== 'object') {

return

}

var ob

if (

hasOwn(value, '__ob__') &&

value.__ob__ instanceof Observer

) {

ob = value.__ob__

} else if (

shouldConvert &&

(isArray(value) || isPlainObject(value)) &&

Object.isExtensible(value) &&

!value._isVue

) {

ob = new Observer(value)

}

if (ob && vm) {

ob.addVm(vm)

}

return ob

}

observe 方法首先判断 value 是否已经添加了 ob 属性,它是一个 Observer 对象的实例。如果是就直接用,否则在 value 满足一些条件(数组或对象、可扩展、非 vue 组件等)的情况下创建一个 Observer 对象。接下来我们看一下 Observer 这个类,它的源码定义如下:

<!-源码目录:src/observer/index.js-->

export function Observer (value) {

this.value = value

this.dep = new Dep()

def(value, '__ob__', this)

if (isArray(value)) {

var augment = hasProto

? protoAugment

: copyAugment

augment(value, arrayMethods, arrayKeys)

this.observeArray(value)

} else {

this.walk(value)

}

}

Observer 类的构造函数主要做了这么几件事:首先创建了一个 Dep 对象实例(关于 Dep 对象我们稍后作介绍);然后把自身 this 添加到 value 的 ob 属性上;最后对 value 的类型进行判断,如果是数组则观察数组,否则观察单个元素。其实 observeArray 方法就是对数组进行遍历,递归调用 observe 方法,最终都会调用 walk 方法观察单个元素。接下来我们看一下 walk 方法,它的源码定义如下:

<!-源码目录:src/observer/index.js-->

Observer.prototype.walk = function (obj) {

var keys = Object.keys(obj)

for (var i = 0, l = keys.length; i < l; i++) {

this.convert(keys[i], obj[keys[i]])

}

}

walk 方法是对 obj 的 key 进行遍历,依次调用 convert 方法,对 obj 的每一个属性进行转换,让它们拥有 getter、setter 方法。只有当 obj 是一个对象时,这个方法才能被调用。接下来我们看一下 convert 方法,它的源码定义如下:

<!-源码目录:src/observer/index.js-->

Observer.prototype.convert = function (key, val) {

defineReactive(this.value, key, val)

}

convert 方法很简单,它调用了 defineReactive 方法。这里 this.value 就是要观察的 data 对象,key 是 data 对象的某个属性,val 则是这个属性的值。defineReactive 的功能是把要观察的 data 对象的每个属性都赋予 getter 和 setter 方法。这样一旦属性被访问或者更新,我们就可以追踪到这些变化。接下来我们看一下 defineReactive 方法,它的源码定义如下:

<!-源码目录:src/observer/index.js-->

export function defineReactive (obj, key, val) {

var dep = new Dep()

var property = Object.getOwnPropertyDescriptor(obj, key)

if (property && property.configurable === false) {

return

}

// cater for pre-defined getter/setters

var getter = property && property.get

var setter = property && property.set

var childOb = observe(val)

Object.defineProperty(obj, key, {

enumerable: true,

configurable: true,

get: function reactiveGetter () {

var value = getter ? getter.call(obj) : val

if (Dep.target) {

dep.depend()

if (childOb) {

childOb.dep.depend()

}

if (isArray(value)) {

for (var e, i = 0, l = value.length; i < l; i++) {

e = value[i]

e && e.__ob__ && e.__ob__.dep.depend()

}

}

}

return value

},

set: function reactiveSetter (newVal) {

var value = getter ? getter.call(obj) : val

if (newVal === value) {

return

}

if (setter) {

setter.call(obj, newVal)

} else {

val = newVal

}

childOb = observe(newVal)

dep.notify()

}

})

}

defineReactive 方法最核心的部分就是通过调用 Object.defineProperty 给 data 的每个属性添加 getter 和setter 方法。当 data 的某个属性被访问时,则会调用 getter 方法,判断当 Dep.target 不为空时调用 dep.depend 和 childObj.dep.depend 方法做依赖收集。如果访问的属性是一个数组,则会遍历这个数组收集数组元素的依赖。当改变 data 的属性时,则会调用 setter 方法,这时调用 dep.notify 方法进行通知。这里我们提到了 dep,它是 Dep 对象的实例。接下来我们看一下 Dep 这个类,它的源码定义如下:

<!-源码目录:src/observer/dep.js-->

export default function Dep () {

this.id = uid++

this.subs = []

}

// the current target watcher being evaluated.

// this is globally unique because there could be only one

// watcher being evaluated at any time.

Dep.target = null

Dep 类是一个简单的观察者模式的实现。它的构造函数非常简单,初始化了 id 和 subs。其中 subs 用来存储所有订阅它的 Watcher,Watcher 的实现稍后我们会介绍。Dep.target 表示当前正在计算的 Watcher,它是全局唯一的,因为在同一时间只能有一个 Watcher 被计算。

前面提到了在 getter 和 setter 方法调用时会分别调用 dep.depend 方法和 dep.notify 方法,接下来依次介绍这两个方法。depend 方法的源码定义如下:

<!-源码目录:src/observer/dep.js-->

Dep.prototype.depend = function () {

Dep.target.addDep(this)

}

depend 方法很简单,它通过 Dep.target.addDep(this) 方法把当前 Dep 的实例添加到当前正在计算的Watcher 的依赖中。接下来我们看一下 notify 方法,它的源码定义如下:

<!-源码目录:src/observer/dep.js-->

Dep.prototype.notify = function () {

// stablize the subscriber list first

var subs = toArray(this.subs)

for (var i = 0, l = subs.length; i < l; i++) {

subs[i].update()

}

}

notify 方法也很简单,它遍历了所有的订阅 Watcher,调用它们的 update 方法。

至此,vm 实例中给 data 对象添加 Observer 的过程就结束了。接下来我们看一下 Vue.js 是如何进行指令解析的。

Directive

Vue 指令类型很多,限于篇幅,我们不会把所有指令的解析过程都介绍一遍,这里结合前面的例子只介绍 v-text 指令的解析过程,其他指令的解析过程也大同小异。

前面我们提到了 Vue 实例创建的生命周期,在给 data 添加 Observer 之后,有一个过程是调用 vm.compile 方法对模板进行编译。compile 方法的源码定义如下:

<!-源码目录:src/instance/internal/lifecycle.js-->

Vue.prototype._compile = function (el) {

var options = this.$options

// transclude and init element

// transclude can potentially replace original

// so we need to keep reference; this step also injects

// the template and caches the original attributes

// on the container node and replacer node.

var original = el

el = transclude(el, options)

this._initElement(el)

// handle v-pre on root node (#2026)

if (el.nodeType === 1 && getAttr(el, 'v-pre') !== null) {

return

}

// root is always compiled per-instance, because

// container attrs and props can be different every time.

var contextOptions = this._context && this._context.$options

var rootLinker = compileRoot(el, options, contextOptions)

// resolve slot distribution

resolveSlots(this, options._content)

// compile and link the rest

var contentLinkFn

var ctor = this.constructor

// component compilation can be cached

// as long as it's not using inline-template

if (options._linkerCachable) {

contentLinkFn = ctor.linker

if (!contentLinkFn) {

contentLinkFn = ctor.linker = compile(el, options)

}

}

// link phase

// make sure to link root with prop scope!

var rootUnlinkFn = rootLinker(this, el, this._scope)

var contentUnlinkFn = contentLinkFn

? contentLinkFn(this, el)

: compile(el, options)(this, el)

// register composite unlink function

// to be called during instance destruction

this._unlinkFn = function () {

rootUnlinkFn()

// passing destroying: true to avoid searching and

// splicing the directives

contentUnlinkFn(true)

}

// finally replace original

if (options.replace) {

replace(original, el)

}

this._isCompiled = true

this._callHook('compiled')

}

我们可以通过下图来看一下这个方法编译的主要流程:

新知图谱, Vue 源码解析:深入响应式原理

这个过程通过 el = transclude(el, option) 方法把 template 编译成一段 document fragment,拿到 el 对象。而指令解析部分就是通过 compile(el, options) 方法实现的。接下来我们看一下 compile 方法的实现,它的源码定义如下:

<!-源码目录:src/compiler/compile.js-->

export function compile (el, options, partial) {

// link function for the node itself.

var nodeLinkFn = partial || !options._asComponent

? compileNode(el, options)

: null

// link function for the childNodes

var childLinkFn =

!(nodeLinkFn && nodeLinkFn.terminal) &&

!isScript(el) &&

el.hasChildNodes()

? compileNodeList(el.childNodes, options)

: null

/**

* A composite linker function to be called on a already

* compiled piece of DOM, which instantiates all directive

* instances.

*

* @param {Vue} vm

* @param {Element|DocumentFragment} el

* @param {Vue} [host] - host vm of transcluded content

* @param {Object} [scope] - v-for scope

* @param {Fragment} [frag] - link context fragment

* @return {Function|undefined}

*/

return function compositeLinkFn (vm, el, host, scope, frag) {

// cache childNodes before linking parent, fix #657

var childNodes = toArray(el.childNodes)

// link

var dirs = linkAndCapture(function compositeLinkCapturer () {

if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag)

if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag)

}, vm)

return makeUnlinkFn(vm, dirs)

}

}

compile 方法主要通过 compileNode(el, options) 方法完成节点的解析,如果节点拥有子节点,则调用 compileNodeList(el.childNodes, options) 方法完成子节点的解析。compileNodeList 方法其实就是遍历子节点,递归调用 compileNode 方法。因为 DOM 元素本身就是树结构,这种递归方法也就是常见的树的深度遍历方法,这样就可以完成整个 DOM 树节点的解析。接下来我们看一下 compileNode 方法的实现,它的源码定义如下:

<!-源码目录:src/compiler/compile.js-->

function compileNode (node, options) {

var type = node.nodeType

if (type === 1 && !isScript(node)) {

return compileElement(node, options)

} else if (type === 3 && node.data.trim()) {

return compileTextNode(node, options)

} else {

return null

}

}

compileNode 方法对节点的 nodeType 做判断,如果是一个非 script 普通的元素(div、p等);则调用 compileElement(node, options) 方法解析;如果是一个非空的文本节点,则调用 compileTextNode(node, options) 方法解析。我们在前面的例子中解析的是非空文本节点 count: {{times}},这实际上是 v-text 指令,它的解析是通过 compileTextNode 方法实现的。接下来我们看一下 compileTextNode 方法,它的源码定义如下:

<!-源码目录:src/compiler/compile.js-->

function compileTextNode (node, options) {

// skip marked text nodes

if (node._skip) {

return removeText

}

var tokens = parseText(node.wholeText)

if (!tokens) {

return null

}

// mark adjacent text nodes as skipped,

// because we are using node.wholeText to compile

// all adjacent text nodes together. This fixes

// issues in IE where sometimes it splits up a single

// text node into multiple ones.

var next = node.nextSibling

while (next && next.nodeType === 3) {

next._skip = true

next = next.nextSibling

}

var frag = document.createDocumentFragment()

var el, token

for (var i = 0, l = tokens.length; i < l; i++) {

token = tokens[i]

el = token.tag

? processTextToken(token, options)

: document.createTextNode(token.value)

frag.appendChild(el)

}

return makeTextNodeLinkFn(tokens, frag, options)

}

compileTextNode 方法首先调用了 parseText 方法对 node.wholeText 做解析。主要通过正则表达式解析 count: {{times}} 部分,我们看一下解析结果,如下图所示:

新知图谱, Vue 源码解析:深入响应式原理

解析后的 tokens 是一个数组,数组的每个元素则是一个 Object。如果是 count: 这样的普通文本,则返回的对象只有 value 字段;如果是 {{times}} 这样的插值,则返回的对象包含 html、onTime、tag、value 等字段。

接下来创建 document fragment,遍历 tokens 创建 DOM 节点插入到这个 fragment 中。在遍历过程中,如果 token 无 tag 字段,则调用 document.createTextNode(token.value) 方法创建 DOM 节点;否则调用processTextToken(token, options) 方法创建 DOM 节点和扩展 token 对象。我们看一下调用后的结果,如下图所示:

新知图谱, Vue 源码解析:深入响应式原理

可以看到,token 字段多了一个 descriptor 属性。这个属性包含了几个字段,其中 def 表示指令相关操作的对象,expression 为解析后的表达式,filters 为过滤器,name 为指令的名称。

在compileTextNode 方法的最后,调用 makeTextNodeLinkFn(tokens, frag, options) 并返回该方法执行的结果。接下来我们看一下 makeTextNodeLinkFn 方法,它的源码定义如下:

<!-源码目录:src/compiler/compile.js-->

function makeTextNodeLinkFn (tokens, frag) {

return function textNodeLinkFn (vm, el, host, scope) {

var fragClone = frag.cloneNode(true)

var childNodes = toArray(fragClone.childNodes)

var token, value, node

for (var i = 0, l = tokens.length; i < l; i++) {

token = tokens[i]

value = token.value

if (token.tag) {

node = childNodes[i]

if (token.oneTime) {

value = (scope || vm).$eval(value)

if (token.html) {

replace(node, parseTemplate(value, true))

} else {

node.data = _toString(value)

}

} else {

vm._bindDir(token.descriptor, node, host, scope)

}

}

}

replace(el, fragClone)

}

}

makeTextNodeLinkFn 这个方法什么也没做,它仅仅是返回了一个新的方法 textNodeLinkFn。往前回溯,这个方法最终作为 compileNode 的返回值,被添加到 compile 方法生成的 childLinkFn 中。

我们回到 compile 方法,在 compile 方法的最后有这样一段代码:

<!-源码目录:src/compiler/compile.js-->

return function compositeLinkFn (vm, el, host, scope, frag) {

// cache childNodes before linking parent, fix #657

var childNodes = toArray(el.childNodes)

// link

var dirs = linkAndCapture(function compositeLinkCapturer () {

if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag)

if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag)

}, vm)

return makeUnlinkFn(vm, dirs)

}

compile 方法返回了 compositeLinkFn,它在 Vue.prototype._compile 方法执行时,是通过 compile(el, options)(this, el) 调用的。compositeLinkFn 方法执行了 linkAndCapture 方法,它的功能是通过调用 compile 过程中生成的 link 方法创建指令对象,再对指令对象做一些绑定操作。linkAndCapture 方法的源码定义如下:

<!-源码目录:src/compiler/compile.js-->

function linkAndCapture (linker, vm) {

/* istanbul ignore if */

if (process.env.NODE_ENV === 'production') {

// reset directives before every capture in production

// mode, so that when unlinking we don't need to splice

// them out (which turns out to be a perf hit).

// they are kept in development mode because they are

// useful for Vue's own tests.

vm._directives = []

}

var originalDirCount = vm._directives.length

linker()

var dirs = vm._directives.slice(originalDirCount)

dirs.sort(directiveComparator)

for (var i = 0, l = dirs.length; i < l; i++) {

dirs[i]._bind()

}

return dirs

}

linkAndCapture 方法首先调用了 linker 方法,它会遍历 compile 过程中生成的所有 linkFn 并调用,本例中会调用到之前定义的 textNodeLinkFn。这个方法会遍历 tokens,判断如果 token 的 tag 属性值为 true 且 oneTime 属性值为 false,则调用 vm.bindDir(token.descriptor, node, host, scope) 方法创建指令对象。vm._bindDir 方法的源码定义如下:

<!-源码目录:src/instance/internal/lifecycle.js-->

Vue.prototype._bindDir = function (descriptor, node, host, scope, frag) {

this._directives.push(

new Directive(descriptor, this, node, host, scope, frag)

)

}

Vue.prototype._bindDir 方法就是根据 descriptor 实例化不同的 Directive 对象,并添加到 vm 实例 directives 数组中的。到这一步,Vue.js 从解析模板到生成 Directive 对象的步骤就完成了。接下来回到 linkAndCapture 方法,它对创建好的 directives 进行排序,然后遍历 directives 调用 dirs[i]._bind 方法对单个directive做一些绑定操作。dirs[i]._bind方法的源码定义如下:

<!-源码目录:src/directive.js-->

Directive.prototype._bind = function () {

var name = this.name

var descriptor = this.descriptor

// remove attribute

if (

(name !== 'cloak' || this.vm._isCompiled) &&

this.el && this.el.removeAttribute

) {

var attr = descriptor.attr || ('v-' + name)

this.el.removeAttribute(attr)

}

// copy def properties

var def = descriptor.def

if (typeof def === 'function') {

this.update = def

} else {

extend(this, def)

}

// setup directive params

this._setupParams()

// initial bind

if (this.bind) {

this.bind()

}

this._bound = true

if (this.literal) {

this.update && this.update(descriptor.raw)

} else if (

(this.expression || this.modifiers) &&

(this.update || this.twoWay) &&

!this._checkStatement()

) {

// wrapped updater for context

var dir = this

if (this.update) {

this._update = function (val, oldVal) {

if (!dir._locked) {

dir.update(val, oldVal)

}

}

} else {

this._update = noop

}

var preProcess = this._preProcess

? bind(this._preProcess, this)

: null

var postProcess = this._postProcess

? bind(this._postProcess, this)

: null

var watcher = this._watcher = new Watcher(

this.vm,

this.expression,

this._update, // callback

{

filters: this.filters,

twoWay: this.twoWay,

deep: this.deep,

preProcess: preProcess,

postProcess: postProcess,

scope: this._scope

}

)

// v-model with inital inline value need to sync back to

// model instead of update to DOM on init. They would

// set the afterBind hook to indicate that.

if (this.afterBind) {

this.afterBind()

} else if (this.update) {

this.update(watcher.value)

}

}

}

Directive.prototype._bind 方法的主要功能就是做一些指令的初始化操作,如混合 def 属性。def 是通过 this.descriptor.def 获得的,this.descriptor 是对指令进行相关描述的对象,而 this.descriptor.def 则是包含指令相关操作的对象。比如对于 v-text 指令,我们可以看一下它的相关操作,源码定义如下:

<!-源码目录:src/directives/public/text.js-->

export default {

bind () {

this.attr = this.el.nodeType === 3

? 'data'

: 'textContent'

},

update (value) {

this.el[this.attr] = _toString(value)

}

}

v-text 的 def 包含了 bind 和 update 方法,Directive 在初始化时通过 extend(this, def) 方法可以对实例扩展这两个方法。Directive 在初始化时还定义了 this.update 方法,并创建了 Watcher,把 this.update 方法作为 Watcher 的回调函数。这里把 Directive 和 Watcher 做了关联,当 Watcher 观察到指令表达式值变化时,会调用 Directive 实例的 _update 方法,最终调用 v-text 的 update 方法更新 DOM 节点。

至此,vm 实例中编译模板、解析指令、绑定 Watcher 的过程就结束了。接下来我们看一下 Watcher 的实现,了解 Directive 和 Observer 之间是如何通过 Watcher 关联的。

Watcher

我们先来看一下 Watcher 类的实现,它的源码定义如下:

<!-源码目录:src/watcher.js-->

export default function Watcher (vm, expOrFn, cb, options) {

// mix in options

if (options) {

extend(this, options)

}

var isFn = typeof expOrFn === 'function'

this.vm = vm

vm._watchers.push(this)

this.expression = expOrFn

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.prevError = null // for async error stacks

// parse expression for getter/setter

if (isFn) {

this.getter = expOrFn

this.setter = undefined

} else {

var res = parseExpression(expOrFn, this.twoWay)

this.getter = res.get

this.setter = res.set

}

this.value = this.lazy

? undefined

: this.get()

// state for avoiding false triggers for deep and Array

// watchers during vm._digest()

this.queued = this.shallow = false

}

Directive 实例在初始化 Watche r时,会传入指令的 expression。Watcher 构造函数会通过 parseExpression(expOrFn, this.twoWay) 方法对 expression 做进一步的解析。在前面的例子中, expression 是times,passExpression 方法的功能是把 expression 转换成一个对象,如下图所示:

新知图谱, Vue 源码解析:深入响应式原理

可以看到 res 有两个属性,其中 exp 为表达式字符串;get 是通过 new Function 生成的匿名方法,可以把它打印出来,如下图所示:

新知图谱, Vue 源码解析:深入响应式原理

可以看到 res.get 方法很简单,它接受传入一个 scope 变量,返回 scope.times。对于传入的 scope 值,稍后我们会进行介绍。在 Watcher 构造函数的最后调用了 this.get 方法,它的源码定义如下:

<!-源码目录:src/watcher.js-->

Watcher.prototype.get = function () {

this.beforeGet()

var scope = this.scope || this.vm

var value

try {

value = this.getter.call(scope, scope)

} catch (e) {

if (

process.env.NODE_ENV !== 'production' &&

config.warnExpressionErrors

) {

warn(

'Error when evaluating expression ' +

'"' + this.expression + '": ' + e.toString(),

this.vm

)

}

}

// "touch" every property so they are all tracked as

// dependencies for deep watching

if (this.deep) {

traverse(value)

}

if (this.preProcess) {

value = this.preProcess(value)

}

if (this.filters) {

value = scope._applyFilters(value, null, this.filters, false)

}

if (this.postProcess) {

value = this.postProcess(value)

}

this.afterGet()

return value

}

Watcher.prototype.get 方法的功能就是对当前 Watcher 进行求值,收集依赖关系。它首先执行 this.beforeGet 方法,源码定义如下:

<!-源码目录:src/watcher.js-->

Watcher.prototype.beforeGet = function () {

Dep.target = this

}

Watcher.prototype.beforeGet 很简单,设置 Dep.target 为当前 Watcher 实例,为接下来的依赖收集做准备。我们回到 get 方法,接下来执行 this.getter.call(scope, scope) 方法,这里的 scope 是 this.vm,也就是当前 Vue 实例。这个方法实际上相当于获取 vm.times,这样就触发了对象的 getter。在第一小节我们给 data 添加 Observer 时,通过 Object.defineProperty 给 data 对象的每一个属性添加 getter 和 setter。回顾一下代码:

<!-源码目录:src/observer/index.js-->

Object.defineProperty(obj, key, {

enumerable: true,

configurable: true,

get: function reactiveGetter () {

var value = getter ? getter.call(obj) : val

if (Dep.target) {

dep.depend()

if (childOb) {

childOb.dep.depend()

}

if (isArray(value)) {

for (var e, i = 0, l = value.length; i < l; i++) {

e = value[i]

e && e.__ob__ && e.__ob__.dep.depend()

}

}

}

return value

},

})

当获取 vm.times 时,会执行到 get 方法体内。由于我们在之前已经设置了 Dep.target 为当前 Watcher 实例,所以接下来就调用 dep.depend() 方法完成依赖收集。它实际上是执行了 Dep.target.addDep(this),相当于执行了 Watcher 实例的 addDep 方法,把 Dep 实例添加到 Watcher 实例的依赖中。addDep 方法的源码定义如下:

<!-源码目录:src/watcher.js-->

Watcher.prototype.addDep = function (dep) {

var id = dep.id

if (!this.newDepIds.has(id)) {

this.newDepIds.add(id)

this.newDeps.push(dep)

if (!this.depIds.has(id)) {

dep.addSub(this)

}

}

}

Watcher.prototype.addDep 方法就是把 dep 添加到 Watcher 实例的依赖中,同时又通过 dep.addSub(this) 把 Watcher 实例添加到 dep 的订阅者中。addSub 方法的源码定义如下:

<!-源码目录:src/observer/dep.js-->

Dep.prototype.addSub = function (sub) {

this.subs.push(sub)

}

至此,指令完成了依赖收集,并且通过 Watcher 完成了对数据变化的订阅。

接下来我们看一下,当 data 发生变化时,视图是如何自动更新的。在前面的例子中,我们通过 setInterval 每隔 1s 执行一次 vm.times++,数据改变会触发对象的 setter,执行 set 方法体的代码。回顾一下代码:

<!-源码目录:src/observer/index.js-->

Object.defineProperty(obj, key, {

set: function reactiveSetter (newVal) {

var value = getter ? getter.call(obj) : val

if (newVal === value) {

return

}

if (setter) {

setter.call(obj, newVal)

} else {

val = newVal

}

childOb = observe(newVal)

dep.notify()

}

})

这里会调用 dep.notify() 方法,它会遍历所有的订阅者,也就是 Watcher 实例。然后调用 Watcher 实例的 update 方法,源码定义如下:

<!-源码目录:src/watcher.js-->

Watcher.prototype.update = function (shallow) {

if (this.lazy) {

this.dirty = true

} else if (this.sync || !config.async) {

this.run()

} else {

// if queued, only overwrite shallow with non-shallow,

// but not the other way around.

this.shallow = this.queued

? shallow

? this.shallow

: false

: !!shallow

this.queued = true

// record before-push error stack in debug mode

/* istanbul ignore if */

if (process.env.NODE_ENV !== 'production' && config.debug) {

this.prevError = new Error('[vue] async stack trace')

}

pushWatcher(this)

}

}

Watcher.prototype.update 方法在满足某些条件下会直接调用 this.run 方法。在多数情况下会调用 pushWatcher(this) 方法把 Watcher 实例推入队列中,延迟 this.run 调用的时机。pushWatcher 方法的源码定义如下:

<!-源码目录:src/batcher.js-->

export function pushWatcher (watcher) {

const id = watcher.id

if (has[id] == null) {

// push watcher into appropriate queue

const q = watcher.user

? userQueue

: queue

has[id] = q.length

q.push(watcher)

// queue the flush

if (!waiting) {

waiting = true

nextTick(flushBatcherQueue)

}

}

}

pushWatcher 方法把 Watcher 推入队列中,通过 nextTick 方法在下一个事件循环周期处理 Watcher 队列,这是 Vue.j s的一种性能优化手段。因为如果同时观察的数据多次变化,比如同步执行 3 次 vm.time++,同步调用 watcher.run 就会触发 3 次 DOM 操作。而推入队列中等待下一个事件循环周期再操作队列里的 Watcher,因为是同一个 Watcher,它只会调用一次 watcher.run,从而只触发一次 DOM 操作。接下来我们看一下 flushBatcherQueue 方法,它的源码定义如下:

<!-源码目录:src/batcher.js-->

function flushBatcherQueue () {

runBatcherQueue(queue)

runBatcherQueue(userQueue)

// user watchers triggered more watchers,

// keep flushing until it depletes

if (queue.length) {

return flushBatcherQueue()

}

// dev tool hook

/* istanbul ignore if */

if (devtools && config.devtools) {

devtools.emit('flush')

}

resetBatcherState()

}

flushBatcherQueue 方法通过调用 runBatcherQueue 来 run Watcher。这里我们看到 Watcher 队列分为内部 queue 和 userQueue,其中 userQueue 是通过 $watch() 方法注册的 Watcher。我们优先 run 内部queue 来保证指令和 DOM 节点优先更新,这样当用户自定义的 Watcher 的回调函数触发时 DOM 已更新完毕。接下来我们看一下 runBatcherQueue 方法,它的源码定义如下:

<!-源码目录:src/batcher.js-->

function runBatcherQueue (queue) {

// do not cache length because more watchers might be pushed

// as we run existing watchers

for (let i = 0; i < queue.length; i++) {

var watcher = queue[i]

var 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] > config._maxUpdateCount) {

warn(

'You may have an infinite update loop for watcher ' +

'with expression "' + watcher.expression + '"',

watcher.vm

)

break

}

}

}

queue.length = 0

}

runBatcherQueued 的功能就是遍历 queue 中 Watcher 的 run 方法。接下来我们看一下 Watcher 的 run 方法,它的源码定义如下:

<!-源码目录:src/watcher.js-->

Watcher.prototype.run = function () {

if (this.active) {

var 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; but only do so if this is a

// non-shallow update (caused by a vm digest).

((isObject(value) || this.deep) && !this.shallow)

) {

// set new value

var oldValue = this.value

this.value = value

// in debug + async mode, when a watcher callbacks

// throws, we also throw the saved before-push error

// so the full cross-tick stack trace is available.

var prevError = this.prevError

/* istanbul ignore if */

if (process.env.NODE_ENV !== 'production' &&

config.debug && prevError) {

this.prevError = null

try {

this.cb.call(this.vm, value, oldValue)

} catch (e) {

nextTick(function () {

throw prevError

}, 0)

throw e

}

} else {

this.cb.call(this.vm, value, oldValue)

}

}

this.queued = this.shallow = false

}

}

Watcher.prototype.run 方法再次对 Watcher 求值,重新收集依赖。接下来判断求值结果和之前 value 的关系。如果不变则什么也不做,如果变了则调用 this.cb.call(this.vm, value, oldValue) 方法。这个方法是 Directive 实例创建 Watcher 时传入的,它对应相关指令的 update 方法来真实更新 DOM。这样就完成了数据更新到对应视图的变化过程。Watcher 巧妙地把 Observer 和 Directive 关联起来,实现了数据一旦更新,视图就会自动变化的效果。尽管 Vue.js 利用 Object.defineProperty 这个核心技术实现了数据和视图的绑定,但仍然会存在一些数据变化检测不到的问题,接下来我们看一下这部分内容。

相关推荐

更多“Vue”相关内容

新知精选