Vue2.1.7 源码学习 (一)

原本文章的名字叫做《源码解析》,不过后来想想,还是用“源码学习”来的合适一点,在没有彻底掌握源码中的每一个字母之前,“解析”就有点标题党了。建议在看这篇文章之前,最好打开2.1.7的源码对照着看,这样可能更容易理解。另外本人水平有限,文中有错误或不妥的地方望大家多多指正共同成长。

补充:Vue 2.2 刚刚发布,作为一个系列文章的第一篇,本篇文章主要从Vue代码的组织,Vue构造函数的还原,原型的设计,以及参数选项的处理和已经被写烂了的数据绑定与如何使用 Virtual DOM 更新视图入手。从整体的大方向观察框架,这么看来 V2.1.7 对于理解 V2.2 的代码不会有太大的影响。该系列文章的后续文章,都会从最新的源码入手,并对改动的地方做相应的提示。

很久之前写过一篇文章:JavaScript实现MVVM之我就是想监测一个普通对象的变化,文章开头提到了我写博客的风格,还是那句话,只写努力让小白,甚至是小学生都能看明白的文章。这不免会导致对于某些同学来说这篇文章有些墨迹,所以大家根据自己的喜好,可以详细的看,也可以跳跃着看。

一、从了解一个开源项目入手

要看一个项目的源码,不要一上来就看,先去了解一下项目本身的元数据和依赖,除此之外最好也了解一下 PR 规则,Issue Reporting 规则等等。特别是“前端”开源项目,我们在看源码之前第一个想到的应该是:package.json文件。

在 package.json 文件中,我们最应该关注的就是 scripts 字段和 devDependencies 以及 dependencies 字段,通过 scripts 字段我们可以知道项目中定义的脚本命令,通过 devDependencies 和 dependencies 字段我们可以了解项目的依赖情况。

了解了这些之后,如果有依赖我们就 npm install 安装依赖就ok了。

除了 package.json 之外,我们还要阅读项目的贡献规则文档,了解如何开始,一个好的开源项目肯定会包含这部分内容的,Vue也不例外:https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md,在这个文档里说明了一些行为准则,PR指南,Issue Reporting 指南,Development Setup 以及 项目结构。通过阅读这些内容,我们可以了解项目如何开始,如何开发以及目录的说明,下面是对重要目录和文件的简单介绍,这些内容你都可以去自己阅读获取:

├── build --------------------------------- 构建相关的文件,一般情况下我们不需要动
        ├── dist ---------------------------------- 构建后文件的输出目录
        ├── examples ------------------------------ 存放一些使用Vue开发的应用案例
        ├── flow ---------------------------------- 类型声明,使用开源项目 [Flow](https://flowtype.org/)
        ├── package.json -------------------------- 不解释
        ├── test ---------------------------------- 包含所有测试文件
        ├── src ----------------------------------- 这个是我们最应该关注的目录,包含了源码
        │   ├── entries --------------------------- 包含了不同的构建或包的入口文件
        │   │   ├── web-runtime.js ---------------- 运行时构建的入口,输出 dist/vue.common.js 文件,不包含模板(template)到render函数的编译器,所以不支持 `template` 选项,我们使用vue默认导出的就是这个运行时的版本。大家使用的时候要注意
        │   │   ├── web-runtime-with-compiler.js -- 独立构建版本的入口,输出 dist/vue.js,它包含模板(template)到render函数的编译器
        │   │   ├── web-compiler.js --------------- vue-template-compiler 包的入口文件
        │   │   ├── web-server-renderer.js -------- vue-server-renderer 包的入口文件
        │   ├── compiler -------------------------- 编译器代码的存放目录,将 template 编译为 render 函数
        │   │   ├── parser ------------------------ 存放将模板字符串转换成元素抽象语法树的代码
        │   │   ├── codegen ----------------------- 存放从抽象语法树(AST)生成render函数的代码
        │   │   ├── optimizer.js ------------------ 分析静态树,优化vdom渲染
        │   ├── core ------------------------------ 存放通用的,平台无关的代码
        │   │   ├── observer ---------------------- 反应系统,包含数据观测的核心代码
        │   │   ├── vdom -------------------------- 包含虚拟DOM创建(creation)和打补丁(patching)的代码
        │   │   ├── instance ---------------------- 包含Vue构造函数设计相关的代码
        │   │   ├── global-api -------------------- 包含给Vue构造函数挂载全局方法(静态方法)或属性的代码
        │   │   ├── components -------------------- 包含抽象出来的通用组件
        │   ├── server ---------------------------- 包含服务端渲染(server-side rendering)的相关代码
        │   ├── platforms ------------------------- 包含平台特有的相关代码
        │   ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
        │   ├── shared ---------------------------- 包含整个代码库通用的代码
        

大概了解了重要目录和文件之后,我们就可以查看 Development Setup 中的常用命令部分,来了解如何开始这个项目了,我们可以看到这样的介绍:

# watch and auto re-build dist/vue.js
    $ npm run dev
    # watch and auto re-run unit tests in Chrome
    $ npm run dev:test
    

现在,我们只需要运行 npm run dev 即可监测文件变化并自动重新构建输出 dist/vue.js,然后运行 npm run dev:test 来测试。不过为了方便,我会在 examples 目录新建一个例子,然后引用 dist/vue.js 这样,我们可以直接拿这个例子一边改Vue源码一边看自己写的代码想怎么玩怎么玩。

二、看源码的小提示

在真正步入源码世界之前,我想简单说一说看源码的技巧:

注重大体框架,从宏观到微观

当你看一个项目代码的时候,最好是能找到一条主线,先把大体流程结构摸清楚,再深入到细节,逐项击破,拿Vue举个栗子:假如你已经知道Vue中数据状态改变后会采用virtual DOM的方式更新DOM,这个时候,如果你不了解virtual DOM,那么听我一句“暂且不要去研究内部具体实现,因为这会是你丧失主线”,而你仅仅需要知道virtual DOM分为三个步骤:

一、createElement(): 用 JavaScript对象(虚拟树) 描述 真实DOM对象(真实树) 二、diff(oldNode, newNode) : 对比新旧两个虚拟树的区别,收集差异 三、patch() : 将差异应用到真实DOM树

有的时候 第二步 可能与 第三步 合并成一步(Vue 中的patch就是这样),除此之外,还比如 src/compiler/codegen 内的代码,可能你不知道他写了什么,直接去看它会让你很痛苦,但是你只需要知道 codegen 是用来将抽象语法树(AST)生成render函数的就OK了,也就是生成类似下面这样的代码:

function anonymous() {
       with(this){return _c('p',{attrs:{"id":"app"}},[_v("\n      "+_s(a)+"\n      "),_c('my-com')])}
       }
    

当我们知道了一个东西存在,且知道它存在的目的,那么我们就很容易抓住这条主线,这个系列的第一篇文章就是围绕大体主线展开的。了解大体之后,我们就知道了每部分内容都是做什么的,比如 codegen 是生成类似上面贴出的代码所示的函数的,那么再去看codegen下的代码时,目的性就会更强,就更容易理解。

三、Vue 的构造函数是什么样的

balabala一大堆,开始来干货吧。我们要做的第一件事就是搞清楚 Vue 构造函数到底是什么样子的。

我们知道,我们要使用 new 操作符来调用 Vue,那么也就是说 Vue 应该是一个构造函数,所以我们第一件要做的事儿就是把构造函数先扒的一清二楚,如何寻找 Vue 构造函数呢?当然是从 entry 开始啦,还记的我们运行 npm run dev 命令后,会输出 dist/vue.js吗,那么我们就去看看 npm run dev 干了什么:

"dev": "TARGET=web-full-dev rollup -w -c build/config.js",
    

首先将 TARGET 得值设置为 ‘web-full-dev’,然后,然后,然后如果你不了解 rollup 就应该简单去看一下啦……,简单的说就是一个JavaScript模块打包器,你可以把它简单的理解为和 webpack 一样,只不过它有他的优势,比如 Tree-shaking (webpack2也有),但同样,在某些场景它也有他的劣势。。。废话不多说,其中 -w 就是watch,-c 就是指定配置文件为 build/config.js ,我们打开这个配置文件看一看:

// 引入依赖,定义 banner
    ...
    // builds 对象
    const builds = {
        ...
     // Runtime+compiler development build (Browser)
     'web-full-dev': {
           entry: path.resolve(__dirname, '../src/entries/web-runtime-with-compiler.js'),
          dest: path.resolve(__dirname, '../dist/vue.js'),
            format: 'umd',
          env: 'development',
         alias: { he: './entity-decoder' },
          banner
      },
      ...
      }
    // 生成配置的方法
    function genConfig(opts){
     ...
     }
    if (process.env.TARGET) {
      module.exports = genConfig(builds[process.env.TARGET])
      } else {
      exports.getBuild = name => genConfig(builds[name])
      exports.getAllBuilds = () => Object.keys(builds).map(name => genConfig(builds[name]))
      }
    

上面的代码是简化过的,当我们运行 npm run dev 的时候 process.env.TARGET 的值等于 ‘web-full-dev’,所以

module.exports = genConfig(builds[process.env.TARGET])
    

这句代码相当于:

module.exports = genConfig({
        entry: path.resolve(__dirname, '../src/entries/web-runtime-with-compiler.js'),
        dest: path.resolve(__dirname, '../dist/vue.js'),
        format: 'umd',
        env: 'development',
        alias: { he: './entity-decoder' },
        banner
        })
    

最终,genConfig 函数返回一个 config 对象,这个config对象就是Rollup的配置对象。那么我们就不难看到,入口文件是:

src/entries/web-runtime-with-compiler.js
    

我们打开这个文件,不要忘了我们的主题,我们在寻找Vue构造函数,所以当我们看到这个文件的第一行代码是:

import Vue from './web-runtime'
    

这个时候,你就应该知道,这个文件暂时与你无缘,你应该打开 web-runtime.js 文件,不过当你打开这个文件时,你发现第一行是这样的:

import Vue from 'core/index'
    

依照此思路,最终我们寻找到Vue构造函数的位置应该是在 src/core/instance/index.js 文件中,其实我们猜也猜得到,上面介绍目录的时候说过:instance 是存放Vue构造函数设计相关代码的目录。总结一下,我们寻找的过程是这样的:

寻找 Vue 构造函数路线

我们回头看一看 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
    

引入依赖,定义 Vue 构造函数,然后以Vue构造函数为参数,调用了五个方法,最后导出 Vue。这五个方法分别来自五个文件:init.js state.js render.js events.js 以及 lifecycle.js

打开这五个文件,找到相应的方法,你会发现,这些方法的作用,就是在 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.$set = set
    Vue.prototype.$delete = del
    Vue.prototype.$watch = function(){}
    // renderMixin(Vue)   src/core/instance/render.js **************************************************
    Vue.prototype.$nextTick = function (fn: Function) {}
    Vue.prototype._render = function (): VNode {}
    Vue.prototype._s = _toString
    Vue.prototype._v = createTextVNode
    Vue.prototype._n = toNumber
    Vue.prototype._e = createEmptyVNode
    Vue.prototype._q = looseEqual
    Vue.prototype._i = looseIndexOf
    Vue.prototype._m = function(){}
    Vue.prototype._o = function(){}
    Vue.prototype._f = function resolveFilter (id) {}
    Vue.prototype._l = function(){}
    Vue.prototype._t = function(){}
    Vue.prototype._b = function(){}
    Vue.prototype._k = function(){}
    // eventsMixin(Vue)   src/core/instance/events.js **************************************************
    Vue.prototype.$on = function (event: string, fn: Function): Component {}
    Vue.prototype.$once = function (event: string, fn: Function): Component {}
    Vue.prototype.$off = function (event?: string, fn?: Function): Component {}
    Vue.prototype.$emit = function (event: string): Component {}
    // lifecycleMixin(Vue)   src/core/instance/lifecycle.js **************************************************
    Vue.prototype._mount = function(){}
    Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
    Vue.prototype._updateFromParent = function(){}
    Vue.prototype.$forceUpdate = function () {}
    Vue.prototype.$destroy = function () {}
    

这样就结束了吗?并没有,根据我们之前寻找 Vue 的路线,这只是刚刚开始,我们追溯路线往回走,那么下一个处理 Vue 构造函数的应该是 src/core/index.js 文件,我们打开它:

import Vue from './instance/index'
    import { initGlobalAPI } from './global-api/index'
    import { isServerRendering } from 'core/util/env'
    initGlobalAPI(Vue)
    Object.defineProperty(Vue.prototype, '$isServer', {
      get: isServerRendering
      })
    Vue.version = '__VERSION__'
    export default Vue
    

这个文件也很简单,从 instance/index 中导入已经在原型上挂载了方法和属性后的 Vue,然后导入 initGlobalAPI 和 isServerRendering,之后将Vue作为参数传给 initGlobalAPI ,最后又在 Vue.prototype 上挂载了 $isServer ,在 Vue 上挂载了 version 属性。

initGlobalAPI 的作用是在 Vue 构造函数上挂载静态属性和方法,Vue 在经过 initGlobalAPI 之后,会变成这样:

// src/core/index.js / src/core/global-api/index.js
    Vue.config
    Vue.util = util
    Vue.set = set
    Vue.delete = del
    Vue.nextTick = util.nextTick
    Vue.options = {
        components: {
            KeepAlive
        },
        directives: {},
        filters: {},
        _base: Vue
        }
    Vue.use
    Vue.mixin
    Vue.cid = 0
    Vue.extend
    Vue.component = function(){}
    Vue.directive = function(){}
    Vue.filter = function(){}
    Vue.prototype.$isServer
    Vue.version = '__VERSION__'
    

其中,稍微复杂一点的就是 Vue.options,大家稍微分析分析就会知道他的确长成那个样子。下一个就是 web-runtime.js 文件了,web-runtime.js 文件主要做了三件事儿:

1、覆盖 Vue.config 的属性,将其设置为平台特有的一些方法 2、Vue.options.directives 和 Vue.options.components 安装平台特有的指令和组件 3、在 Vue.prototype 上定义 __patch__ 和 $mount

经过 web-runtime.js 文件之后,Vue 变成下面这个样子:

// 安装平台特定的utils
    Vue.config.isUnknownElement = isUnknownElement
    Vue.config.isReservedTag = isReservedTag
    Vue.config.getTagNamespace = getTagNamespace
    Vue.config.mustUseProp = mustUseProp
    // 安装平台特定的 指令 和 组件
    Vue.options = {
        components: {
            KeepAlive,
            Transition,
            TransitionGroup
        },
        directives: {
            model,
            show
        },
        filters: {},
        _base: Vue
        }
    Vue.prototype.__patch__
    Vue.prototype.$mount
    

这里大家要注意的是 Vue.options 的变化。另外这里的 $mount 方法很简单:

Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
      ): Component {
      el = el && inBrowser ? query(el) : undefined
      return this._mount(el, hydrating)
      }
    

