一、前言
数据响应式和组件化系统现在是前端框架的标配了,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
}
- 调用 beforeMount 钩子
-
创建渲染 Watcher,且 Watcher 实例会首次计算表达式,创建 VNode Tree,进而生成 DOM Tree
- 这里回顾一下响应式依赖收集的过程
- 调用 mounted 钩子
- 返回组件实例 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 编辑器案例