vue源码之架构

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

配置package.json文件如上,则运行npm run dev后,如果打印process.env.TARGETweb-full-dev。。。其实这个过程就相当于,给node里的process进程的env对象上添加属性。-w是watch模式,-c是指定配置文件

Runtime Only VS Runtime+Compiler

通常我们利用 vue-cli 去初始化我们的 Vue.js 项目的时候会询问我们用 Runtime Only 版本的还是 Runtime+Compiler 版本。下面我们来对比这两个版本。

  1. Runtime Only 我们在使用 Runtime Only 版本的 Vue.js 的时候,通常需要借助如 webpack 的 vue-loader 工具把 .vue 文件编译成JavaScript,因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,因此代码体积也会更轻量。
  2. Runtime+Compiler 我们如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,则需要在客户端编译模板,如下所示:
// 需要编译器的版本
new Vue({
  template: '<div></div>'
})

// 这种情况不需要
new Vue({
  render (h) {
    return h('div', this.hi)
  }
})

因为在 Vue.js 2.0 中,最终渲染都是通过 render 函数,如果写 template 属性,则需要编译成 render 函数,那么这个编译过程会发生运行时,所以需要带有编译器的版本。 很显然,这个编译过程对性能会有一定损耗,所以通常我们更推荐使用 Runtime-Only 的 Vue.js。

总结

通过这一节的分析,我们可以了解到 Vue.js 的构建打包过程,也知道了不同作用和功能的 Vue.js 它们对应的入口以及最终编译生成的 JS 文件。尽管在实际开发过程中我们会用 Runtime Only 版本开发比较多,但为了分析 Vue 的编译过程,我们这门课重点分析的源码是 Runtime+Compiler 的 Vue.js。

路线图:package.json -> scripts脚本 -> scripts/config.js -> resolve各个构建版本的来源 -> src/platforms/web/entry-runtime-with-compiler.js

按上述路线图,会经过 src/core/index.js,这里有两个主要注意地方,如下

  • import Vue from './instance/index'
  • initGlobalAPI(Vue) 前者是在原型上挂载初始化方法,后者是初始化vue全局api,咱分别来看。。。

原型上挂载初始化方法
在 src/core/instance/index.js 中:就是vue构造函数的最终来源,主要目的是是在Vue的prototype上挂载一些初始化方法

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全局api

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  Object.defineProperty(Vue, 'config', configDef)
 
  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
 
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick
 
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
 
  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue
 
  extend(Vue.options.components, builtInComponents)
 
  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}


注意,其实源码src目录就是vue相关的源码部分,而最近美团的mpvue其实就可以归总到platforms目录下

src
  |--compiler      # 编译相关 
  |--core          # 核心代码 
  |--platforms     # 不同平台的支持 
  |   |--web
  |   |--weex
  |--server        # 服务端渲染 
  |--sfc           # .vue 文件解析
  |--shared        # 共享代码

compiler
compiler 目录包含 Vue.js 所有编译相关的代码。它包括把模板解析成 ast 语法树,ast 语法树优化,代码生成等功能。

编译的工作可以在构建时做(借助 webpack、vue-loader 等辅助插件);也可以在运行时做,使用包含构建功能的 Vue.js。显然,编译是一项耗性能的工作,所以更推荐前者——离线编译。

core
core 目录包含了 Vue.js 的核心代码,包括内置组件、全局 API 封装,Vue 实例化、观察者、虚拟 DOM、工具函数等等。

这里的代码可谓是 Vue.js 的灵魂,也是我们之后需要重点分析的地方。

platform
Vue.js 是一个跨平台的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 natvie 客户端上。platform 是 Vue.js 的入口,2 个目录代表 2 个主要入口,分别打包成运行在 web 上和 weex 上的 Vue.js。

我们会重点分析 web 入口打包后的 Vue.js,对于 weex 入口打包的 Vue.js,感兴趣的同学可以自行研究。

server
Vue.js 2.0 支持了服务端渲染,所有服务端渲染相关的逻辑都在这个目录下。注意:这部分代码是跑在服务端的 Node.js,不要和跑在浏览器端的 Vue.js 混为一谈。

