数据响应式和组件化系统如今是前端框架的标配了,vue固然也不例外。前端
以前已经聊过数据响应式的原理,这一期原本想对组件化系统展开探讨。vue
组件包括根组件和子组件,每一个 Vue 实例,都是用new Vue(options)建立而来的,只是应用的根组件实例是用户显式建立的,而根组件实例里的子组件是在渲染过程当中隐式建立的。node
因此问题是咱们所写的以vue后缀结尾的文件是通过怎么样的流程到渲染到页面上的dom结构?vue-cli
但这个问题太庞大,以至涉及到许多的前置知识点,本文从vue构造函数开始,来梳理一下其中的流程!缓存
为何要了解这些前端框架
业务中不多会去处理Vue构造函数,在vue-cli初始化的项目中有main.js文件,通常会看到以下结构app
new Vue({ el: '#app', i18n, template: '<App/>', components: { App } })
记得以前在分享virtual-dom的时候提到,vue组件经过render方法获取到vnode,以后再通过patch的处理,渲染到真实的dom。因此咱们的目标就是从vue构造函数开始,来梳理这个主流程框架
vue构造函数dom
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) } }
先不关注具体方法作了什么大体流程包括
初始化组件数据
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 }
建立渲染 Watcher,且 Watcher 实例会首次计算表达式,建立 VNode Tree,进而生成 DOM Tree
为何经过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
参考vue官网提供的例子
在实例上访问计算属性实际是作了什么
看一下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 操做符将枚举出原型上的全部属性,包括继承而来的计算属性,所以针对组件特有的计算属性与继承而来的计算属性,访问方式不同 // 一、组件实例特有的属性:组件独有的计算属性将挂载在 vm 上 // 二、组件继承而来的属性:组件继承而来的计算属性已挂载在 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) } } } }
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大概是
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的观察者模式)
本文思路从vue构造函数开始,在初始化流程中关注initstate方法,选择其中的computed属性展开介绍。
对computed属性的初始化处理也是vue典型的初始化处理模式,其中多处可见的Object.defineProperty方法,实例化观察者watcher对象,基于dep和watcher创建的观察者模式。
在其它的数据初始化章节,在响应式处理流程都会遇到这些概念。
最后介绍一个数据流驱动的项目案例 H5编辑器案例