1. 首页

【大型干货】手拉手带你过一遍 vue 部分源码

本文希望可以帮助那些想吃蛋糕,但又觉得蛋糕太大而又不知道从哪下口的人们。

一、如何开始第一步

  • 将源码项目clone下来后,按照CONTRIBUTING中的Development Setup中的顺序,逐个执行下来

$ npm install # watch and auto re-build dist/vue.js $ npm run dev
  • 学会看package.json文件,就像你在使用MVVM去关注它的render一样。

既然$ npm run dev命令可以重新编译出vue.js文件,那么我们就从scripts中的dev开始看吧。


"dev":"rollup -w -c scripts/config.js --environment TARGET:web-full-dev"

如果这里你还不清楚rollup是做什么的,可以戳这里,简单来说就是一个模块化打包工具。具体的介绍这里就跳过了,因为我们是来看vue的,如果太跳跃的话,基本就把这次主要想做的事忽略掉了,跳跳跳不一定跳哪里了,所以在阅读源码的时候,一定要牢记这次我们的目的是什么。

注意上面指令中的两个关键词scripts/config.jsweb-full-dev,接下来让我们看看script/config.js这个文件。


if (process.env.TARGET) { module.exports = genConfig(process.env.TARGET) } else { exports.getBuild = genConfig exports.getAllBuilds = () => Object.keys(builds).map(genConfig) }

回忆上面的命令,我们传入的TARGETweb-full-dev,那么带入到方法中,最终会看到这样一个object


