从vue构造函数开始

44次阅读

共计 6548 个字符,预计需要花费 17 分钟才能阅读完成。

一、前言

数据响应式和组件化系统现在是前端框架的标配了,vue 当然也不例外。

之前已经聊过数据响应式的原理,这一期本来想对组件化系统展开探讨。

组件包括根组件和子组件,每个 Vue 实例,都是用 new Vue(options) 创建而来的,只是应用的根组件实例是用户显式创建的,而根组件实例里的子组件是在渲染过程中隐式创建的。

所以问题是我们所写的以 vue 后缀结尾的文件是经过怎么样的流程到渲染到页面上的 dom 结构?

但这个问题太庞大,以致涉及到许多的前置知识点,本文从 vue 构造函数开始,来梳理一下其中的流程!

为什么要了解这些

  • 数据驱动
  • 多端渲染
  • 分层设计 vnode
  • 设计思想

二、vue 构造函数

业务中很少会去处理 Vue 构造函数,在 vue-cli 初始化的项目中有 main.js 文件, 一般会看到如下结构

new Vue({
  el: '#app',
  i18n,
  template: '<App/>',
  components: {App}
})

记得之前在分享 virtual-dom 的时候提到,vue 组件通过 render 方法获取到 vnode, 之后再经过 patch 的处理,渲染到真实的 dom。所以我们的目标就是从 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)
}

Vue.prototype.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
    )
  }
  // 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')

    // 针对根组件
  if (vm.$options.el) {vm.$mount(vm.$options.el)
  }
}

先不关注具体方法做了什么大致流程包括

  • 合并组件的 options
  • 初始化组件数据

    • 生命周期相关数据
    • 事件相关数据
    • 渲染相关数据
    • 调用 beforeCreate 钩子
    • provide/inject 相关数据
    • 状态相关数据
    • 调用 created 钩子

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

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

mountComponent 函数

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el

  // 非生产环境下,对使用 Vue.js 的运行时版本进行警告
  callHook(vm, 'beforeMount')
  let updateComponent
  updateComponent = () => {vm._update(vm._render(), hydrating)
  }
  // 创建 watcher 实例
  new Watcher(vm, updateComponent, noop, {before () {if (vm._isMounted) {callHook(vm, 'beforeUpdate')
    }
   }
  }, true /* isRenderWatcher */)
  return vm
}
  1. 调用 beforeMount 钩子
  2. 创建渲染 Watcher,且 Watcher 实例会首次计算表达式,创建 VNode Tree,进而生成 DOM Tree

    1. 这里回顾一下响应式依赖收集的过程
  3. 调用 mounted 钩子
  4. 返回组件实例 vm
  • vm._render 函数的作用是调用 vm.$options.render 函数并返回生成的虚拟节点 (vnode)
  • vm._update 函数的作用是把 vm._render 函数生成的虚拟节点渲染成真正的 DOM

三、代理访问

为什么通过 vm.xxx 可以访问到 props 和 data 数据?

通过 Object.defineProperty 在 vm 上新增加了一属性,属性访问器描述符的 get 特性就是获取 vm._props[key](以 props 为例)的值并返回,属性的访问器描述符的 set 特性就是设置 vm._props[key] 的值。

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
// 定义了 get/set
export function proxy (target: Object, sourceKey: string, key: string) {sharedPropertyDefinition.get = function proxyGetter () {return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
// 代理访问
proxy(vm, `_props`, key)

// initData 里
proxy(vm, `_data`, key)

访问 this.a 实际是访问 this.data.a

四、计算属性

4.1: 计算属性和 methods 的例子

参考 vue 官网提供的例子

  • 计算属性是基于它们的响应式依赖进行缓存的,只在相关响应式依赖发生改变时它们才会重新求值
  • 相比之下,每当触发重新渲染时,调用方法将总会再次执行函数

4.2: 代理访问

在实例上访问计算属性实际是做了什么

4.3: 初始化计算属性

看一下 initComputed 方法

const computedWatcherOptions = {lazy: true}

function initComputed (vm: Component, computed: Object) {
  // 初始化在实例上挂载_computedWatchers
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(`Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      // 创建计算属性 Watcher
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.

    // 注意此处:in 操作符将枚举出原型上的所有属性,包括继承而来的计算属性,因此针对组件特有的计算属性与继承而来的计算属性,访问方式不一样
    // 1、组件实例特有的属性:组件独有的计算属性将挂载在 vm 上
    // 2、组件继承而来的属性:组件继承而来的计算属性已挂载在 vm.constructor.prototype
    if (!(key in vm)) {
      // 处理组件实例独有的计算属性
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // 计算属性的 key 不能存在在 data 和 prop 里
      if (key in vm.$data) {warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
  • 创建 vm._computedWatchers 属性
  • 根据 computed 的 key 创建 watcher 实例,称为计算属性的观察者
  • defineComputed(vm, key, userDef)
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.set
      ? userDef.set
      : noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {sharedPropertyDefinition.set = function () {
      warn(`Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  // 往 vm 上添加 computed 的访问器属性描述符对象
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
  • 确定 sharedPropertyDefinition.get 是什么
  • 添加加 computed 的访问器属性描述符对象

最后的访问器属性 sharedPropertyDefinition 大概是

sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: createComputedGetter(key),
  set: userDef.set // 或 noop
}

访问计算属性 this.a 实际触发 getter 如下

function createComputedGetter (key) {return function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {if (watcher.dirty) {
        // 若是有依赖发生过变化,则重新求值
        watcher.evaluate()}
      if (Dep.target) {
        // 将该计算属性的所有依赖添加到当前 Dep.target 的依赖里
        watcher.depend()}
      return watcher.value()}
  }
}

先来看一下 watcher 构造函数

class Watcher {
    constructor (
    vm: Component,
    expOrFn: string | Function,// 触发 get 的方式
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean // 是否是渲染函数的观察者
    )
  if (this.computed) {
    this.value = undefined
    // computed 的观察者
    this.dep = new Dep()} else {
  // 求值,什么时候收集依赖
    this.value = this.get()}
  
  // 收集依赖
  depend () {
  // Dep.target 值是渲染函数的观察者对象
    if (this.dep && Dep.target) {this.dep.depend()
    }
  }
  // 求值
  evaluate () {if (this.dirty) {
    // 关键地方
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }
}
  • 回顾一下响应式原理 Dep-watcher 的观察者模式
  • 在计算属性的 watcher 里收集了渲染函数的观察者对象
  • 初始化求值的时候会触发属性的 get,从而收集依赖也就是计算属性的观察者
  • 在计算属性所依赖的数据变化时,就会触发更新

4.4: 总结

到这里我们来回顾一下计算属性相关的流程

  • 在 vue 实例上定义 watchers 属性
  • 根据计算属性的 key, 以及实际的 get 方法创建 watcher 实例
  • 实现代理访问,定义访问器属性
  • 访问计算属性,第一次走到 evaluate 函数,从而触发触发渲染函数的 get 导致对应的 watcher 收集依赖

最后提供一个计算属性实际的例子,来分析流程,(但是这里貌似需要读者熟悉 dep,watcher 的观察者模式)

五、其它

本文思路从 vue 构造函数开始,在初始化流程中关注 initstate 方法,选择其中的 computed 属性展开介绍。

对 computed 属性的初始化处理也是 vue 典型的初始化处理模式,其中多处可见的 Object.defineProperty 方法,实例化观察者 watcher 对象,基于 dep 和 watcher 建立的观察者模式。

在其它的数据初始化章节,在响应式处理流程都会遇到这些概念。

最后介绍一个数据流驱动的项目案例 H5 编辑器案例

正文完
 0