vue 2.X 项目结构

这篇文章CC会记录一下学习

vue [1]

源码的笔记。基于的源码是

2.6 [2]

的版本。

项目目录结构

下图为源码的目录结构,相关信息可以参见

CONTRIBUTING [3]

文件。

整体目录结构
整体目录结构

主要的业务代码都在src中,其它的目录和一般比较规范的前端JS框架项目类似。

整体架构

从整体架构来看,Vue的代码可以分为下面几个层次:

*  compile层。负责对template的html字符串编译得到AST
*  vdom层。负责将AST转为VNode,并提供基于patch从VNode生成DOM的能力
*  observer层。负责提供双向的数据绑定,例如将VNode与其使用到的data绑定,保证data的更新能变动到VNode上并最终落地DOM
*  instance层。负责提供Vue核心数据结构对象的各个方法,包含其生命周期的管理、options的管理等

层与层之间的关系如下:

层与层之间的关系
层与层之间的关系

每一层都可以单独使用。例如compile实际实现了一个template语法的解析器,vdom提供了一套vue的虚拟DOM实现方式,observer则提供一套基于get和set的数据绑定框架,而instance则基于这些功能将用户定义的data和methods作为observer的数据进行使用,实现了一个MVVM的框架。

核心对象间的关系如下,后文CC会重点介绍:

VNode和其它对象的关系
VNode和其它对象的关系

关键代码

这里CC列一下vue中几个CC个人比较感兴趣的地方的实现。

入口代码和打包

打包从package.json中找入口,对应的build的命令为:

"build": "node scripts/build.js",

--- package.json

从build.js的内容看vue使用了rollup.js来打包自己的代码。build.js里比较重要的代码如下:

let builds = require('./config').getAllBuilds() ...... build(builds)

--- scripts\build.js

其通过config.js获取到本次build需要build哪些target,然后对config中获取的build依次执行build函数。build函数主要就是调用config中获取到的target的build动作,然后进行zip压缩后写入到生成文件中,因此这里的重点是config.js中的内容。CC没有用过rollup.js,但这里的config.js应该是满足rollup.js要求的格式。

config.js的builds常量中包含了各类需要被build的target,例如vue.js的target:

// 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 },

--- scripts\config.js

vue.js的入口在这里为上面代码显示的

web/entry-runtime-with-compiler.js

。下面看下这个js文件的实现。

entry-runtime-with-compiler.js的源码在platform的web目录下,从目录结构中我们知道platform中包含的是平台相关的源码。目前在2.6的源码中,platform下面包含了web和weex两个平台。

在entry-runtime-with-compiler.js中可以看到,其对mount方法进行了重载,另外就是给compile进行了赋值:

const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( ...... Vue.compile = compileToFunctions

--- src\platforms\web\entry-runtime-with-compiler.js

这里的Vue对象依旧是platform下的对象,不是core中的,因此代码还是平台相关的。Vue的导入语句为:

import Vue from './runtime/index'

--- src\platforms\web\entry-runtime-with-compiler.js

而在runtime/index中的Vue才来自于平台无关的Vue核心代码:

import Vue from 'core/index'

--- src\platforms\web\runtime\index.js

现在我们来看下entry-runtime-with-compiler.js和runtime/index.js中平台相关的代码都做了什么事。先看index.js的:

// install platform specific utils Vue.config.mustUseProp = mustUseProp Vue.config.isReservedTag = isReservedTag Vue.config.isReservedAttr = isReservedAttr Vue.config.getTagNamespace = getTagNamespace Vue.config.isUnknownElement = isUnknownElement

--- src\platforms\web\runtime\index.js

这里给Vue的config赋值了一些平台特定的工具方法。接着:

// install platform runtime directives & components extend(Vue.options.directives, platformDirectives) extend(Vue.options.components, platformComponents)

--- src\platforms\web\runtime\index.js

这里注册了一些平台特定的directives和components。接着:

// install platform patch function Vue.prototype.__patch__ = inBrowser ? patch : noop // public mount method Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }

--- src\platforms\web\runtime\index.js

这里给Vue对象薪资了moune和__patch__两个方法。其中mountComponent来自于core,因此是平台无关的。接着:

// 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) }

--- src\platforms\web\runtime\index.js

这里的代码就是用于判断是否要显示devtools信息。

可以看到platforms/web/runtime/index.js主要做了下面几件事:

*  赋值了几个平台特定的方法
*  加载注册了平台特定的directive和component
*  生成了mount方法
*  判断是否显示devtools信息

而对于entry-runtime-with-compiler.js来说,则是由其名字显示的一样,对mount方法又添加了compiler的动作。所以对于platform来说,其主要做的事情就是runtime/index.js做的几个事情,平台特定代码也就是主要是指一些工具函数以及directive和component的特定代码。在使用vue的时候入口就是new Vue,这里的Vue按照这里的分析是在core/index中,而在core/index中可以看到vue引用自instance/index,因此如果要学习Vue的代码的话,从这里开始学习即可:

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

--- src\core\instance\index.js

数据绑定的实现

来看个官网上简单的数据绑定例子的代码:

<div id="app-6"> <p>{{ message }}</p> <input v-model="message"> </div>

--- https://cn.vuejs.org/v2/guide/

var app6 = new Vue({ el: '#app-6', data: { message: 'Hello Vue!' } })

--- https://cn.vuejs.org/v2/guide/

这里CC看源码前有两个想知道的事情,一个是message的变动是怎么更新input的value的,另一个是input的value的变动是怎么更新message的值的。

最底层的原理大家都知道,基于JS提供的getter和setter可以实现这个效果(不过实现这个效果对于JS来说有

好几种方式 [4]

)。现在很多的语言都提供了这种机制,例如python、swift等。不过看下vue对这个机制的封装还是很有趣的。

先来看message的变动是怎么影响到input的吧。在new Vue这个instance后,关键的代码如下:

observe(data, true /* asRootData */)

--- src\core\instance\state.js

这里的data就是new Vue参数中的data。observe后面的true表面这里的data是root的data,可以猜测子组件也是用的这个方法实现数据绑定,但参数传递的应该是false。

observe的功能就是实现数据绑定,代码如下:

/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */ 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 }

--- src\core\observer\index.js

这里通过new Observer生成一个Observer实例,这几个对象之间的关系如下:

Observer和data的关系
Observer和data的关系

可以看到vue在这里将data转为了一个Observer,后续这个Observer的任何get和set操作,包括其属性的get和set操作都会被监听并触发绑定的事件。Observer的代码如下:

/** * Observer class that is attached to each observed * object. Once attached, the observer converts the target * object's property keys into getter/setters that * collect dependencies and dispatch updates. */ 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) } }

--- src\core\observer\index.js

重点看这里的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]) } }

--- src\core\observer\index.js

defineReactive是数据绑定的关键代码,但实际上它的实现非常简单:

/** * Define a reactive property on an Object. */ 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)111 Object.defineProperty(obj, key, { enumerable: true, configurable: true, 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 }, 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() } }) }

--- src\core\observer\index.js

defineReactive对相关对象执行了上面提到的observe方法,可以看到这里没有指定第二个参数。接着通过defineProperty绑定get和set方法。对于get来说,每次有一个使用了此对象的调用时,其都会的调用dep.depend将其加入到依赖中。对于set,每次对这个对象进行新的赋值时,都会通过dep.notify对所有get过此对象的依赖执行通知操作,vue就是通过这个机制实现了从变量到变量依赖的通知机制。

注意这里的dep由于其被get和set引用,因此这里的get和set其实是个闭包,捕获了dep。

在Dep中,订阅Dep的订阅者在类型里叫做Wathcer:

/** * 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) { 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() } } }

--- src\core\observer\dep.js

因此整体的关系图为:

对象关系
对象关系

从图中可以清楚的看到对象与对象之间的关系。

既然知道了绑定的工作方式,那么就来看下template是怎么和这里的data关联起来的,及template是怎么dep到data的。首先从上面的代码可以知道Watcher会订阅Dep,而这个订阅动作从代码看应该发生在getter阶段。注意在getter中有如下的代码:

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 },

--- src\core\observer\index.js

这里if Dep.target是关键,如果Dep.target不为空,那么dep.depend()就会设置这个依赖关系。Dep.target从全局的代码看都是null的,而真正对其赋值的阶段,正是发生在Watcher对象中:

/** * 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 }

--- src\core\observer\watcher.js

注意这里的pushTarget和popTarget两个动作,其正是将Dep.target这个全局变量分别设置为Watcher自身及重新设置为null。

可以猜测,template中input绑定的message最终会成为一个watcher。其在被获取其值的时候,会调用到data中的message的get动作,此时这个绑定关系就完成了。具体template的实现参见下文。

template

这里看下template的实现,template实现分为两部分,一部分是编译,一部分是生成watcher并和data绑定。

template的parser入口代码如下:

const ast = parse(template.trim(), options) if (options.optimize !== false) { optimize(ast, options) }

--- src\compiler\index.js

可以看到这里会编译template的字符串生成AST。parse的实现是一个手写的HTML词法及语法分析器。

parse的结果是一个ast对象。在flow中可以看到起定义:

declare type ASTElement = { type: 1; tag: string; attrsList: Array<ASTAttr>; attrsMap: { [key: string]: any }; rawAttrsMap: { [key: string]: ASTAttr }; parent: ASTElement | void; children: Array<ASTNode>; start?: number; end?: number; processed?: true; static?: boolean; staticRoot?: boolean; staticInFor?: boolean; staticProcessed?: boolean; hasBindings?: boolean; text?: string; attrs?: Array<ASTAttr>; props?: Array<ASTAttr>; plain?: boolean; pre?: true; ns?: string; component?: string; inlineTemplate?: true; transitionMode?: string | null; slotName?: ?string; slotTarget?: ?string; slotScope?: ?string; scopedSlots?: { [name: string]: ASTElement }; ref?: string; refInFor?: boolean; if?: string; ifProcessed?: boolean; elseif?: string; else?: true; ifConditions?: ASTIfConditions; for?: string; forProcessed?: boolean; key?: string; alias?: string; iterator1?: string; iterator2?: string; staticClass?: string; classBinding?: string; staticStyle?: string; styleBinding?: string; events?: ASTElementHandlers; nativeEvents?: ASTElementHandlers; transition?: string | true; transitionOnAppear?: boolean; model?: { value: string; callback: string; expression: string; }; directives?: Array<ASTDirective>; forbidden?: true; once?: true; onceProcessed?: boolean; wrapData?: (code: string) => string; wrapListeners?: (code: string) => string; // 2.4 ssr optimization ssrOptimizability?: number; // weex specific appendAsTree?: boolean; // 2.6 $slot check has$Slot?: boolean };

--- flow\compiler.js

在生成ast后,会调用generate生成最终的结果:

const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } })

--- src\compiler\index.js

generate的实现如下:

export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult { const state = new CodegenState(options) const code = ast ? genElement(ast, state) : '_c("div")' return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns } }

--- src\compiler\index.js

这里generate的结果,都会被vue放入到options中:

const { render, staticRenderFns } = compileToFunctions(template, { outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns

--- src\platforms\web\entry-runtime-with-compiler.js

对于generate来说,其重点是生成code,因为render的核心就是code的字符串内容。在Vue中,基于虚拟DOM来实现对真实DOM的操作,这里的code可以认为就是一个虚拟DOM。简单的可以认为虚拟DOM就是一个数据结构,这个数据结构最终可以生成真实的DOM。因此这里的数据绑定应该就是将options的data和虚拟DOM进行绑定。

那么这里解析器生成的虚拟DOM是怎么和options中的data绑定的呢?具体代码如下:

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 && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }

--- src\core\instance\lifecycle.js

这段代码主要就是创建一个Render的Watcher。之前CC提到过,Vue的数据绑定主要是通过订阅Dep的Watcher来获取到通知的,因此可以猜测这里的Render Watcher就是负责监听虚拟DOM的一个Watcher。如果是这样的话这个Watcher的构造方法里应该会进行数据绑定,来看下其实现:

this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this)

--- src\core\observer\watcher.js

这里isRenderWatcher为true,因此vm的_watcher就绑定到了这个Watcher上。接着的重要代码为:

this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } }

--- src\core\observer\watcher.js

可以看到这个Watcher的getter被赋值了expOrFn。exprOrFn的内容为:

updateComponent = () => { vm._update(vm._render(), hydrating) }

--- src\core\instance\lifecycle.js

在Watcher构造方法的最后,其会调用其get方法:

this.value = this.lazy ? undefined : this.get()

--- src\core\observer\watcher.js

这里的get在上文提到Dep.Target的时候有提到过,这里再来看一下:

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 }

--- src\core\observer\watcher.js

可以看到这里首先将Dep.Target设置为该Render Watcher,接着执行this.getter方法,即上面提到的updateComponent。如果不出意外那么Render中的变量的绑定就是发生在这里的。来看下updateComponent的实现:

updateComponent = () => { vm._update(vm._render(), hydrating) }

--- src\core\instance\lifecycle.js

首先来看_render()的实现:

Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots ) } // set parent vnode. this allows render functions to have access // to the data on the placeholder node. vm.$vnode = _parentVnode // render self let vnode try { vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) { try { vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) } catch (e) { handleError(e, vm, `renderError`) vnode = vm._vnode } } else { vnode = vm._vnode } } // if the returned array contains only a single node, allow it if (Array.isArray(vnode) && vnode.length === 1) { vnode = vnode[0] } // return empty vnode in case the render function errored out if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ) } vnode = createEmptyVNode() } // set parent vnode.parent = _parentVnode return vnode }

--- src\core\instance\render.js

其主要用途是生成vnode,即虚拟DOM。而生成这个vnode则是上面提到的options中的render函数。render的具体代码这里CC就不看了,只要知道现在vnode以及生成完毕了,其包含了虚拟DOM的所有内容,同时也包含了template中的所有内容就行。

_update的定义如下:

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. }

--- src\core\instance\lifecycle.js

可以看到起主要是调用__patch__进行“补丁”动作。这里的__patch__就涉及到虚拟DOM的原理了。如果每次虚拟DOM一被更新就去同步生成DOM的话,性能是无法接受的。因此这里的patch就类似于打补丁一样,打个patch,只把diff出来的差异找出来并运用到DOM中。

也就是说在Render Watcher的get调用中,触发了虚拟DOM的真实生成DOM的动作,自然其涉及到的相关options的data如果被用到的话肯定也会被调用其相关的get方法,此时Render Watcher就订阅了相关data的通知。后续这些data变动时,就会调用其update方法。

值得一提的是对于Watcher来说,其update方法其实是一个调度器。感兴趣的可以看下其代码:

/** * 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) } } } }

--- src\core\observer\watcher.js

这几个对象间的关系如下图所示:

VNode和其它对象的关系
VNode和其它对象的关系
[1] https://cn.vuejs.org/index.html
[2] https://github.com/vuejs/vue/tree/2.6
[3] https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md
[4] https://segmentfault.com/a/1190000003882976
本文微信分享二维码


本文由星空水景整理编写。
如需转载请注明出处并保留文章所有引用的资料来源。
欢迎关注 星空水景 微博[微博搜索 星空水景 或扫描下方二维码]。