首先根据是否是浏览器环境决定要不要 query(el) 获取元素,然后将 el 作为参数传递给 this._mount()

最后一个处理 Vue 的文件就是入口文件 web-runtime-with-compiler.js 了,该文件做了两件事:

1、缓存来自 web-runtime.js 文件的 $mount 函数

const mount = Vue.prototype.$mount
    

然后覆盖覆盖了 Vue.prototype.$mount

2、在 Vue 上挂载 compile

Vue.compile = compileToFunctions
    

compileToFunctions 函数的作用,就是将模板 template 编译为render函数。

至此,我们算是还原了 Vue 构造函数,总结一下:

1、Vue.prototype 下的属性和方法的挂载主要是在 src/core/instance 目录中的代码处理的

2、Vue 下的静态属性和方法的挂载主要是在 src/core/global-api 目录下的代码处理的

3、web-runtime.js 主要是添加web平台特有的配置、组件和指令,web-runtime-with-compiler.js 给Vue的 $mount 方法添加 compiler 编译器,支持 template

四、一个贯穿始终的例子

在了解了 Vue 构造函数的设计之后,接下来,我们一个贯穿始终的例子就要登场了,掌声有请:

let v = new Vue({
        el: '#app',
     data: {
         a: 1,
           b: [1, 2, 3]
        }
        })
    

好吧,我承认这段代码你家没满月的孩子都会写了。这段代码就是我们贯穿始终的例子,它就是这篇文章的主线,在后续的讲解中,都会以这段代码为例,当讲到必要的地方,会为其添加选项,比如讲计算属性的时候当然要加上一个 computed 属性了。不过在最开始,我只传递了两个选项 el 以及 data,“我们看看接下来会发生什么,让我们拭目以待“ —- NBA球星在接受采访时最喜欢说这句话。

当我们按照例子那样编码使用Vue的时候,Vue都做了什么?

想要知道Vue都干了什么,我们就要找到 Vue 初始化程序,查看 Vue 构造函数:

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

我们发现,_init() 方法就是Vue调用的第一个方法,然后将我们的参数 options 透传了过去。在调用 _init() 之前,还做了一个安全模式的处理,告诉开发者必须使用 new 操作符调用 Vue。根据之前我们的整理,_init() 方法应该是在 src/core/instance/init.js 文件中定义的,我们打开这个文件查看 _init() 方法:

Vue.prototype._init = function (options?: Object) {
          const vm: Component = this
          // a uid
          vm._uid = uid++
          // 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 {
            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)
          callHook(vm, 'beforeCreate')
          initState(vm)
          callHook(vm, 'created')
          initRender(vm)
          }
        

_init() 方法在一开始的时候,在 this 对象上定义了两个属性:_uid 和 _isVue,然后判断有没有定义 options._isComponent,在使用 Vue 开发项目的时候,我们是不会使用 _isComponent 选项的,这个选项是 Vue 内部使用的,按照本节开头的例子,这里会走 else分支,也就是这段代码:

vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
      )
    

这样 Vue 第一步所做的事情就来了:使用策略对象合并参数选项

可以发现,Vue使用 mergeOptions 来处理我们调用Vue时传入的参数选项(options),然后将返回值赋值给 this.$options (vm === this),传给 mergeOptions 方法三个参数,我们分别来看一看,首先是:resolveConstructorOptions(vm.constructor),我们查看一下这个方法:

export function resolveConstructorOptions (Ctor: Class<Component>) {
      let options = Ctor.options
      if (Ctor.super) {
        const superOptions = Ctor.super.options
        const cachedSuperOptions = Ctor.superOptions
        const extendOptions = Ctor.extendOptions
        if (superOptions !== cachedSuperOptions) {
          // super option changed
          Ctor.superOptions = superOptions
          extendOptions.render = options.render
          extendOptions.staticRenderFns = options.staticRenderFns
          extendOptions._scopeId = options._scopeId
          options = Ctor.options = mergeOptions(superOptions, extendOptions)
          if (options.name) {
            options.components[options.name] = Ctor
          }
        }
      }
      return options
      }
    

这个方法接收一个参数 Ctor,通过传入的 vm.constructor 我们可以知道,其实就是 Vue构造函数本身。所以下面这句代码:

let options = Ctor.options
    

相当于:

let options = Vue.options
    

大家还记得 Vue.options 吗?在寻找Vue构造函数一节里,我们整理了 Vue.options 应该长成下面这个样子:

Vue.options = {
        components: {
            KeepAlive,
            Transition,
            TransitionGroup
        },
        directives: {
            model,
            show
        },
        filters: {},
        _base: Vue
        }
    

之后判断是否定义了 Vue.super ,这个是用来处理继承的,我们后续再讲,在本例中,resolveConstructorOptions 方法直接返回了 Vue.options。也就是说,传递给 mergeOptions 方法的第一个参数就是 Vue.options

传给 mergeOptions 方法的第二个参数是我们调用Vue构造函数时的参数选项,第三个参数是 vm 也就是 this 对象,按照本节开头的例子那样使用 Vue,最终运行的代码应该如下:

 vm.$options = mergeOptions(
         // Vue.options
       {
        components: {
            KeepAlive,
            Transition,
            TransitionGroup
        },
        directives: {
            model,
            show
        },
        filters: {},
        _base: Vue
        },
    // 调用Vue构造函数时传入的参数选项 options
       {
          el: '#app',
     data: {
         a: 1,
           b: [1, 2, 3]
        }
       },
       // this
       vm
     )
     

了解了这些,我们就可以看看 mergeOptions 到底做了些什么了,根据引用寻找到 mergeOptions 应该是在 src/core/util/options.js 文件中定义的。这个文件第一次看可能会头大,下面是我处理后的简略展示,大家看上去应该更容易理解了:

// 1、引用依赖
    import Vue from '../instance/index'
    其他引用...
    // 2、合并父子选项值为最终值的策略对象,此时 strats 是一个空对象,因为 config.optionMergeStrategies = Object.create(null)
    const strats = config.optionMergeStrategies
    // 3、在 strats 对象上定义与参数选项名称相同的方法
    strats.el = 
    strats.propsData = function (parent, child, vm, key){}
    strats.data = function (parentVal, childVal, vm)
    config._lifecycleHooks.forEach(hook => {
      strats[hook] = mergeHook
      })
    config._assetTypes.forEach(function (type) {
      strats[type + 's'] = mergeAssets
      })
    strats.watch = function (parentVal, childVal)
    strats.props =
    strats.methods =
    strats.computed = function (parentVal: ?Object, childVal: ?Object)
    // 默认的合并策略,如果有 `childVal` 则返回 `childVal` 没有则返回 `parentVal`
    const defaultStrat = function (parentVal: any, childVal: any): any {
      return childVal === undefined
        ? parentVal
        : childVal
        }
    // 4、mergeOptions 中根据参数选项调用同名的策略方法进行合并处理
    export function mergeOptions (
      parent: Object,
      child: Object,
      vm?: Component
      ): Object {
      // 其他代码
      ...
      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
        
      }
    

上面的代码中,我省略了一些工具函数,例如 mergeHook 和 mergeAssets 等等,唯一需要注意的是这段代码:

config._lifecycleHooks.forEach(hook => {
      strats[hook] = mergeHook
      })
    config._assetTypes.forEach(function (type) {
      strats[type + 's'] = mergeAssets
      })
    

config 对象引用自 src/core/config.js 文件,最终的结果就是在 strats 下添加了相应的生命周期选项的合并策略函数为 mergeHook,添加指令(directives)、组件(components)、过滤器(filters)等选项的合并策略函数为 mergeAssets

这样看来就清晰多了,拿我们贯穿本文的例子来说:

let v = new Vue({
       el: '#app',
     data: {
         a: 1,
           b: [1, 2, 3]
        }
        })
    

其中 el 选项会使用 defaultStrat 默认策略函数处理,data 选项则会使用 strats.data策略函数处理,并且根据 strats.data 中的逻辑,strats.data 方法最终会返回一个函数:mergedInstanceDataFn

这里就不详细的讲解每一个策略函数的内容了,后续都会讲到,这里我们还是抓住主线理清思路为主,只需要知道Vue在处理选项的时候,使用了一个策略对象对父子选项进行合并。并将最终的值赋值给实例下的 $options 属性即:this.$options,那么我们继续查看 _init() 方法在合并完选项之后,又做了什么:

合并完选项之后,Vue 第二部做的事情就来了:初始化工作与Vue实例对象的设计

前面讲了 Vue 构造函数的设计,并且整理了 Vue原型属性与方法 和 Vue静态属性与方法,而 Vue 实例对象就是通过构造函数创造出来的,让我们来看一看 Vue 实例对象是如何设计的,下面的代码是 _init() 方法合并完选项之后的代码:

/* 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)
       callHook(vm, 'beforeCreate')
       initState(vm)
       callHook(vm, 'created')
       initRender(vm)
       

根据上面的代码,在生产环境下会为实例添加两个属性,并且属性值都为实例本身:

vm._renderProxy = vm
    vm._self = vm
    

然后,调用了四个 init* 方法分别为:initLifecycleinitEventsinitStateinitRender,且在 initState 前后分别回调了生命周期钩子 beforeCreate 和 created,而 initRender 是在 created 钩子执行之后执行的,看到这里,也就明白了为什么 created 的时候不能操作DOM了。因为这个时候还没有渲染真正的DOM元素到文档中。created 仅仅代表数据状态的初始化完成。

根据四个 init* 方法的引用关系打开对应的文件查看对应的方法,我们发现,这些方法是在处理Vue实例对象,以及做一些初始化的工作,类似整理Vue构造函数一样,我同样针对Vue实例做了属性和方法的整理,如下:

// 在 Vue.prototype._init 中添加的属性       **********************************************************
    this._uid = uid++
    this._isVue = true
    this.$options = {
        components,
        directives,
        filters,
        _base,
        el,
        data: mergedInstanceDataFn()
        }
    this._renderProxy = this
    this._self = this
    // 在 initLifecycle 中添加的属性        **********************************************************
    this.$parent = parent
    this.$root = parent ? parent.$root : this
     
    this.$children = []
    this.$refs = {}
    this._watcher = null
    this._inactive = false
    this._isMounted = false
    this._isDestroyed = false
    this._isBeingDestroyed = false
    // 在 initEvents   中添加的属性     **********************************************************
    this._events = {}
    this._updateListeners = function(){}
    // 在 initState 中添加的属性        **********************************************************
    this._watchers = []
        // initData
        this._data
        // 在 initRender    中添加的属性     **********************************************************
    this.$vnode = null // the placeholder node in parent tree
    this._vnode = null // the root of the child tree
    this._staticTrees = null
    this.$slots
    this.$scopedSlots
    this._c
    this.$createElement
    

以上就是一个Vue实例所包含的属性和方法,除此之外要注意的是,在 initEvents 中除了添加属性之外,如果有 vm.$options._parentListeners 还要调用 vm._updateListeners() 方法,在 initState 中又调用了一些其他init方法,如下:

export function initState (vm: Component) {
      vm._watchers = []
      initProps(vm)
      initMethods(vm)
      initData(vm)
      initComputed(vm)
      initWatch(vm)
      }
    

最后在 initRender 中如果有 vm.$options.el 还要调用 vm.$mount(vm.$options.el),如下:

if (vm.$options.el) {
      vm.$mount(vm.$options.el)
      }
    

这就是为什么如果不传递 el 选项就需要手动 mount 的原因了。

那么我们依照我们本节开头的的例子,以及初始化的先后顺序来逐一看一看都发生了什么。我们将 initState 中的 init* 方法展开来看,执行顺序应该是这样的(从上到下的顺序执行):

initLifecycle(vm)
    initEvents(vm)
    callHook(vm, 'beforeCreate')
    initProps(vm)
    initMethods(vm)
    initData(vm)
    initComputed(vm)
    initWatch(vm)
    callHook(vm, 'created')
    initRender(vm)
    

首先是 initLifecycle,这个函数的作用就是在实例上添加一些属性,然后是 initEvents,由于 vm.$options._parentListeners 的值为 undefined 所以也仅仅是在实例上添加属性, vm._updateListeners(listeners) 并不会执行,由于我们只传递了 el 和 data,所以initPropsinitMethodsinitComputedinitWatch 这四个方法什么都不会做,只有 initData 会执行。最后是 initRender,除了在实例上添加一些属性外,由于我们传递了 el 选项,所以会执行 vm.$mount(vm.$options.el)

综上所述:按照我们的例子那样写,初始化工作只包含两个主要内容即:initData 和 initRender

五、通过initData看Vue的数据响应系统

Vue的数据响应系统包含三个部分:ObserverDepWatcher。关于数据响应系统的内容真的已经被文章讲烂了,所以我就简单的说一下,力求大家能理解就ok,我们还是先看一下 initData 中的代码:

function initData (vm: Component) {
      let data = vm.$options.data
      data = vm._data = typeof data === 'function'
        ? data.call(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
      let i = keys.length
      while (i--) {
        if (props && hasOwn(props, keys[i])) {
          process.env.NODE_ENV !== 'production' && warn(
            `The data property "${keys[i]}" is already declared as a prop. ` +
            `Use prop default value instead.`,
            vm
          )
        } else {
          proxy(vm, keys[i])
        }
      }
      // observe data
      observe(data)
      data.__ob__ && data.__ob__.vmCount++
      }
    

首先,先拿到 data 数据:let data = vm.$options.data,大家还记得此时 vm.$options.data的值应该是通过 mergeOptions 合并处理后的 mergedInstanceDataFn 函数吗?所以在得到 data 后,它又判断了 data 的数据类型是不是 ‘function’,最终的结果是:data 还是我们传入的数据选项的 data,即:

data: {
        a: 1,
       b: [1, 2, 3]
       }
    

然后在实例对象上定义 _data 属性,该属性与 data 是相同的引用。

然后是一个 while 循环,循环的目的是在实例对象上对数据进行代理,这样我们就能通过 this.a 来访问 data.a 了,代码的处理是在 proxy 函数中,该函数非常简单,仅仅是在实例对象上设置与 data 属性同名的访问器属性,然后使用 _data 做数据劫持,如下:

function proxy (vm: Component, key: string) {
      if (!isReserved(key)) {
        Object.defineProperty(vm, key, {
          configurable: true,
          enumerable: true,
          get: function proxyGetter () {
            return vm._data[key]
          },
          set: function proxySetter (val) {
            vm._data[key] = val
          }
        })
      }
      }
    

做完数据的代理,就正式进入响应系统,

observe(data)
    

我们说过,数据响应系统主要包含三部分:ObserverDepWatcher,代码分别存放在:observer/index.jsobserver/dep.js 以及 observer/watcher.js 文件中,这回我们换一种方式,我们先不看其源码,大家先跟着我的思路来思考,最后回头再去看代码,你会有一种:”奥,不过如此“的感觉。

假如,我们有如下代码:

var data = {
        a: 1,
        b: {
            c: 2
        }
        }
    observer(data)
    new Watch('a', () => {
        alert(9)
        })
    new Watch('a', () => {
        alert(90)
        })
    new Watch('b.c', () => {
        alert(80)
        })
    

这段代码目的是,首先定义一个数据对象 data,然后通过 observer 对其进行观测,之后定义了三个观察者,当数据有变化时,执行相应的方法,这个功能使用Vue的实现原来要如何去实现?其实就是在问 observer 怎么写?Watch 构造函数又怎么写?接下来我们逐一实现。

首先,observer 的作用是:将数据对象data的属性转换为访问器属性:

class Observer {
        constructor (data) {
            this.walk(data)
        }
        walk (data) {
            // 遍历 data 对象属性,调用 defineReactive 方法
            let keys = Object.keys(data)
            for(let i = 0; i < keys.length; i++){
                defineReactive(data, keys[i], data[keys[i]])
            }
        }
        }
    // defineReactive方法仅仅将data的属性转换为访问器属性
    function defineReactive (data, key, val) {
      // 递归观测子属性
        observer(val)
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function () {
                return val
            },
            set: function (newVal) {
                if(val === newVal){
                    return
                }
                // 对新值进行观测
                observer(newVal)
            }
        })
        }
    // observer 方法首先判断data是不是纯JavaScript对象,如果是,调用 Observer 类进行观测
    function observer (data) {
        if(Object.prototype.toString.call(data) !== '[object Object]') {
            return
        }
        new Observer(data)
        }
    

上面的代码中,我们定义了 observer 方法,该方法检测了数据data是不是纯JavaScript对象,如果是就调用 Observer 类,并将 data 作为参数透传。在 Observer 类中,我们使用 walk 方法对数据data的属性循环调用 defineReactive 方法,defineReactive 方法很简单,仅仅是将数据data的属性转为访问器属性,并对数据进行递归观测,否则只能观测数据data的直属子属性。这样我们的第一步工作就完成了,当我们修改或者获取data属性值的时候,通过 get 和 set 即能获取到通知。

我们继续往下看,来看一下 Watch

new Watch('a', () => {
        alert(9)
        })
    

现在的问题是,Watch 要怎么和 observer 关联???????我们看看 Watch 它知道些什么,通过上面调用 Watch 的方式,传递给 Watch 两个参数,一个是 ‘a’ 我们可以称其为表达式,另外一个是回调函数。所以我们目前只能写出这样的代码:

class Watch {
        constructor (exp, fn) {
            this.exp = exp
            this.fn = fn
        }
        }
    

那么要怎么关联呢,大家看下面的代码会发生什么:

class Watch {
        constructor (exp, fn) {
            this.exp = exp
            this.fn = fn
            data[exp]
        }
        }
    

多了一句 data[exp],这句话是在干什么?是不是在获取 data 下某个属性的值,比如 exp 为 ‘a’ 的话,那么 data[exp] 就相当于在获取 data.a 的值,那这会放生什么?大家不要忘了,此时数据 data 下的属性已经是访问器属性了,所以这么做的结果会直接触发对应属性的 get 函数,这样我们就成功的和 observer 产生了关联,但这样还不够,我们还是没有达到目的,不过我们已经无限接近了,我们继续思考看一下可不可以这样:

既然在 Watch 中对表达式求值,能够触发 observer 的 get,那么可不可以在 get中收集 Watch 中函数呢?

答案是可以的,不过这个时候我们就需要 Dep 出场了,它是一个依赖收集器。我们的思路是:data 下的每一个属性都有一个唯一的 Dep 对象,在 get 中收集仅针对该属性的依赖,然后在 set 方法中触发所有收集的依赖,这样就搞定了,看如下代码:

class Dep {
        constructor () {
            this.subs = []
        }
        addSub () {
            this.subs.push(Dep.target)
        }
        notify () {
            for(let i = 0; i < this.subs.length; i++){
                this.subs[i].fn()
            }
        }
        }
    Dep.target = null
    function pushTarget(watch){
        Dep.target = watch
        }
    class Watch {
        constructor (exp, fn) {
            this.exp = exp
            this.fn = fn
            pushTarget(this)
            data[exp]
        }
        }
    

上面的代码中,我们在 Watch 中增加了 pushTarget(this),可以发现,这句代码的作用是将 Dep.target 的值设置为该Watch对象。在 pushTarget 之后我们才对表达式进行求值,接着,我们修改 defineReactive 代码如下

function defineReactive (data, key, val) {
        observer(val)
        let dep = new Dep()       // 新增
        Object.defineProperty(data, key, {
            enumerable: true,
            configurable: true,
            get: function () {
                dep.addSub()   // 新增
                return val
            },
            set: function (newVal) {
                if(val === newVal){
                    return
                }
                observer(newVal)
                dep.notify() // 新增
            }
        })
        }
    

如标注,新增了三句代码,我们知道,Watch 中对表达式求值会触发 get 方法,我们在 get 方法中调用了 dep.addSub,也就执行了这句代码:this.subs.push(Dep.target),由于在这句代码执行之前,Dep.target 的值已经被设置为一个 Watch 对象了,所以最终结果就是收集了一个 Watch 对象,然后在 set 方法中我们调用了 dep.notify,所以当data属性值变化的时候,就会通过 dep.notify 循环调用所有收集的Watch对象中的回调函数:

notify () {
            for(let i = 0; i < this.subs.length; i++){
                this.subs[i].fn()
            }
            }
        

这样 observerDepWatch 三者就联系成为一个有机的整体,实现了我们最初的目标,完整的代码可以戳这里:observer-dep-watch。这里还给大家挖了个坑,因为我们没有处理对数组的观测,由于比较复杂并且这又不是我们讨论的重点,如果大家想了解可以戳我的这篇文章: JavaScript实现MVVM之我就是想监测一个普通对象的变化,另外,在 Watch 中对表达式求值的时候也只做了直接子属性的求值,所以如果 exp 的值为 ‘a.b’ 的时候,就不可以用了,Vue的做法是使用 . 分割表达式字符串为数组,然后遍历一下对其进行求值,大家可以查看其源码。如下:

/**
         * Parse simple path.
         */
         const bailRE = /[^\w.$]/
        export function parsePath (path: string): any {
          if (bailRE.test(path)) {
            return
          } else {
            const segments = path.split('.')
            return function (obj) {
              for (let i = 0; i < segments.length; i++) {
                if (!obj) return
                obj = obj[segments[i]]
              }
              return obj
            }
          }
          }
        