服务端渲染主要的工作是把组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记”混合”为客户端上完全交互的应用程序。

sfc
通常我们开发 Vue.js 都会借助 webpack 构建, 然后通过 .vue 单文件的编写组件。

这个目录下的代码逻辑会把 .vue 文件内容解析成一个 JavaScript 的对象。

shared
Vue.js 会定义一些工具方法,这里定义的工具方法都是会被浏览器端的 Vue.js 和服务端的 Vue.js 所共享的

建立demo

在vue源码的examples文件下新建目录,如my-test

my-test
  |--index.html      # html文件 
  |--app.js          # 核心代码 
<body>
  <div id="app">
    
  </div>
  <script src="../../dist/vue.js"></script>
  <script src="app.js"></script>
</body>
var app = new Vue({
  el: '#app',
  data: {
    message: 'hello vue'
  }
})

打开index.html即可在浏览器里显示hello vue。。。还可以在dist/vue.js文件里任意位置打debugger断点,然后页面回显时{ {message} },这就说明数据驱动视图是有个更新过程的,也就是把模板编译成渲染函数是需要过程的,假如直接用render函数,则不存在屏闪的效果。

断点技巧
断点技巧 依次向右:

  1. Resume script execution 断点之间跳转,点击一次跳下一个断点
  2. Setp over next function call 跳过函数内部逻辑,执行下一行代码
  3. Setp into next function call 不跳过函数内部逻辑,逐行执行
  4. Setp out of current function

学习源码的路线图,要分模块学习,切记忘了主线

how-to-study
  |--part1           # 数据驱动
  |   |--模板及数据如何渲染成最终的dom
  |   |--数据更新驱动视图变化
  |--app.js          # 核心代码 

part1 模板及数据如何渲染成最终的dom

how-to-study
  |--new Vue(option) -> Vue.prototype_init() -> mergeOptions -> initRender(vm) -> vm.$mount(vm.$options.el) -> 重写Vue.prototype.$mount() -> {render} = compileToFunctions() ->mount.call(this,el,hydrating) -> 原始Vue.prototype.$mount() -> mountComponent(this,el,hydrating) -> 注册watcher的getter方法updateComponent(){vm._update(vm._render(),hydrating)} -> new Watcher(vm,updateComponent,...) -> 

  vm._render() -> render.call(vm._renderProxy,vm.$createElement) 返回vnode ->  

  vm._update(vnode,hydrating) -> render.call(vm._renderProxy,vm.$createElement) 返回vnode ->  

new Vue()
当执行new Vue时,都发生了什么,我们知道Vue是一个构造函数,在src/core/instance/index.js中,有如下代码

function Vue (options) {
  // 当直接Vue({})时,会警告如下...
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    console.log('this', this)
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

注意点:
其实构造函数和普通函数都是函数,都可以直接调用,只是这里如果直接调用,this指向undefined(不过这是为什么?),如果在浏览器直接定义构造函数并调用则指向window

此时在终端里运行npm run dev就是构建的runtime-with-compiler版本,可以直接修改源码,然后会自动监听并构建出最新包,也就可以进行一些调试

此时还可直接在浏览器的devTools里直接获取到Vue相关的api,直接调用,比如直接Vue({})就可以看到警告,new Vue({})就可以打印出Vue实例 。当然devTools还可以查看很多东西,比如使用了keep-alive的组件,切换页面后会看到inactive标识,如果未缓存的话,直接整个就干掉了。

this._init(options)
_init函数在src/core/instance/init.js中定义,打开可以看到,也是在Vue的原型对象上添加的一个方法,如下:

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // a uid
  vm._uid = uid++
 
  let startTag, endTag
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    startTag = `vue-perf-start:${vm._uid}`
    endTag = `vue-perf-end:${vm._uid}`
    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 {
    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)
  }
}

总结起来,init初始化函数主要做了这几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。

注意点:
1、function (options?: Object){}是静态类型检查工具Flow的语法(类似TS),这里的意思是函数的参数可有可无,如果有的话必须为对象(但直接new Vue(1)也不报错为啥?官网说:可选参数将接受缺少的、undefined或匹配的类型。 但他们不会接受null。,但new Vue(null)也正常?)

