准备

一、首先去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类在此不多详细分析,后面再细讲。

Vue2.0源码学习(1) - 数据和模板的渲染(上)的更多相关文章

  1. Vue2.0源码学习(2) - 数据和模板的渲染(下)

    vm._render是怎么实现的 上述updateComponent方法调用是运行了一个函数: // src\core\instance\lifecycle.js updateComponent = ...

  2. Vue2.0源码学习(4) - 合并配置

    合并配置 通过之前的源码学习,我们已经了解到了new Vue主要有两种场景,第一种就是在外部主动调用new Vue创建一个实例,第二个就是代码内部创建子组件的时候自行创建一个new Vue实例.但是无 ...

  3. Vue2.0源码学习(3) - 组件的创建和patch过程

    组件化 组件化是vue的另一个核心思想,所谓的组件化就,就是说把页面拆分成多个组件(component),每个组件依赖的css.js.图片等资源放在一起开发和维护.组件是资源独立的,在内部系统中是可以 ...

  4. Vue2.0源码学习(6) - 组件注册

    组件注册 前言 在 Vue.js 中,除了它内置的组件如 keep-alive.component.transition.transition-group 等,其它用户自定义组件在使用前必须注册.在开 ...

  5. Spring5.0源码学习系列之事务管理概述

    Spring5.0源码学习系列之事务管理概述(十一),在学习事务管理的源码之前,需要对事务的基本理论比较熟悉,所以本章节会对事务管理的基本理论进行描述 1.什么是事务? 事务就是一组原子性的SQL操作 ...

  6. 【Spark2.0源码学习】-1.概述

          Spark作为当前主流的分布式计算框架,其高效性.通用性.易用性使其得到广泛的关注,本系列博客不会介绍其原理.安装与使用相关知识,将会从源码角度进行深度分析,理解其背后的设计精髓,以便后续 ...

  7. [Android FrameWork 6.0源码学习] View的重绘过程之WindowManager的addView方法

    博客首页:http://www.cnblogs.com/kezhuang/p/关于Activity的contentView的构建过程,我在我的博客中已经分析过了,不了解的可以去看一下<[Andr ...

  8. spark2.0源码学习

    [Spark2.0源码学习]-1.概述 [Spark2.0源码学习]-2.一切从脚本说起 [Spark2.0源码学习]-3.Endpoint模型介绍 [Spark2.0源码学习]-4.Master启动 ...

  9. Spring5.0源码学习系列之浅谈BeanFactory创建

    Spring5.0源码学习系列之浅谈BeanFactory创建过程 系列文章目录 提示:Spring源码学习专栏链接 @ 目录 系列文章目录 博客前言介绍 一.获取BeanFactory主流程 二.r ...

随机推荐

  1. Webstorm安装与配置

    一 下载 链接:https://pan.baidu.com/s/1gKxzGWvnoCpXPoe8zzfLnQ 提取码:5lyf 二 安装 https://www.jb51.net/softs/598 ...

  2. Visaul Studio 2015 MFC控件使用之--按钮(Button)

    在MFC开发当中,比较常用的控件之一便是Button控件了,该控件的除了可以通过点击产生的开关量当作开关来使用,还可以设置其颜色变化当作显示灯,按钮控件的使用相对来比较简单. 打开工程解决方案的资源视 ...

  3. TextBox,RichTextBox设置行高

    /// <summary> /// 设置行距 /// </summary> /// <param name="ctl">控件</param ...

  4. C# 实现Parallel.For

    static class MyParallel { //4.0及以上用Task, Task的背后的实现也是使用了线程池线程 //static List<Task> tasks = new ...

  5. UML 有关类图知识及类间关系

    原文链接:https://blog.csdn.net/mj_ww/article/details/53020346 1. 类的含义 类图(Class diagram)显示了系统的静态结构,而系统的静态 ...

  6. SSM项目使用拦截器实现登录验证功能

    SSM项目使用拦截器实现登录验证功能 登录接口实现 public User queryUser(String UserName, String Password,HttpServletRequest ...

  7. Python如何把八进制转换成ASCII码

    做题途中拿到一串八进制字符串 0126 062 0126 0163 0142 0103 0102 0153 0142 062 065 0154 0111 0121 0157 0113 0111 010 ...

  8. 【Android】安卓四大组件之内容提供者

    [Android]安卓四大组件之内容提供者 1.关于内容提供者 1.1 什么是内容提供者 内容提供者就是contentProvider,作用有如下: 给多个应用提供数据 类似一个接口 可以和多个应用分 ...

  9. 边带权并查集 学习笔记 & 洛谷P1196 [NOI2002] 银河英雄传说 题解

    花了2h总算把边带权并查集整明白了qaq 1.边带权并查集的用途 众所周知,并查集擅长维护与可传递关系有关的信息.然而我们有时会发现并查集所维护的信息不够用,这时"边带权并查集"就 ...

  10. Kubernetes之日志和监控(十五)

    一.日志和监控 1.1.Log 1.1.1.容器级别 通过docker命令查看容器级别的日志 docker ps --->containerid docker logs containerid ...