數據響應式和組件化系統如今是前端框架的標配了,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編輯器案例