2、在上述代码末尾有判断if(vm.$options.el)判断,就是将调用vm.$mount方法挂载vm(可理解为viewModel,连接view和model的桥梁),挂载的目标就是把模板渲染成最终的dom。

3、可以看到如果每次调用new Vue(),实例的uid都会++,因此每次实例化都会变化

Vue 实例挂载的实现
因为$mount这个方法的实现和平台、构建方式都相关。因此该方法多个目录下都有,我们重点分析带 compiler 版本的 $monut 实现,因为抛开 webpack 的 vue-loader,我们在纯前端浏览器环境分析 Vue 的工作原理,有助于我们对原理理解的深入

先来看一下 src/platform/web/entry-runtime-with-compiler.js 文件中定义:

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
 
  /* istanbul ignore if */
  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
  // resolve template/el and convert to render function
  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) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
 
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
 
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

首先,它对 el 做了限制,Vue 不能挂载在 body、html 这样的根节点上。接下来的是很关键的逻辑 —— 如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法。

这里我们要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个“在线编译”的过程,它是调用 compileToFunctions 方法实现的,编译过程我们之后会介绍。最后,调用原先原型上的 $mount 方法挂载。

注意点:
1、el不能是body和html标签,因为vue挂载完,会整体把这些标签替换成模板内容,如果是html或者body,覆盖之后页面结构就不正常了。

2、我们调用挂载函数时,只传入了el参数,即vm.$mount(vm.$options.el),但务必要注意,这里先经过el = el && query(el)处理,传入的是字符串#app,但经过query处理后,就返回一个dom对象了(前提是没有render函数,有的话不会执行这个):如下

/**
 * Query an element selector if it's not an element already.
 */
export function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      // 当app.js里定义的el在页面上找不到,会进来
      // 并返回一个div
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')
    }
    return selected
  } else {
    // 如果是dom对象,则直接返回
    return el
  }
}

注意当我们没有写render函数和template时,el一般传入的是类似#app的字符串,然后经过处理会返回dom对象,然后经过getOuterHTML处理返回字符串的template(其实就是html),然后再经过compileToFunctions将模板编译成render函数,因此最终都是为了得到render函数。

3、这里判断if (!options.render) {},因为调用$mount的时候只传入了el,那这里的render应该都是undefined啊,这样判断有什么意义?其实不是,若在实例化时定义了render函数,则这里能获取到(在执行_init时,有合并选项相关的代码),那为什么初始化时的data获取不到??

渲染函数作为字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力。该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode。

new Vue({
  el: '#app',
  render (createElement) {
    return createElement('div', this.message)
  },
  // 还可写成下面样式
  render (h) {
    // div是tag,this.message是子节点vNode,这里只是text而已
    // vNode可以多层嵌套
    return h('div', {
      attrs: {
        id: 'app1'
      }
    },
    this.message)
  },
  data(){
    return {
      message: 'hello vue'
    }
  }
})

如果组件是一个函数组件,渲染函数还会接收一个额外的 context 参数,为没有实例的函数组件提供上下文信息。

Vue 选项中的 render 函数若存在,则 Vue 构造函数不会从 template 选项或通过 el 选项指定的挂载元素中提取出的 HTML 模板编译渲染函数。

4、上面注意到const mount = Vue.prototype.$mount,也就是先把$mount方法缓存到一个变量里,然后再重新定义一个$mount方法。。。这是因为被缓存起来的$mount方法是共用的,但又根据平台的不同又需要一些不同的逻辑,比如这里是runtime-compiler版本,也就是需要编译模板,因此就需要增加Vue.prototype.$mount = function (){...}里的逻辑。

src/platform/web/runtime/index.js里可以看到$mount方法,也可以被runtime-only版本的Vue直接使用。

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

这里便看到$mount方法的参数一可以是字符串,也可以是dom对象。。。因为我们一般传的都是id名也就是字符串,因此会先经过query转为dom对象,这里判断在浏览器环境,如果非浏览器环境,当然也就没有dom了,所以就undefined了。参数二是和服务端渲染相关的。

然后看到又调用mountComponent方法,可以在src/core/instance/lifecycle.js中找到

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

