Vue2.0源码理解(1) - 数据和模板的渲染(上)

准备

一、首先去GitHub上把vue源码download下来,传送门:https://github.com/vuejs/vue
二、搭建一个vue-cli跑起来,用于代码调试,不看着代码动起来只看源码是没效果的,运行代码在 node_modules\vue\dist\vue.runtime.esm.js 中。

数据驱动

什么是数据驱动?数据驱动对我们又有什么帮助呢?

Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,对视图的修改,不会直接操作 DOM,而是通过修改数据。它相比我们传统的前端开发,如使用 jQuery 等前端库直接修改 DOM,大大简化了代码量。特别是当交互复杂的时候,只关心数据的修改会让代码的逻辑变的非常清晰,因为 DOM 变成了数据的映射,我们所有的逻辑都是对数据的修改,而不用碰触 DOM,这样的代码非常利于维护。
接下来这片文章就开始从源码的角度分析一下模板和数据如何渲染成最终的DOM的。

首先先整个小demo。

 <!-- html -->
<div id="app">
{{message}}
</div>
 // js
var app = new Vue({
el: "#app",
data: {
message: "Hello Vue!"
},
mounted(){
console.log(this.message);
console.log(this._data.message);
}
})

new Vue做了什么

首先从入口代码开始分析,new VUe究竟做了什么,在js语法中,new即创建一个实例化对象 ,Vue实际上就是一个类(构造函数),下面看一下vue源码

 // src\core\instance\index.js
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) //重点代码,由此进入初始化
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

Vue方法代码不多, 关键只是执行了this._init ,由于new Vue创建了实例化对象,因此this指向的其实就是Vue,所以运行的是Vue._init代码。而Vue._init则是在initMixin方法中有做定义。

 // src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++

let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}

// 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 {
// 合并option配置项,用于访问new Vue中的配置
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = 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')

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}

// 有el对象时候则使用$mount处理,然而现在脚手架默认以render的形式,因此此方法调试是不会往下跑。
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}

vue._init其实是一个大集合的初始化,由于东西太多,我们没理由一步步的往下分析,我们这章只探究数据和模板如何渲染的,我们先从数据入手。

data的数据是怎么挂载到this上的

简单使用过vue的都知道,当在data上面定义了一个属性,如属性名为msg,则在mount中我们可以使用this.msg访问该属性,然而data中的数据又是怎样挂载到this上面的呢?下面我们来分析一下。
其核心代码在Vue._init时候的initState方法

 // src\core\instance\init.js
initState(vm); //line57

找到运行initState方法的地方后,我们去看看initState方法到底做了什么

 // src\core\instance\state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options //获取new Vue中的配置
if (opts.props) initProps(vm, opts.props) //有配置props时,初始化props
if (opts.methods) initMethods(vm, opts.methods) //有配置methods时,初始化methods
if (opts.data) {
initData(vm) //我们配置了data,因此运行到这里
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

显而易见,initState其实就是看我们options配置了什么,配置了什么那就初始化什么。现在我们暂时之关注data,因此只需要把目光定位到initData方法。

 //初始化数据 - data处理
// src\core\instance\state.js
function initData (vm: Component) {
let data = vm.$options.data
//判断类型,挂载vm._data是为了后续监听数据变化同时做改变
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
}

initData其实就是把new VUe配置中的data取出来做处理,首先会去分析data的类型是对象还是方法(Vue3已经不允许设置对象了,犹大表示免得你们串数据了还怪我的Vue优化没写好>_<|||),getData其实主要是返回了function方法的return对象,具体这边不做详细分析了。对象最终会赋值给data和vm._data, 先划重点vm._data !后续会用到。接下来我们先继续往下看initData。

 //初始化数据 - proxy
// src\core\instance\state.js
function initData (vm: Component) {
...
// proxy data on instance
const keys = Object.keys(data) //获取data的keys值
const props = vm.$options.props //获取options配置中的props
const methods = vm.$options.methods
let i = keys.length
// 遍历data中的key,筛查是否props已经有定义,有则警告,没有则进入proxy
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}

上述代码首先是获取了data的keys值和options配置中的props,遍历keys数组筛查是否props中已经定义( 这也就是我们平时为什么props和data同时定义的时候data的会失效 ),没有匹配上则进入proxy, proxy也是data属性挂载到this上的核心方法 。至于最后的observe方法就是对data做了一个响应式的处理,这个后续会再分析。

 // 代理监听数据变化
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
//即this._data[key]
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
//即this._data[key]
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition) //key值是'_data'
}

