Vue.js的核心是**数据驱动**。指**视图是由数据驱动生成的**,我们**对视图的修改,不会直接操作DOM,而是通过修改数据**。
与传统前端(jQuery):大大简化了代码量,逻辑变得清晰,非常利于维护。只关心数据的变化会使逻辑变得非常清晰。
# new Vue发生了什么
new 实例化是一个对象。Vue只能通过new关键字初始化,然后调用**this._init**方法
Vue初始化主要就干了几件事:**合并配置,初始化生命周期,初始化事件中心,初始化render,初始化data、props、computed、watcher等**。
~~~
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 {
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)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
~~~
*****
**当 new Vue的时候:**
执行this._init:一堆初始化工作
1. **合并options:将所有传入的options,merge到$options上。**
所有可以通过vm.$options.el,访问代码的el,vm.$options.data.
2. 初始化一堆内容:**初始化生命周期,初始化事件中心,初始化render,初始化data、props、computed、watcher等**
3. 初始化之后,判断当前options是否有el(字符串),会调用**vm.$mount**方法,挂载页面。
调试源码方法:debugger
**this.message能访问到?**
initState(vm)中initData
data:是一个函数,
vm._data = data
**所有的data,methods,computed的内容,最终都会挂载到vm上。**,可以通过this方法。
通过proxy函数实现,定义getter和setter,通过defineProperty方法代理key
~~~
proxy(vm, '_data', key)
~~~
**通过proxy做了一层代理:当调用了this.message时,实际上调用了this._data.message,即访问this.data.message。**
**此时还做了响应式的处理。**
# Vue实例挂载的实现(vm.$mount)
Vue中我们**通过$mount实例方法去挂载vm的,**因为$mount这个方法的实现和平台、构建方式都相关,重点分析compiler版本的$mount实现,抛开webpakc的**vue-loader**,我们在纯前端浏览器环境分析Vue的工作原理。
分析代码:
* 首先,它对el做了限制,Vue不能挂载在**body,html**这样的根节点上(因为**会覆盖**)。
* **如果没有定义render方法**,则会把**el**或则**template字符串**转换成**render方法**。(Vue2.0版本中,所有Vue的组件的渲染都需要render方法,无论我们用单文件.vue开发,还是写了el或则template属性,最终都会转换成render方法),这个过程是vue的一个“在线编译”的过程。**Vue最终只认render函数。**
* 调用原先**原型上的$mount方法挂载**。
$mount 方法支持传入2个参数,一个是el(挂载的元素,字符串或dom对象),第二个参数和服务端渲染有关。
$mount方法实际上会去调用mountComponent方法,**核心是先实例化一个渲染Watcher**,在它的回调函数中**会调用updateComponent方法**,在此方法中**调用`vm._render`方法先生成虚拟Node,最终调用`vm._update`更新DOM。**
**new Watcher(渲染Watcher)**,实际上是一个观察者模式。
在这里两个作用:
1. 初始化的时候会执行回调函数
2. 当vm实例中的检测的数据发生变化的时候执行回调函数。
函数最后判断为根节点的时候设置`vm._isMounted`为`true`, 表示这个实例已经挂载了,同时执行`mounted`钩子函数。
**梳理:**
拿到render函数(**template编译转换的函数(调用vm._c)或手写render(调用vm.$createElement)**),调用$mount方法(mountComponent方法(定义了updateComponent函数,是个渲染watcher,实际上执行了真实的渲染,除了了首次,还有监听更新。入口都是updateComponent)),
# render
Vue的_render方式是实例的一个私有方法,用来**把实例渲染成一个虚拟Node**。返回一个Vnode。
我们平时开发中手写render方法的场
景比较少,而写的比较多的是template模板,在之前的**mounted方法实现中,会把template编译成render方法**,这个编译过程非常复杂。
render函数的第一个参数是createElement,就是vm.$createElement.
实际上vm.$createElement方法定义在执行**initRender方法**的时候,可以看到除了`vm.$createElement`方法,还有一个`vm._c`方法,它是被模板编译成的render函数使用,而**vm.$createElement是用户手写render方法使用的**,这两个方法制成的参数相同,并且内部都调用了ceateElement方法。
**vm._render最终通过执行createElement方法并返回的是vnode,它是一个虚拟Node。**
~~~
render(createElement){
return createElement('div',{
attrs:{
id:'#app1',
}
}, this.$message)
}
~~~
根节点只能有一个Vnode,
# Virtual DOM
产生的前提是**浏览器中DOM是很“昂贵”的**。真正的DOM元素是非常庞大的,因为浏览器的标准就把**DOM设计的非常复杂**。当我们**频繁的去做DOM更新**,会产生**一定的性能问题**。
Virtual DOM(VNode)就是用一个原生**JS对象去描述一个DOM节点**,所以它比**创建一个DOM的代价要小很多**。
Virtual DOM使用VNode这么一个Class去描述。实际上Vue.js中Virtual DOM是**借鉴了一个开源库snabbdom的实现**。然后加入了一些Vue.js特色的东西。
其实VNode是对**真实DOM的一种抽象描述**,它的核心定义无非就是几个**关键属性**,标签名、数据、子节点、键值、class属性等,其他属性都是用来扩展VNode的灵活性以及实现一些特殊feature的。**由于VNode只是用来映射到真实DOM的渲染,不需要包含操作DOM的方法,因此它是非常轻量和简单的**。
Virtual DOM除了它的**数据结构的定义**,映射到真实的DOM实际上要经历**VNode的create、diff、patch等过程**。
那么在Vue.js中,VNode的**create是通过之前提到的createElement方法创建的**。
# createElement
render函数(**template编译转换的函数(调用vm._c)或手写render(调用vm.$createElement)**),这两个方法**都会调用createElement函数**。
Vue.js利用createElement方法创建VNode,createElement方法实际上是对`_createElement`方法的封装,它允许传入的参数更加灵活,**在处理这些参数后,调用真正创建VNode的函数_createElement**
~~~
~~~
_createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode>
~~~
~~~
_createElement方法有5个参数,context表示VNode的上下文环境,它是Component类型;tag表示标签,它可以是一个字符串,也可以是一个Component,data表示VNode数据,children表示当前VNode的子节点,它是任意类型的,它接下来需要被规范为标准的VNode数组,normalizationType表示子节点规范的类型,类型不同规范的方法也就不一样。
**具有属性__ob__,说明是响应式的。**
isPrimitive:是否是基础类型
## children的规范化
**由于Virtual DOM实际上是一个树状结构,每一个VNode可能会有若干个子节点,这些子节点应该也是VNode类型。**
经过对children的规范化,children变成了一个类型为VNode的Array。
## VNode的创建
回到createElement函数,规范化children后,接下来会去创建一个VNode的实例。
总结:
createElement创建VNode的过程:**每个VNode有children,children每个元素也是一个VNode,这样就形成了一个VNode Tree,它很好的描述了我们的DOM Tree。**
回到mountComponent函数的过程,我们已经知道**vm._render是如何创建了一个VNode**,接下来就是把这个**VNode渲染成一个真实的DOM并渲染出来,这个过程通过vm._update完成**。
# update
Vue的_update是实例的一个私有方法(原型方法),它被调用的时机有2个:**一个是首次渲染,一个是数据更新的时候**。
本次只分析首次渲染,数据更新部分在之后的分析响应式原理时涉及。
**_update方法的作用是把VNode渲染成真实的DOM**
**_update的核心就是调用`vm.__patch__`方法**,这个方法实际上在不同平台定义是不一样的。是否是服务器渲染也会对这个方法产生影响。因为在服务端渲染中,没有真实的浏览器DOM环境,所以不需要把VNode最终转换成DOM,因此是一个空函数。
~~~
function patch (oldVnode, vnode, hydrating, removeOnly)
~~~
**patch方法**:接收4个参数,**oldVnode**表示旧的**VNode节点**,它也可以**不存在或者是一个DOM对象**;**vnode**表示执行_render后返回VNode的节点;**hydrating**表示是否是服务端渲染;**removeOnly**是给transition-group用的。
我们在vm._update的方法是这么调用patch方法的:
**函数科里化技巧,一次性传入,每次调用pathc时,不用关心差异化,因为第一次调用的时候已经存起来了**
~~~
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
~~~
首次渲染场景:
在执行path函数的时候,传入的vm.$el对应的是例子中id为app的DOM对象,vm.$el的赋值是在之前mountComponent函数做的,vnode对应的是render函数里的返回值。hydrating在非服务端情况下渲染为false,removeOnly为false。
我们传入的oldVnode实际上是一个DOM container, 把oldVnode转化成VNode对象,然后再调用createElm方法,这个方法非常重要:
~~~
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
)
~~~
createElm的作用是通过**虚拟节点创建真实的DOM并插入到它的父节点**。
最后调用insert方法,把DOM插入到父节点中,因为是递归调用,子元素会优先调用insert,所以真个vnode树节点的插入顺序是先子后父。
insert其实就是调用原生DOM的API进行DOM操作。
![](https://box.kancloud.cn/9286e387ffaf5851147f3ba171d7549a_1083x510.png)
实际项目中,我们是把页面拆成很多组件的,**Vue另一个核心思想就是组件化**。
*****
- 空白目录
- 双樾
- JS基础知识
- JS-WEB-API
- 开发环境
- 运行环境
- ES6
- 原型
- 异步
- 虚拟dom
- mvvm
- 组件化和React
- hybrid
- 其他
- 补充
- 技巧
- 快乐动起来呀
- css
- 掘金小册子
- js基础知识
- ES6知识点
- JS异步
- JS进阶知识
- 思考题
- DevTools Tips
- 浏览器基础知识
- 浏览器缓存机制0
- 浏览器渲染原理
- 安全防范知识点0
- 从V8中看JS性能优化0
- 性能优化琐碎事
- Webpack性能优化0
- 实现小型打包工具0
- React和Vue
- Vue生命周期
- vue基础知识点
- Vue响应式
- vue高级
- React基础
- Vue.js技术解密
- 准备工作
- 数据驱动
- new Vue()
- vue实例挂载
- 组件化
- 深入响应式原理
- 编译
- 扩展
- Vue Router
- Vuex