从上面的代码可以看到,mountComponent 核心就是先调用 vm._render 方法先生成虚拟 Node,再实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,最终调用 vm._update 更新 DOM。

Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中监测的数据发生变化的时候执行回调函数

函数最后判断为根节点的时候设置 vm._isMounted 为 true, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例??。

注意点:

1、上面代码显示判断 if (!vm.$options.render){},因为这是compiler版本,即使用户没写自己的render函数,这里也已经有了系统提供的render函数了,当然用户在app.js自己写render函数,就会使用用户自己写的。。。

2、callHook(vm, 'beforeMount')是生命周期相关

3、config.performance 是性能检测,也就是说vue有自己的性能检测逻辑,但需要另外配置才可以启动

mountComponent方法的核心逻辑就是vm._render 和 vm._update。接下来再分析 _render
一般下划线开头的函数都是源码的私有方法,这里是用来渲染成一个虚拟node的,在 src/core/instance/render.js 中可以看到:

  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    // reset _rendered flag on slots for duplicate slot check
    if (process.env.NODE_ENV !== 'production') {
      for (const key in vm.$slots) {
        // $flow-disable-line
        vm.$slots[key]._rendered = false
      }
    }

    if (_parentVnode) {
      vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
    }

    // 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') {
        if (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
        }
      } else {
        vnode = vm._vnode
      }
    }
    // 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
  }

这段代码最关键的是 render 方法的调用,我们在平时的开发工作中手写 render 方法的场景比较少,而写的比较多的是 template 模板,在之前的 mounted 方法的实现中,会把 template 编译成 render 方法,

这里我们看到vnode = render.call(vm._renderProxy, vm.$createElement),call方法参数一是调用上下文,参数二是vm.$createElement,也就对应了render函数中的createElement方法了,而这里的vm.$createElement是什么呢。。。

src/core/instance/render.js 文件中,同样有个initRender方法,可以看到

export function initRender (vm: Component) {
  // ...
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}

可以看到,此时定义了两个方法,其中 vm._c是被模板编译成的 render 函数使用,而而 vm.$createElement 是用户自己写 render 方法使用的,两个方法参数相同,只是最后一个标志位不同而已。那createElement方法做了什么呢?其实就是返回一个虚拟node,因此先来说说虚拟dom

virtual dom
如果打开控制台,打印任意一个dom对象下的属性,会发现有很多属性。。。如果频繁的操作很多的dom,计算量可想而知,势必会造成一定的性能问题。。。因此假如可以用一个js对象去模拟dom节点,那它比创建一个真实dom代价要小很多。

在vue源码里,把VNode定义成了一个类,也就是说所有的虚拟dom都是基于这个类的。在 src/core/vdom/vnode.js中可以看到

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node
 
  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  fnScopeId: ?string; // functional scope id support
 
  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
 
  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

其实vNode是对真是dom的一种描述,它的核心无非就是定义几个关键属性,标签名、数据、子节点、键值等,其它属性都是都是用来扩展 VNode 的灵活性以及实现一些特殊 feature 的。由于 VNode 只是用来映射到真实 DOM 的渲染,不需要包含操作 DOM 的方法,因此它是非常轻量和简单的。

Virtual DOM 除了它的数据结构的定义,映射到真实的 DOM 实际上要经历 VNode 的 create、diff、patch 等过程。那么在 Vue.js 中,VNode 的 create 是通过之前提到的 createElement 方法创建的,我们接下来分析这部分的实现。

注意点:
1、这里类的写法,是flow的语法,也就是增加一些注释字段,可以直接使用

2、其实这里的虚拟dom借鉴了开源库snabbdom的实现,然后加入了一些vue特色的东西。因此可以了解snabbdom

createElement
可以在 src/core/vdom/create-elemenet.js找到相关逻辑,可以发现createElement 方法实际上是对 _createElement 方法的封装,它允许传入的参数更加灵活,在处理这些参数后,调用真正创建 VNode 的函数 _createElement:

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