Vue 的求值代码是在 src/core/util/lang.js 文件中 parsePath 函数中实现的。总结一下Vue的依赖收集过程应该是这样的:

Vue的依赖收集过程

实际上,Vue并没有直接在 get 中调用 addSub,而是调用的 dep.depend,目的是将当前的 dep 对象收集到 watch 对象中,如果要完整的流程,应该是这样的:(大家注意数据的每一个字段都拥有自己的 dep 对象和 get 方法。)

Vue完整的收集依赖的流程

这样 Vue 就建立了一套数据响应系统,之前我们说过,按照我们的例子那样写,初始化工作只包含两个主要内容即:initData 和 initRender。现在 initData 我们分析完了,接下来看一看 initRender

六、通过initRender看Vue的 render(渲染) 与 re-render(重新渲染)

在 initRender 方法中,因为我们的例子中传递了 el 选项,所以下面的代码会执行:

if (vm.$options.el) {
      vm.$mount(vm.$options.el)
      }
    

这里,调用了 $mount 方法,在还原Vue构造函数的时候,我们整理过所有的方法,其中 $mount 方法在两个地方出现过:

1、在 web-runtime.js 文件中:

Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
      ): Component {
      el = el && inBrowser ? query(el) : undefined
      return this._mount(el, hydrating)
      }
    