proxy方法其实就是使用defineProperty实现对vm(new Vue的实例化对象,因此可以理解为this)代理监听
举个例子,假设data中有定义了一个msg的属性,那么当使用 a = this.msg 取值的时候就会触发defineProperty代理中的sharedPropertyDefinition.get方法,从而返回了this._data.msg(sourceKey在initData方法传过来的时候已经写死了是'_data')的值,this._data.msg的值又是哪里来的呢?还记得上面叫划重点的地方吗?就是那里从配置的data中拿出来的,所以data中的属性就是这样被通过this._data.msg,再通过代理监听从而挂载到了this上面。至于this.msg = 1这种设值其他同理,通过代理进入到了set方法,从而改动了this._data.msg的值,虽然没有改动到配置中的data值,但其实data中的值已经下岗了,this._data已经接替了它的任务了。顺带说一下,不建议访问this._data,编程界下划线代表的是默认属性,不应该是直接访问的。其实响应式的原理也差不多,至于响应式后面会再详细分析。

Vue实例挂载的实现

Vue._init中在方法最后还有一个vm.$mount,这就是我们接下来要详细分析的。

 // src\core\instance\init.js
Vue.prototype._init = function (options?: Object) {
...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}

上面代码块是通过new Vue时候配置el入参,还有一种外部调用$mount的方法,其中vm其实new Vue的实例化对象,传参其实也是一样。

 new Vue({
render: h => h(App),
}).$mount('#app')

下面我们开始详细分析一下$mount。

 // src\platforms\web\entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (){
...
}

首先用变量 mount把Vue.prototype.$mount缓存起来,然后再进行重写 ,那么Vue.prototype.$mount一开始是在哪里定义的呢?我们找到src\platforms\web\runtime\index.js,可以看到它的最初定义。

 // src\platforms\web\runtime\index.js
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}

下面我们对重写后的$mount做分段分析。
当调用$mount时,会重写$mount方法对传入的render函数或html结构做处理。

 // src\platforms\web\entry-runtime-with-compiler.js
// part 1
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)

/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
...
}

首先先去拿到需要处理的对象(query方法其实就是将穿进来的id转换为dom对象,具体实现可以自行看方法),然后再做了层逻辑判断,el不能是body或者document这两个dom对象,因为后续是会覆盖掉的。

 // src\platforms\web\entry-runtime-with-compiler.js
// part 2
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
...
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}

const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}

part2就相对复杂了。首先判断看看是不是render函数,不是则继续往下判断是否为template。
一、render函数:直接到最后一步return。
二、非render函数:
  1、template不为空:
    ①、template为string类型:利用其传入ID转换成dom对象。
    ②、template为dom对象,则拿出innerHTML。
    ③、都不是符合条件,那就拜拜,结束这次旅途。
  2、template为空,且el不为空:
    运行getOuterHTML(el),相当于将el的outerHTML赋值给template。
template处理完毕后,下面的方法则是一个对tempalte做编译(后续会另外细说),编译后会把template编译成一个render函数,然后最后执行mount方法。说到底mount方法它只认render函数,是它就直接运行,不是那就转成render,转换不来,那就告辞。
至于mount方法,一开始也有说过它缓存了初始定义的Vue.prototype.$mount,也就是如下代码:

 // src\platforms\web\runtime\index.js
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}

然后会执行到mountComponent函数。
下面代码隐藏了updateComponent函数上面的代码,这里不详细讲就简单提一下,主要3个点,①非render函数的处理;②callHook生命周期相关(后面细说);③performance的性能埋点(有兴趣的自行了解)。

 // src\core\instance\lifecycle.js
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean): Component {
...
//主要运行函数,vm._render方法下文会细说
updateComponent = () => {
// update后续再详细分析
vm._update(vm._render(), hydrating)
}

// 渲染watch,函数源文件在src\core\observer\watcher.js
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
}

这里显而易见,updateComponent是再new Watcher中运行起来的,下面我们刨析一下渲染类Watcher是怎么调用到updateComponent函数。

 // src\core\observer\watcher.js
export default class Watcher {
...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
...
if (typeof expOrFn === 'function') {
this.getter = expOrFn; //updateComponent赋予到getter对象
}
//this.lazy = !!options.lazy,options没传,因此运行this.get()
this.value = this.lazy ? undefined : this.get();
},
get () {
pushTarget(this)
let value
const vm = this.vm
try {
// 自调用updateComponent
value = this.getter.call(vm, vm)
}
...
return value
}
}

接收参数是updateComponent被expOrFn接收,尔后代码中定义了this.getter = expOrFn,后续 this.value = this.lazy ? undefined : this.get(); 三元表达式中调用了this.get(),然后调用了this.getter.call(),从而触发了updateComponent方法。
watcher类在此不多详细分析,后面再细讲。

标签: Javascript

添加新评论