_createElement 方法有 5 个参数,context 表示 VNode 的上下文环境,它是 Component 类型;tag 表示标签,它可以是一个字符串,也可以是一个 Component;data 表示 VNode 的数据,它是一个 VNodeData 类型,可以在 flow/vnode.js 中找到它的定义,这里先不展开说;children 表示当前 VNode 的子节点,它是任意类型的,它接下来需要被规范为标准的 VNode 数组;normalizationType 表示子节点规范的类型,类型不同规范的方法也就不一样,它主要是参考 render 函数是编译生成的还是用户手写的。

下面说两个重点的流程,children 的规范化和vnode的创建 children 的规范化
由于virtual dom实际上是一个树状结构,每个vnode可能会有若干个子节点,这些子节点也应该是vnode的类型,_createElement 接收的第 4 个参数 children 是任意类型的,因此需要把他们规范成vnode类型。

我们看到根据normalizationType的不同,调用不同的方法normalizeChildren(children)simpleNormalizeChildren(children),他们都定义在 src/core/vdom/helpers/normalzie-children.js 中,

// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}
 
// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

simpleNormalizeChildren方法调用场景是render函数是编译生成的时候。理论上编译生成的children都已经是vnode类型的了,但这里有一个例外,就是functional component函数式组件返回的是一个数组而不是一个根节点,所以会通过 Array.prototype.concat 方法把整个 children 数组打平,让它的深度只有一层。

_update
_update是实例的一个私有方法,被调用的时机是2个,一个是首次渲染,一个是数据更新的时候。方法的作用是将虚拟dom渲染成真实的dom。。。可以在src/core/instance/lifecycle.js里找到

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = 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)
  }
  activeInstance = prevActiveInstance
  // 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.
}

_update的核心就是调用vm.__patch__方法,这个方法实际上在不同的平台,比如web和weex上的定义是不一样的,如下:

Vue.prototype.__patch__ = inBrowser ? patch : noop

甚至在web平台上,是否是服务端渲染也会对这个方法产生影响,因为在服务端渲染中,是没有真实浏览器dom环境的,所以不需要把vnode最终转换为dom,因此patch的主要作用就是将虚拟dom转为真正的dom,因此是一个空函数noop(空函数一般都这样表示)

而浏览器中的__patch__方法在src/platforms/web/runtime/patch.js中,

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'
 
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)
 
export const patch: Function = createPatchFunction({ nodeOps, modules })

createPatchFunction 内部定义了一系列的辅助方法,最终返回了一个 patch 方法,这个方法就赋值给了 vm._update 函数里调用的 vm.patch

在介绍 patch 的方法实现之前,我们可以思考一下为何 Vue.js 源码绕了这么一大圈,把相关代码分散到各个目录。因为前面介绍过,patch 是平台相关的,在 Web 和 Weex 环境,它们把虚拟 DOM 映射到 “平台 DOM” 的方法是不同的,并且对 “DOM” 包括的属性模块创建和更新也不尽相同。因此每个平台都有各自的 nodeOps 和 modules,它们的代码需要托管在 src/platforms 这个大目录下。

而不同平台的 patch 的主要逻辑部分是相同的,所以这部分公共的部分托管在 core 这个大目录下。差异化部分只需要通过参数来区别,这里用到了一个函数柯里化的技巧,通过 createPatchFunction 把差异化参数提前固化,这样不用每次调用 patch 的时候都传递 nodeOps 和 modules 了,这种编程技巧也非常值得学习。其实这就是函数柯理化

nodeOps表示对平台dom的一些操作方法,modules表示平台的一些模块,他们会在整个patch过程的不同阶段执行相应的钩子函数,

总结:
到这里,我们就从从主线上把模板和数据如何渲染成最终的dom的过程分析完毕了,

总结:

当传给渲染函数的是字符串时,是编译模板,当将组件作为对象模式传给render函数,会执行creatComponent,而这个createComponent在渲染一个组件的时候,主要执行以下三个逻辑:

  • 构造子类构造器
  • 安装组件钩子函数(钩子函数允许人为的在某些时期加些自定义的处理)
  • 实例化vnode。 createComponent返回的是组件vnode,它也一样会走到vm._update方法,进而执行patch函数,把vnode转换为真正的dom节点。patch过程中会调用createEle创建元素节点,在完成组件的整个patch过程后,最后执行insert完成组件的dom插入,如果patch过程中又创建了子组件,那么dom的插入顺序是先子后父。

