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