它的作用是通过 el 获取相应的DOM元素,然后调用 lifecycle.js 文件中的 _mount 方法。

2、在 web-runtime-with-compiler.js 文件中:

// 缓存了来自 web-runtime.js 的 $mount 方法
    const mount = Vue.prototype.$mount
    // 重写 $mount 方法
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
      ): Component {
      // 根据 el 获取相应的DOM元素
      el = el && query(el)
      // 不允许你将 el 挂载到 html 标签或者 body 标签
      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
      // 如果我们没有写 render 选项,那么就尝试将 template 或者 el 转化为 render 函数
      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) {
          const { render, staticRenderFns } = compileToFunctions(template, {
            warn,
            shouldDecodeNewlines,
            delimiters: options.delimiters
          }, this)
          options.render = render
          options.staticRenderFns = staticRenderFns
        }
      }
      // 调用已经缓存下来的 web-runtime.js 文件中的 $mount 方法
      return mount.call(this, el, hydrating)
      }
    

分析一下可知 web-runtime-with-compiler.js 的逻辑如下:

1、缓存来自 web-runtime.js 文件的 $mount 方法

2、判断有没有传递 render 选项,如果有直接调用来自 web-runtime.js 文件的 $mount 方法

3、如果没有传递 render 选项,那么查看有没有 template 选项,如果有就使用 compileToFunctions 函数根据其内容编译成 render 函数

4、如果没有 template 选项,那么查看有没有 el 选项,如果有就使用 compileToFunctions 函数将其内容(template = getOuterHTML(el))编译成 render 函数

5、将编译成的 render 函数挂载到 this.$options 属性下,并调用缓存下来的 web-runtime.js 文件中的 $mount 方法

简单的用一张图表示 mount 方法的调用关系,从上至下调用:

mount调用关系

不过不管怎样,我们发现这些步骤的最终目的是生成 render 函数,然后再调用 lifecycle.js 文件中的 _mount 方法,我们看看这个方法做了什么事情,查看 _mount 方法的代码,这是简化过得:

Vue.prototype._mount = function (
      el?: Element | void,
      hydrating?: boolean
      ): Component {
      const vm: Component = this
      // 在Vue实例对象上添加 $el 属性,指向挂载点元素
      vm.$el = el
      // 触发 beforeMount 生命周期钩子
      callHook(vm, 'beforeMount')
      vm._watcher = new Watcher(vm, () => {
        vm._update(vm._render(), hydrating)
      }, noop)
      // 如果是第一次mount则触发 mounted 生命周期钩子
      if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
      }
      return vm
      }
    

上面的代码很简单,该注释的都注释了,唯一需要看的就是这段代码:

vm._watcher = new Watcher(vm, () => {
      vm._update(vm._render(), hydrating)
      }, noop)
    

看上去很眼熟有没有?我们平时使用Vue都是这样使用 watch的:

this.$watch('a', (newVal, oldVal) => {
     
    })
    // 或者
    this.$watch(function(){
      return this.a + this.b
      }, (newVal, oldVal) => {
       
    })
    

第一个参数是 表达式或者函数,第二个参数是回调函数,第三个参数是可选的选项。原理是 Watch 内部对表达式求值或者对函数求值从而触发数据的 get 方法收集依赖。可是 _mount 方法中使用 Watcher 的时候第一个参数 vm 是什么鬼。我们不妨去看看源码中 $watch 函数是如何实现的,根据之前还原Vue构造函数中所整理的内容可知:$warch 方法是在 src/core/instance/state.js 文件中的 stateMixin 方法中定义的,源码如下:

Vue.prototype.$watch = function (
      expOrFn: string | Function,
      cb: Function,
      options?: Object
      ): Function {
      const vm: Component = this
      options = options || {}
      options.user = true
      const watcher = new Watcher(vm, expOrFn, cb, options)
      if (options.immediate) {
        cb.call(vm, watcher.value)
      }
      return function unwatchFn () {
        watcher.teardown()
      }
      }
    

我们可以发现,$warch 其实是对 Watcher 的一个封装,内部的 Watcher 的第一个参数实际上也是 vm 即:Vue实例对象,这一点我们可以在 Watcher 的源码中得到验证,代开 observer/watcher.js 文件查看:

export default class Watcher {
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: Object = {}
      ) {
        
      }
      }
    

可以发现真正的 Watcher 第一个参数实际上就是 vm。第二个参数是表达式或者函数,然后以此类推,所以现在再来看 _mount 中的这段代码:

vm._watcher = new Watcher(vm, () => {
      vm._update(vm._render(), hydrating)
      }, noop)
    

忽略第一个参数 vm,也就说,Watcher 内部应该对第二个参数求值,也就是运行这个函数:

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

所以 vm._render() 函数被第一个执行,该函数在 src/core/instance/render.js 中,该方法中的代码很多,下面是简化过的:

Vue.prototype._render = function (): VNode {
      const vm: Component = this
      // 解构出 $options 中的 render 函数
      const {
        render,
        staticRenderFns,
        _parentVnode
      } = vm.$options
      ...
      let vnode
      try {
        // 运行 render 函数
        vnode = render.call(vm._renderProxy, vm.$createElement)
      } catch (e) {
        ...
      }
      
      // set parent
      vnode.parent = _parentVnode
      return vnode
      }
    

_render 方法首先从 vm.$options 中解构出 render 函数,大家应该记得:vm.$options.render 方法是在 web-runtime-with-compiler.js 文件中通过 compileToFunctions 方法将 template 或 el 编译而来的。解构出 render 函数后,接下来便执行了该方法:

vnode = render.call(vm._renderProxy, vm.$createElement)
    

其中使用 call 指定了 render 函数的作用域环境为 vm._renderProxy,这个属性在我们整理实例对象的时候知道,他是在 Vue.prototype._init 方法中被添加的,即:vm._renderProxy = vm,其实就是Vue实例对象本身,然后传递了一个参数:vm.$createElement。那么 render 函数到底是干什么的呢?让我们根据上面那句代码猜一猜,我们已经知道 render 函数是从 template 或 el 编译而来的,如果没错的话应该是返回一个虚拟DOM对象。我们不妨使用 console.log 打印一下 render 函数,当我们的模板这样编写时:

<ul id="app">
      <li>{{a}}</li>
      </ul>
    

打印的 render 函数如下:

render函数1

我们修改模板为:

<ul id="app">
      <li v-for="i in b">{{a}}</li>
      </ul>
    

打印出来的 render 函数如下:

render函数2

其实了解Vue2.x版本的同学都知道,Vue提供了 render 选项,作为 template 的代替方案,同时为JavaScript提供了完全编程的能力,下面两种编写模板的方式实际是等价的:

// 方案一:
    new Vue({
     el: '#app',
     data: {
         a: 1
        },
      template: '<ul><li>{{a}}</li><li>{{a}}</li></ul>'
      })
    // 方案二:
    new Vue({
     el: '#app',
     render: function (createElement) {
          createElement('ul', [
               createElement('li', this.a),
                createElement('li', this.a)
         ])
      }
      })
    

现在我们再来看我们打印的 render 函数:

function anonymous() {
     with(this){
         return _c('ul', { 
              attrs: {"id": "app"}
            },[
             _c('li', [_v(_s(a))])
           ])
      }
      }
    

是不是与我们自己写 render 函数很像?因为 render 函数的作用域被绑定到了Vue实例,即:render.call(vm._renderProxy, vm.$createElement),所以上面代码中 _c_v_s 以及变量 a相当于Vue实例下的方法和变量。大家还记得诸如 _c_v_s 这样的方法在哪里定义的吗?我们在整理Vue构造函数的时候知道,他们在 src/core/instance/render.js文件中的 renderMixin 方法中定义,除了这些之外还有诸如:_l、 _m、 _o 等等。其中 _l 就在我们使用 v-for 指令的时候出现了。所以现在大家知道为什么这些方法都被定义在render.js 文件中了吧,因为他们就是为了构造出 render 函数而存在的。

现在我们已经知道了 render 函数的长相,也知道了 render 函数的作用域是Vue实例本身即:this(或vm)。那么当我们执行 render 函数时,其中的变量如:a,就相当于:this.a,我们知道这是在求值,所以 _mount 中的这段代码:

vm._watcher = new Watcher(vm, () => {
      vm._update(vm._render(), hydrating)
      }, noop)
    

当 vm._render 执行的时候,所依赖的变量就会被求值,并被收集为依赖。按照Vue中 watcher.js 的逻辑,当依赖的变量有变化时不仅仅回调函数被执行,实际上还要重新求值,即还要执行一遍:

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

这实际上就做到了 re-render,因为 vm._update 就是文章开头所说的虚拟DOM中的最后一步:patch

vm_render 方法最终返回一个 vnode 对象,即虚拟DOM,然后作为 vm_update 的第一个参数传递了过去,我们看一下 vm_update 的逻辑,在 src/core/instance/lifecycle.js 文件中有这么一段代码:

if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(
        vm.$el, vnode, hydrating, false /* removeOnly */,
        vm.$options._parentElm,
        vm.$options._refElm
      )
      } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
      }
    

如果还没有 prevVnode 说明是首次渲染,直接创建真实DOM。如果已经有了 prevVnode 说明不是首次渲染,那么就采用 patch 算法进行必要的DOM操作。这就是Vue更新DOM的逻辑。只不过我们没有将 virtual DOM 内部的实现。

现在我们来好好理理思路,当我们写如下代码时:

new Vue({
        el: '#app',
     data: {
         a: 1,
           b: [1, 2, 3]
        }
        })
    

Vue 所做的事:

1、构建数据响应系统,使用 Observer 将数据data转换为访问器属性;将 el 编译为 render 函数,render 函数返回值为虚拟DOM

2、在 _mount 中对 _update 求值,而 _update 又会对 render 求值,render 内部又会对依赖的变量求值,收集为被求值的变量的依赖,当变量改变时,_update 又会重新执行一遍,从而做到 re-render

用一张详细一点的图表示就是这样的:

详细流程

到此,我们从大体流程,挑着重点的走了一遍Vue,但是还有很多细节我们没有提及,比如:

1、将模板转为 render 函数的时候,实际是先生成的抽象语法树(AST),再将抽象语法树转成的 render 函数,而且这一整套的代码我们也没有提及,因为他在复杂了,其实这部分内容就是在完正则。

2、我们也没有详细的讲 Virtual DOM 的实现原理,网上已经有文章讲了,大家可以搜一搜

3、我们的例子中仅仅传递了 el ,data 选项,大家知道 Vue 支持的选项很多,比如我们都没有讲到,但都是触类旁通的,比如你搞清楚了 data 选项再去看 computed 选项或者 props 选项就会很容易,比如你知道了 Watcher 的工作机制再去看 watch 选项就会很容易。

本篇文章作为Vue源码的启蒙文章,也许还有很多缺陷,全当抛砖引玉了。

转载于:https://my.oschina.net/martin123/blog/897498

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值