占位符可以理解为:当使用组件时,比如<HelloWorld></HelloWorld>就是占位符。

我们知道编写一个组件实际上是编写一个js对象,对象的描述就是各种配置,之前我们提到在_init的最初阶段执行的就是merge options的逻辑。

合并配置

new Vue的过程通常有两种,一种是外部我们的代码主动调用new Vue(options)的方式实例化一个Vue对象;另一个是在创建组件过程中内部new Vue(options)实例化子组件。

无论那种场景,都会执行实例的_init(options)方法,它首先会执行一个merge options的逻辑,相关代码在src/core/instance/init.js如下:

Vue.prototype._init = function (options?: Object) {
  // 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
    )
  }
  // ...
}

外部new Vue调用方式
当直接使用new Vue(options)就是进入else逻辑,实际就是resolveConstructorOptions(vm.constructor)的返回值与options做合并,而在最基础的场景下,它还是简单返回vm.constuctor.options,相当于Vue.options。。。在 src/core/global-api/index.js有相关 initGlobalAPI(Vue)的定义:

export function initGlobalAPI (Vue: GlobalAPI) {
  // ...
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
 
  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue
 
  extend(Vue.options.components, builtInComponents)
  // ...
}

其实就是初始化一个空对象,然后在其上遍历并挂载几个属性,然后将Vue构造函数挂载在Vue.options._base上,最后通过extend(Vue.options.components, builtInComponents) 将一些内置组件扩展到Vue.options.components上,如下:

Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}
Vue.options._base = Vue

再回到主函数mergeOptions上,主要功能就是把parent和child这两个对象根据一定合并策略,合并成一个新对象并返回。

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }
 
  if (typeof child === 'function') {
    child = child.options
  }
 
  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
}

比较核心的几步,先递归把extends和mixins合并到parent上,然后遍历 parent,调用 mergeField,然后再遍历 child,如果 key 不在 perent 的自身属性上,则调用 mergeField。而这个mergeField对不同的key有着不同的合并策略。。。比如钩子函数,一旦parent和child都定义了相同的钩子函数,则他们会把两个钩子函数合并成一个数组(执行顺序是???通过mixins传入的create是parent先),合并完结果大致如下:

{
  components: { },
  created: [
    function created() {
      console.log('parent created') 
    }
  ],
  directives: { },
  filters: { },
  _base: function Vue(options) {
    // ...
  },
  el: "#app",
  render: function (h) {  
    //...
  }
}

组件场景
当组件场景下, options._isComponent 为 true,那么合并 options 的过程走到了 initInternalComponent(vm, options) 逻辑。先来看一下它的代码实现,在 src/core/instance/init.js 中:

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode
 
  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag
 
  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}

initInternalComponent 只是做了简单一层对象赋值,并不涉及到递归、合并策略等复杂逻辑。

当合并完以后,大致结果如下:

vm.$options = {
  parent: Vue /*父Vue实例*/,
  propsData: undefined,
  _componentTag: undefined,
  _parentVnode: VNode /*父VNode实例*/,
  _renderChildren:undefined,
  __proto__: {
    components: { },
    directives: { },
    filters: { },
    _base: function Vue(options) {
        //...
    },
    _Ctor: {},
    created: [
      function created() {
        console.log('parent created') 
      }, function created() {
        console.log('child created') 
      }
    ],
    mounted: [
      function mounted() {
        console.log('child mounted') 
      }
    ],
    data() {
       return {
         msg: 'Hello Vue'
       }
    },
    template: '<div></div>'
  }
}

由于组件的构造函数时通过Vue.extend继承自Vue的,代码定义在在 src/core/global-api/extend.js 中。

/**
 * Class inheritance
 */
Vue.extend = function (extendOptions: Object): Function {
  // ...
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
 
  // ...
  // keep a reference to the super options at extension time.
  // later at instantiation we can check if Super's options have
  // been updated.
  Sub.superOptions = Super.options
  Sub.extendOptions = extendOptions
  Sub.sealedOptions = extend({}, Sub.options)
 
  // ...
  return Sub
}

