1

一、前言

数据响应式和组件化系统现在是前端框架的标配了,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编辑器案例


currygolden
31 声望1 粉丝

人生如逆旅,我亦是行人