'web-full-dev': { // 入口文件 entry: resolve('web/entry-runtime-with-compiler.js'), // 输出文件 dest: resolve('dist/vue.js'), // 格式 format: 'umd', // 环境 env: 'development', // 别名 alias: { he: './entity-decoder' }, banner },

虽然这里我们还不知道它具体是做什么的,暂且通过语义来给它补上注释吧。既然有了入口文件,那么我们继续打开文件web/entry-runtime-with-compiler.js。OK,打开这个文件后,终于看到了我们的一个目标关键词


import Vue from './runtime/index'

江湖规矩,继续往这个文件里跳,然后你就会看到:


import Vue from 'core/index'

是不是又看到了代码第一行中熟悉的关键词Vue


import Vue from './instance/index'

打开instance/index后,结束了我们的第一步,已经从package.json中到框架中的文件,找到了Vue的定义地方。让我们再回顾下流程:

【大型干货】手拉手带你过一遍 vue 部分源码

二、学会利用demo

切记,在看源码时为了防止看着看着看跑偏了,我们一定要按照代码执行的顺序看。

  • 项目结构中有examples目录,让我们也创建一个属于自己的demo在这里面吧,随便copy一个目录,命名为demo,后面我们的代码都通过这个demo来进行测试、观察。

    index.html内容如下:


<!DOCTYPE html> <html> <head> <title>Demo</title> <script src="../../dist/vue.js">//JS中文网 – 前端进阶资源分享 https://www.javascriptc.com/ </script> </head> <body> <div id="demo"> <\template> <span>{{text}}</span> </template> </div> <script src="app.js">//JS中文网 – 前端进阶资源分享 https://www.javascriptc.com/ </script> </body> </html>
app.js文件内容如下:

var demo = new Vue({ el: '#demo', data() { return { text: 'hello world!' } } })

引入vue.js

上面demo的html中我们引入了dist/vue.js,那么window下,就会有Vue对象,暂且先将app.js的代码修改如下:


console.dir(Vue);

如果这里你还不知道console.dir,而只知道console.log,那你就亲自试试然后记住他们之间的差异吧。

从控制台我们可以看出,Vue对象以及原型上有一系列属性,那么这些属性是从哪儿来的,做什么的,就是我们后续去深入的内容。

三、从哪儿来的

是否还记得我们在第一章中找到最终Vue构造函数的文件?如果不记得了,就再回去看一眼吧,我们在本章会按照那个顺序倒着来看一遍Vue的属性挂载。

instance(src/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


接下来我们就开始按照代码执行的顺序,先来分别看看这几个函数到底是弄啥嘞?


initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue)
  1. initMixin(src/core/instance/init.js)

Vue.prototype._init = function (options?: Object) {}

JS中文网 – 前端进阶资源教程 www.javascriptC.com
一个致力于帮助开发者用代码改变世界为使命的平台,每天都可以在这里找到技术世界的头条内容

在传入的Vue对象的原型上挂载了_init方法。

  1. stateMixin(src/core/instance/state.js)

// Object.defineProperty(Vue.prototype, '$data', dataDef) // 这里$data只提供了get方法,set方法再非生产环境时会给予警告 Vue.prototype.$data = undefined; // Object.defineProperty(Vue.prototype, '$props', propsDef) // 这里$props只提供了get方法,set方法再非生产环境时会给予警告 Vue.prototype.$props = undefined; Vue.prototype.$set = set Vue.prototype.$delete = del Vue.prototype.$watch = function() {}
> 如果这里你还不知道`Object.defineProperty`是做什么的,我对你的建议是可以把对象的原型这部分好好看一眼,对于后面的代码浏览会有很大的效率提升,不然云里雾里的,你浪费的只有自己的时间而已。
  1. eventsMixin(src/core/instance/events.js)

Vue.prototype.$on = function() {} Vue.prototype.$once = function() {} Vue.prototype.$off = function() {} Vue.prototype.$emit = function() {}
  1. lifecycleMixin(src/core/instance/lifecycle.js)

Vue.prototype._update = function() {} Vue.prototype.$forceUpdate = function () {} Vue.prototype.$destroy = function () {}
  1. renderMixin(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() {} Vue.prototype._render = function() {}

将上面5个方法执行完成后,instance中对Vue的原型一波疯狂输出后,Vue的原型已经变成了:

【大型干货】手拉手带你过一遍 vue 部分源码

如果你认为到此就结束了?答案当然是,不。让我们顺着第一章整理的图,继续回到core/index.js中。

Core(src/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' // 初始化全局API 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

按照代码执行顺序,我们看看initGlobalAPI(Vue)方法内容:


// Object.defineProperty(Vue, 'config', configDef) Vue.config = { devtools: true, …} Vue.util = { warn, extend, mergeOptions, defineReactive, } Vue.set = set Vue.delete = delete Vue.nextTick = nextTick Vue.options = { components: {}, directives: {}, filters: {}, _base: Vue, } // extend(Vue.options.components, builtInComponents) Vue.options.components.KeepAlive = { name: 'keep-alive' …} // initUse Vue.use = function() {} // initMixin Vue.mixin = function() {} // initExtend Vue.cid = 0 Vue.extend = function() {} // initAssetRegisters //> JS中文网 - 前端进阶资源教程 [www.javascriptC.com](https://javascriptc.com) // > 一个致力于帮助开发者用代码改变世界为使命的平台,每天都可以在这里找到技术世界的头条内容 Vue.component = function() {} Vue.directive = function() {} Vue.filter = function() {}

不难看出,整个Core在instance的基础上,又对Vue的属性进行了一波输出。经历完Core后,整个Vue变成了这样:

【大型干货】手拉手带你过一遍 vue 部分源码

继续顺着第一章整理的路线,来看看runtime又对Vue做了什么。

runtime(src/platforms/web/runtime/index.js)

这里还是记得先从宏观入手,不要去看每个方法的详细内容。可以通过debugger来暂停代码执行,然后通过控制台的console.dir(Vue)随时观察Vue的变化,

  1. 这里首先针对web平台,对Vue.config来了一小波方法添加。

Vue.config.mustUseProp = mustUseProp Vue.config.isReservedTag = isReservedTag Vue.config.isReservedAttr = isReservedAttr Vue.config.getTagNamespace = getTagNamespace Vue.config.isUnknownElement = isUnknownElement
  1. 向options中directives增加了model以及show指令:

// extend(Vue.options.directives, platformDirectives) Vue.options.directives = { model: { componentUpdated: ƒ …} show: { bind: ƒ, update: ƒ, unbind: ƒ } }
  1. 向options中components增加了Transition以及TransitionGroup

// extend(Vue.options.components, platformComponents) Vue.options.components = { KeepAlive: { name: "keep-alive" …} Transition: {name: "transition", props: {…} …} TransitionGroup: {props: {…}, beforeMount: ƒ, …} }
  1. 在原型中追加__patch__以及$mount:

// 虚拟dom所用到的方法 Vue.prototype.__patch__ = patch Vue.prototype.$mount = function() {}
  1. 以及对devtools的支持。

entry(src/platforms/web/entry-runtime-with-compiler.js)

  1. 在entry中,覆盖了$mount方法。

  2. 挂载compile,compileToFunctions方法是将template编译为render函数


Vue.compile = compileToFunctions

小结

至此,我们完整的过了一遍在web中Vue的构造函数的变化过程:

  • 通过instance对Vue.prototype进行属性和方法的挂载。
  • 通过core对Vue进行静态属性和方法的挂载。
  • 通过runtime添加了对platform === ‘web’的情况下,特有的配置、组件、指令。
  • 通过entry来为$mount方法增加编译template的能力。

四、做什么的

上一章我们从宏观角度观察了整个Vue构造函数的变化过程,那么我们本章将从微观角度,看看new Vue()后,都做了什么。

将我们demo中的app.js修改为如下代码:


var demo = new Vue({ el: '#demo', data() { return { text: 'hello world!' } } })

还记得instance/init中的Vue构造函数吗?在代码执行了this._init(options),那我们就从_init入手,开始本章的旅途。

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    // 浏览器环境&支持window.performance&非生产环境&配置了performance
    if (process.env.NODE_ENV !== 'production'
        && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      // 相当于 window.performance.mark(startTag)
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // 将options进行合并
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    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. 在当前实例中,添加_uid,_isVue属性。
  2. 当非生产环境时,用window.performance标记vue初始化的开始。
  3. 由于我们的demo中,没有手动处理_isComponent,所以这里会进入到else分支,将Vue.options与传入options进行合并。
  4. 为当前实例添加_renderProxy_self属性。
  5. 初始化生命周期,initLifecycle
  6. 初始化事件,initEvents
  7. 初始化render,initRender
  8. 调用生命周期中的beforeCreate
  9. 初始化注入值 initInjections
  10. 初始化状态 initState
  11. 初始化Provide initProvide
  12. 调用生命周期中的 created
  13. 非生产环境下,标识初始化结束,为当前实例增加_name属性
  14. 根据options传入的el,调用当前实例的$mount

OK,我们又宏观的看了整个_init方法,接下来我们结合我们的demo,来细细的看下每一步产生的影响,以及具体调用的方法。

mergeOptions(src/core/util/options.js)


vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) function resolveConstructorOptions (Ctor: Class<Component>) { let options = Ctor.options if (Ctor.super) { const superOptions = resolveConstructorOptions(Ctor.super) const cachedSuperOptions = Ctor.superOptions if (superOptions !== cachedSuperOptions) { // super option changed, // need to resolve new options. Ctor.superOptions = superOptions // check if there are any late-modified/attached options (#4976) const modifiedOptions = resolveModifiedOptions(Ctor) // update base extend options if (modifiedOptions) { extend(Ctor.extendOptions, modifiedOptions) } options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions) if (options.name) { options.components[options.name] = Ctor } } } return options }

还记得我们在第三章中,runtime对Vue的变更之后,options变成了什么样吗?如果你忘了,这里我们再回忆一下:


Vue.options = { components: { KeepAlive: { name: "keep-alive" …} Transition: {name: "transition", props: {…} …} TransitionGroup: {props: {…}, beforeMount: ƒ, …} }, directives: { model: { componentUpdated: ƒ …} show: { bind: ƒ, update: ƒ, unbind: ƒ } }, filters: {}, _base: ƒ Vue }

我们将上面的代码进行拆解,首先将this.constructor传入resolveConstructorOptions中,因为我们的demo中没有进行继承操作,所以在resolveConstructorOptions方法中,没有进入if,直接返回得到的结果,就是在runtime中进行处理后的options选项。而options就是我们在调用new Vue({})时,传入的options。此时,mergeOptions方法变为:


vm.$options = mergeOptions( { components: { KeepAlive: { name: "keep-alive" …} Transition: {name: "transition", props: {…} …} TransitionGroup: {props: {…}, beforeMount: ƒ, …} }, directives: { model: { componentUpdated: ƒ …} show: { bind: ƒ, update: ƒ, unbind: ƒ } }, filters: {}, _base: ƒ Vue }, { el: '#demo', data: ƒ data() }, vm )

JS中文网 – 前端进阶资源教程 www.javascriptC.com
一个致力于帮助开发者用代码改变世界为使命的平台,每天都可以在这里找到技术世界的头条内容

接下来开始调用mergeOptions方法。打开文件后,我们发现在引用该文件时,会立即执行一段代码:


// config.optionMergeStrategies = Object.create(null) const strats = config.optionMergeStrategies

仔细往下看后面,还有一系列针对strats挂载方法和属性的操作,最终strats会变为:

【大型干货】手拉手带你过一遍 vue 部分源码

其实这些散落在代码中的挂载操作,有点没想明白尤大没有放到一个方法里去统一处理一波?

继续往下翻,看到了我们进入这个文件的目标,那就是mergeOptions方法:


function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object { debugger; if (process.env.NODE_ENV !== 'production') { // 根据用户传入的options,检查合法性 checkComponents(child) } if (typeof child === 'function') { child = child.options } // 标准化传入options中的props normalizeProps(child, vm) // 标准化注入 normalizeInject(child, vm) // 标准化指令 normalizeDirectives(child) 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 }

因为我们这里使用了最简单的hello world,所以在mergeOptions中,可以直接从30行开始看,这里初始化了变量options,32行、35行的for循环分别根据合并策略进行了合并。看到这里,恍然大悟,原来strats是定义一些标准合并策略,如果没有定义在其中,就使用默认合并策略defaultStrat

这里有个小细节,就是在循环子options时,仅合并父options中不存在的项,来提高合并效率。

让我们继续来用最直白的方式,回顾下上面的过程:


// 初始化合并策略 const strats = config.optionMergeStrategies strats.el = strats.propsData = function (parent, child, vm, key) {} strats.data = function (parentVal, childVal, vm) {} constants.LIFECYCLE_HOOKS.forEach(hook => strats[hook] = mergeHook) constants.ASSET_TYPES.forEach(type => strats[type + 's'] = mergeAssets) strats.watch = function(parentVal, childVal, vm, key) {} strats.props = strats.methods = strats.inject = strats.computed = function(parentVal, childVal, vm, key) {} strats.provide = mergeDataOrFn // 默认合并策略 const defaultStrat = function (parentVal, childVal) { return childVal === undefined ? parentVal : childVal } function mergeOptions (parent, child, vm) { // 本次demo没有用到省略前面代码 ... 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 }

怎么样,是不是清晰多了?本次的demo经过mergeOptions之后,变为了如下:

【大型干货】手拉手带你过一遍 vue 部分源码

OK,因为我们本次是来看_init的,所以到这里,你需要清除Vue通过合并策略,将parent与child进行了合并即可。接下来,我们继续回到_initoptions合并处理完之后做了什么?

initProxy(src/core/instance/proxy.js)

在merge完options后,会判断如果是非生产环境时,会进入initProxy方法。


if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } vm._self = vm

带着雾水,进入到方法定义的文件,看到了Proxy这个关键字,如果这里你还不清楚,可以看下阮老师的ES6,上面有讲。

  • 这里在非生产环境时,对config.keyCodes的一些关键字做了禁止赋值操作。
  • 返回了vm._renderProxy = new Proxy(vm, handlers),这里的handlers,由于我们的options中没有render,所以这里取值是hasHandler。

这部分具体是做什么用的,暂且知道有这么个东西,主线还是不要放弃,继续回到主线吧。

initLifecycle(src/core/instance/lifecycle.js)

初始化了与生命周期相关的属性。


function initLifecycle (vm) { const options = vm.$options // 省去部分与本次demo无关代码 ... vm.$parent = undefined vm.$root = vm vm.$children = [] vm.$refs = {} vm._watcher = null vm._inactive = null vm._directInactive = false vm._isMounted = false vm._isDestroyed = false vm._isBeingDestroyed = false }

initEvents(src/core/instance/events.js)


function initEvents (vm) { vm._events = Object.create(null) vm._hasHookEvent = false // 省去部分与本次demo无关代码 ... }

initRender(src/core/instance/render.js)


function initRender (vm: Component) { vm._vnode = null // the root of the child tree vm._staticTrees = null // v-once cached trees vm.$slots = {} vm.$scopedSlots = {} vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) vm.$createElement= (a, b, c, d) => createElement(vm, a, b, c, d, true) vm.$attrs = {} vm.$listeners = {} }

callHook(vm, ‘beforeCreate)

调用生命周期函数beforeCreate

initInjections(src/core/instance/inject.js)

由于本demo没有用到注入值,对本次vm并无实际影响,所以这一步暂且忽略,有兴趣可以自行翻阅。

initState(src/core/instance/state.js)

本次的只针对这最简单的demo,分析initState,可能忽略了很多过程,后续我们会针对更复杂的demo来继续分析一波。

【大型干货】手拉手带你过一遍 vue 部分源码

这里你可以先留意到几个关键词ObserverDepWatcher。每个Observer都有一个独立的Dep。关于Watcher,暂时没用到,但是请相信,马上就可以看到了。

initProvide(src/core/instance/inject.js)

由于本demo没有用到,对本次vm并无实际影响,所以这一步暂且忽略,有兴趣可以自行翻阅。

callHook(vm, ‘created’)

这里知道为什么在created时候,没法操作DOM了吗?因为在这里,还没有涉及到实际的DOM渲染。

vm.$mount(vm.$options.el)

这里前面有个if判断,所以当你如果没有在new Vue中的options没有传入el的话,就不会触发实际的渲染,就需要自己手动调用了$mount

这里的$mount最终会调向哪里?还记得我们在第三章看到的compiler所做的事情吗?就是覆盖Vue.prototype.$mount,接下来,我们一起进入$mount函数看看它都做了什么吧。


// 只保留与本次相关代码,其余看太多会影响视线 const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) const options = this.$options if (!options.render) { let template = getOuterHTML(el) if (template) { const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns } } return mount.call(this, el, hydrating) }

这里在覆盖$mount之前,先将原有的$mount保留至变量mount中,整个覆盖后的方法是将template转为render函数挂载至vmoptions,然后调用调用原有的mount。所以还记得mount来自于哪嘛?那就继续吧runtime/index,方法很简单,调用了生命周期中mountComponent


// 依然只保留和本demo相关的内容 function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el callHook(vm, 'beforeMount') let updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }

OK,精彩的部分来了,一个Watcher,盘活了整个我们前面铺垫的一系列东西。打开src/core/observer/watcher.js,让我们看看Watcher的构造函数吧。为了清楚的看到Watcher的流程。依旧只保留方法我们需要关注的东西:


constructor (vm, expOrFn, cb, options, isRenderWatcher) { this.vm = vm vm._watcher = this vm._watchers.push(this) this.getter = expOrFn this.value = this.get() } get () { pushTarget(this) let value const vm = this.vm value = this.getter.call(vm, vm) popTarget() this.cleanupDeps() return value }
  1. Watcher的构造函数中,本次传入的updateComponent作为Wathergetter
  2. get方法调用时,又通过pushTarget方法,将当前Watcher赋值给Dep.target
  3. 调用getter,相当于调用vm._update,先调用vm._render,而这时vm._render,此时会将已经准备好的render函数进调用。
  4. render函数中又用到了this.text,所以又会调用textget方法,从而触发了dep.depend()
  5. dep.depend()会调回WatcheraddDep,这时Watcher记录了当前dep实例。
  6. 继续调用dep.addSub(this)dep又记录了当前Watcher实例,将当前的Watcher存入dep.subs中。
  7. 这里顺带提一下本次demo还没有使用的,也就是当this.text发生改变时,会触发Observer中的set方法,从而触发dep.notify()方法来进行update操作。

最后这段文字太干了,可以自己通过断点,耐心的走一遍整个过程。如果没有耐心看完这段描述,可以看看笔者这篇文章100行代码带你玩vue响应式

就这样,Vue的数据响应系统,通过ObserverWatcherDep完美的串在了一起。也希望经历这个过程后,你能对真正的对这张图,有一定的理解。

【大型干货】手拉手带你过一遍 vue 部分源码

当然,$mount中还有一步被我轻描淡写了,那就是这部分,将template转换为render,render实际调用时,会经历_render, $createElement, __patch__, 方法,有兴趣可以自己浏览下’src/core/vdom/’目录下的文件,来了解vue针对虚拟dom的使用。

最后

如果你喜欢,可以继续浏览笔者关于vue template转换部分的文章《Vue对template做了什么》

作者:
链接:https://juejin.im/post/5adff30f518825672d33d596

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:【大型干货】手拉手带你过一遍 vue 部分源码

链接:https://www.javascriptc.com/2856.html

« 彻底搞懂 word-break、word-wrap、white-space
腾讯一面总结-web 前端之结果还是凉凉了»
Flutter 中文教程资源

相关推荐

QR code