这里的 extendOptions 对应的就是前面定义的组件对象,它会和 Vue.options 合并到 Sub.opitons 中。

总结:

纵观一些库、框架的设计几乎都是类似的,自身定义了一些默认配置,同时又可以在初始化阶段传入一些定义配置,然后去 merge 默认配置,来达到定制化不同需求的目的。

生命周期

每个 Vue 实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到 DOM、在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。

这一节主要介绍了 Vue 生命周期中各个钩子函数的执行时机以及顺序,通过分析,我们知道了如在 created 钩子函数中可以访问到数据,在 mounted 钩子函数中可以访问到 DOM,在 destroy 钩子函数中可以做一些定时器销毁工作,了解它们有利于我们在合适的生命周期去做不同的事情。

组件注册

普通组件注册
在我们平时的开发工作中,为了减少首屏代码体积,往往会把一些非首屏的组件设计成异步组件,按需加载。Vue 也原生支持了异步组件的能力,如下:

Vue.component('async-example', function (resolve, reject) {
   // 这个特殊的 require 语法告诉 webpack
   // 自动将编译后的代码分割成不同的块,
   // 这些块将通过 Ajax 请求自动下载。
   require(['./my-async-component'], resolve)
})

示例中可以看到,Vue 注册的组件不再是一个对象,而是一个工厂函数,函数有两个参数 resolve 和 reject,函数内部用 setTimout 模拟了异步,实际使用可能是通过动态请求异步组件的 JS 地址,最终通过执行 resolve 方法,它的参数就是我们的异步组件对象。

由于组件的定义并不是一个普通对象,所以不会执行Vue.extend的逻辑把它变成一个组件的构造函数,但是仍然会执行到createComponent函数。

Promise异步组件

Vue.component(
  'async-webpack-example',
  // 该 `import` 函数返回一个 `Promise` 对象。
  () => import('./my-async-component')
)

webpack 2+ 支持了异步加载的语法糖:() => import(‘./my-async-component’),当执行完 res = factory(resolve, reject),返回的值就是 import(‘./my-async-component’) 的返回值,它是一个 Promise 对象。接着进入 if 条件,又判断了 typeof res.then === ‘function’),条件满足,执行:

if (isUndef(factory.resolved)) { res.then(resolve, reject) } 当组件异步加载成功后,执行 resolve,加载失败则执行 reject,这样就非常巧妙地实现了配合 webpack 2+ 的异步加载组件的方式(Promise)加载异步组件。

高级异步组件
由于异步加载组件需要动态加载 JS,有一定网络延时,而且有加载失败的情况,所以通常我们在开发异步组件相关逻辑的时候需要设计 loading 组件和 error 组件,并在适当的时机渲染它们。Vue.js 2.3+ 支持了一种高级异步组件的方式,它通过一个简单的对象配置,帮你搞定 loading 组件和 error 组件的渲染时机,你完全不用关心细节,非常方便。接下来我们就从源码的角度来分析高级异步组件是怎么实现的。

const AsyncComp = () => ({
  // 需要加载的组件。应当是一个 Promise
  component: import('./MyComp.vue'),
  // 加载中应当渲染的组件
  loading: LoadingComp,
  // 出错时渲染的组件
  error: ErrorComp,
  // 渲染加载中组件前的等待时间。默认:200ms。
  delay: 200,
  // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  timeout: 3000
})
Vue.component('async-example', AsyncComp)

高级异步组件的初始化逻辑和普通异步组件一样,也是执行 resolveAsyncComponent,当执行完 res = factory(resolve, reject),返回值就是定义的组件对象,显然满足 else if (isDef(res.component) && typeof res.component.then === ‘function’) 的逻辑,接着执行 res.component.then(resolve, reject),当异步组件加载成功后,执行 resolve,失败执行 reject。

因为异步组件加载是一个异步过程,它接着又同步执行了如下逻辑(其实就是针对传入的):

