一、前言

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