回忆
前端的工作,把数据在视图上展示,完善用户交互逻辑。数据驱动渲染,大幅减少前端工作人员对于数据在Dom上展示的工作,只要专注于数据处理即可。这一小节我们了解一下数据驱动渲染的大概过程。
Vue就是一个Function类,new Vue->_init,created和beforeCreated在这个阶段调用。主要做了合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等初始化工作。
->$mount,我们不借助vue-loader等编译工具,采用compile版本的vue进行线上编译的话,会先把template编译成render函数。再调用Vue.prototype.$mount,会调用mountComponent(由New Watcher触发update(render),render把render渲染成vnode,update根据vnode通过creat、diff、patch把vnode变成真实Dom插入)。
概述
Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据。它相比我们传统的前端开发,如使用 jQuery 等前端库直接修改 DOM,大大简化了代码量。特别是当交互复杂的时候,只关心数据的修改会让代码的逻辑变的非常清晰,因为 DOM 变成了数据的映射,我们所有的逻辑都是对数据的修改,而不用碰触 DOM,这样的代码非常利于维护。
在 Vue.js 中我们可以采用简洁的模板语法来声明式的将数据渲染为 DOM:
最终它会在页面上渲染出 Hello Vue。接下来,我们会从源码角度来分析 Vue 是如何实现的,分析过程会以主线代码为主,重要的分支逻辑会放在之后单独分析。数据驱动还有一部分是数据更新驱动视图变化,这一块内容我们也会在之后的章节分析,这一章我们的目标是弄清楚模板和数据如何渲染成最终的 DOM。
new Vue 发生了什么
从入口代码开始分析,我们先来分析 new Vue 背后发生了哪些事情。我们都知道,new 关键字在 Javascript 语言中代表实例化是一个对象,而 Vue 实际上是一个类,类在 Javascript 中是用 Function 来实现的。
初始化
来看一下源码,在src/core/instance/index.js 中调用了init()
instance/init.js
Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。
我们来看看initState(instance/state.js)
其中我们主要看initData
Vue 的初始化逻辑写的非常清楚,把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然,这样的编程思想是非常值得借鉴和学习的。
由于我们这一章的目标是弄清楚模板和数据如何渲染成最终的 DOM,所以各种初始化逻辑我们先不看。在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载(这一行为)的目标就是把模板渲染成最终的 DOM,那么接下来我们来分析 Vue 的挂载过程。
$mount实例挂载
Vue 中我们是通过 $mount 实例方法去挂载 vm 的,$mount 方法在多个文件中都有定义,如 src/platform/web/entry-runtime-with-compiler.js、src/platform/web/runtime/index.js、src/platform/weex/runtime/index.js。因为 $mount 这个方法的实现是和平台、构建方式都相关的。接下来我们重点分析带 compiler 版本的 $monut 实现,因为抛开 webpack 的 vue-loader,我们在纯前端浏览器环境分析 Vue 的工作原理,有助于我们对原理理解的深入。
compiler 版本的 $monut 实现非常有意思,先来看一下 src/platform/web/entry-runtime-with-compiler.js 文件中定义。
compile版本的话,先对我们el做一些处理,在没有定义render函数时候,尝试去获取reder函数(把整个template通过一系列逻辑判断,因为template有很多实现方式,比如可以直接写template,也可以template是个dom,不写的话通过el获取dom,再把template通过编译手段转化成render函数),再去调用mountComponent(定义了updateComponent函数),updateComponent函数实际上定一个Watcher进行渲染(实际上执行了真正的渲染),视图首次渲染、更新渲染,都通过它来完成。
$mount 方法支持传入 2 个参数,第一个是 el,它表示挂载的元素,可以是字符串,也可以是 DOM 对象,如果是字符串在浏览器环境下会调用 query 方法转换成 DOM 对象的。第二个参数是和服务端渲染相关,在浏览器环境下我们不需要传第二个参数。
$mount 方法实际上会去调用 mountComponent 方法(core/instance/lifecyle.js)原先原型上的 $mount 方法在这里重新定义,之所以这么设计完全是为了复用,因为它是可以被 runtime only 版本的 Vue 直接使用的,同时也要被compile版本使用。
接下来我们看看原型上的mount方法 它在core/instance/lifecyle.js
mountComponent核心就是先调用 vm._render 方法先生成虚拟 Node,再实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,最终调用 vm._update 更新 DOM。
Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数(updateComponent),另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数(updateComponent),这块儿我们会在之后的章节中介绍。
函数最后判断为根节点的时候设置 vm._isMounted 为 true, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。
mountComponent 方法的逻辑也是非常清晰的,它会完成整个渲染工作,接下来我们要重点分析其中的细节,也就是最核心的 2 个方法:vm._render 和 vm._update。