总结 通过以上代码分析,我们对 Vue 的异步组件的实现有了深入的了解,知道了 3 种异步组件的实现方式,并且看到高级异步组件的实现是非常巧妙的,它实现了 loading、resolve、reject、timeout 4 种状态。异步组件实现的本质是 2 次渲染,除了 0 delay 的高级异步组件第一次直接渲染成 loading 组件外,其它都是第一次渲染生成一个注释节点,当异步获取组件成功后,再通过 forceRender 强制重新渲染,这样就能正确渲染出我们异步加载的组件了

组件注册

之前我们知道了Vue 怎么实现数据渲染和组件化的,,主要讲的是初始化的过程,把原始的数据最终映射到 DOM 中,但并没有涉及到数据变化到 DOM 变化的部分。而 Vue 的数据驱动除了数据渲染 DOM 之外,还有一个很重要的体现就是数据的变更会触发 DOM 的变化。

其实前端开发最重要的两个工作,一个是把数据渲染到页面,另一个是处理用户交互。 来看一个场景:

<div id="app" @click="changeMsg">
  
</div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  },
  methods: {
    changeMsg() {
      this.message = 'Hello World!'
    }
  }
})

当我们修改this.message的时候,模板对应的插值也会渲染成新的数据,那么这一切是怎么做到的呢? 如果不使用vue,我们会:监听点击事件,修改数据,手动操作dom更新渲染(如innerHTML)。这个过程和使用vue的最大区别就是多了一步手动操作dom重新渲染。这一步看上去并不多,但它背后又潜在的几个要处理的问题:

  1. 我需要修改哪块的 DOM?
  2. 我的修改效率和性能是不是最优的?
  3. 我需要对数据每一次的修改都去操作 DOM 吗?
  4. 我需要 case by case 去写修改 DOM 的逻辑吗?

响应式对象

能很多小伙伴之前都了解过 Vue.js 实现响应式的核心是利用了 ES5 的 Object.defineProperty,这也是为什么 Vue.js 不能兼容 IE8 及以下浏览器的原因,我们先来对它有个直观的认识。

Object.defineProperty 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象,先来看一下它的语法:

Object.defineProperty(obj, prop, descriptor) obj 是要在其上定义属性的对象;prop 是要定义或修改的属性的名称;descriptor 是将被定义或修改的属性描述符。

比较核心的是 descriptor,它有很多可选键值,具体的可以去参阅它的文档。这里我们最关心的是 get 和 set,get 是一个给属性提供的 getter 方法,当我们访问了该属性的时候会触发 getter 方法;set 是一个给属性提供的 setter 方法,当我们对该属性做修改的时候会触发 setter 方法。


初始化多个对象

Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
  Vue.options[type + 's'] = Object.create(null)
})

函数柯理化



参考:

  1. learnVue
  2. Vue技术内幕
  3. 如何阅读vue源码
  4. vue.js源码学习笔记(尤大推荐)

源码架构

为了对源码分析有一个整体的概念,先说说vue源码的整体结构,然后再具体分析每一个部分。

最近的文章

微信小程序原理

主题线路 小程序产生的原因。 小程序与普通网页对比,找到不同点。 分析每个不同点背后的技术原理。这篇文章没有具体讲如何写小程序,而是介绍小程序背后的一些技术原理,然后给大家一个对小程序整体的轮廓概念,然后具体的开发大家还得查看文档。。。首先声明这篇文章大多观点依然来自微信小程序官方文档,我只是结合自己的理解重新说明了一下,但更简练,更通俗些,有些东西我也是点到为止,因为咱们的主线是小程序,主线以外的东西,大家自己再补充吧。。。为何产生小程序?黑格尔说过:存在即合理。因此小程序的存在也...…

继续阅读
更早的文章

状态管理器

参考:React,flux,redux阮一峰说redux阮一峰说flux状态管理的由来一般来说,程序猿们大部分时间关注的可能不是研发某个具体算法,这是算法工程师/数学家们擅长的东东。程序猿的工作主要是通过调用编程环境中现成的工具函数或接口来实现具体的应用功能,将各个底层接口或算法模块用代码有秩序地拼装联接起来,实现酷炫好用的产品功能,如同组装一件乐高玩具一样。也就是说程序猿的很多工作往往不是围绕某个高大上的具体算法(“我们不生产算法,我们只是算法的搬运工”),而是像代码界的城管、...…

继续阅读