vue源码解析
源码结构
├─dist # 项目构建后的文件
├─scripts # 与项目构建相关的脚本和配置文件
├─flow # flow的类型声明文件
├─src # 项目源代码
│ ├─complier # 与模板编译相关的代码
│ ├─core # 通用的、与运行平台无关的运行时代码
│ │ ├─observe # 实现变化侦测的代码
│ │ ├─vdom # 实现virtual dom的代码
│ │ ├─instance # Vue.js实例的构造函数和原型方法
│ │ ├─global-api # 全局api的代码
│ │ └─components # 内置组件的代码
│ ├─server # 与服务端渲染相关的代码
│ ├─platforms # 特定运行平台的代码,如weex
│ ├─sfc # 单文件组件的解析代码
│ └─shared # 项目公用的工具代码
└─test # 项目测试代码
vue的前置知识点
1.Flow基础语法
2.原型与原型继承
3.Object.defineProperty
4.Vnode概念
5.函数柯里化
6.Macrotask与Microtask
7.递归编程算法
8.编译原理基础知识
1.变化侦测篇
学习Vue中如何实现数据的响应式系统,从而达到数据驱动视图。
-
1.1Object变化侦听
-
1.1.1图示如下:
1).Data通过observer转换成了getter/setter的形式来追踪变化。
2).当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
3).当数据发生了变化时,会触发setter,从而向Dep中的依赖(即Watcher)发送通知。
4).Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
-
1.1.2不足之处:
虽然我们通过Object.d efineProperty方法实现了对object数据的可观测,但是这个方法仅仅只能观测到object数据的取值及设置值,当我们向object数据里添加一对新的key/value或删除一对已有的key/value时,它是无法观测到的,导致当我们对object数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。 当然,Vue也注意到了这一点,为了解决这一问题,Vue增加了两个全局API:Vue.set和Vue.delete
-
1.2Array变化侦听
-
1.2.1vue数组原理
在Array原型链上定义一个新方法,然后内部调用了原生push方法,这样就保证了新的newPush方法跟原生push方法具有相同的功能,而且我们还可以在新的newPush方法内部干一些别的事情,比如通知变化。
let arr = [1,2,3]
arr.push(4)
Array.prototype.newPush = function(val){
console.log('arr被修改了')
this.push(val)
}
arr.newPush(4)
-
1.2.1图示如下:
数组里添加了拦截器,来进行了一些自定义操作
// 源码地址:node_modules/vue/src/core/observer/array.js
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import { def } from '../util/index'
const arrayProto = Array.prototype
// 创建一个对象作为拦截器
export const arrayMethods = Object.create(arrayProto)
// 改变数组自身内容的7个方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/**
* Intercept mutating methods and emit events
*/
methodsToPatch.forEach(function (method) {
// 缓存原生方法
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 侦听改变
ob.dep.notify()
return result
})
})
-
1.2.2不足之处:
对于数组变化侦测是通过拦截器实现的,也就是说只要是通过数组原型上的方法(push,pop,shift,unshift,splice,sort,reverse)对数组进行操作就都可以侦测到,但是如果直接通过索引添加删除修改元素的时候,就侦听不到了
2.虚拟DOM篇
学习什么是虚拟DOM,以及Vue中的DOM-Diff原理
-
2.1源码
// 源码位置:node_modules/vue/src/core/vdom/vnode.js
/* @flow */
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
// strictly internal
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?
asyncFactory: Function | void; // async component factory function
asyncMeta: Object | void;
isAsyncPlaceholder: boolean;
ssrContext: Object | void;
fnContext: Component | void; // real context vm for functional nodes
fnOptions: ?ComponentOptions; // for SSR caching
devtoolsMeta: ?Object; // used to store functional render context for devtools
fnScopeId: ?string; // functional scope id support
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions,
asyncFactory?: Function
) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm
this.ns = undefined
this.context = context
this.fnContext = undefined
this.fnOptions = undefined
this.fnScopeId = undefined
this.key = data && data.key
this.componentOptions = componentOptions
this.componentInstance = undefined
this.parent = undefined
this.raw = false
this.isStatic = false
this.isRootInsert = true
this.isComment = false
this.isCloned = false
this.isOnce = false
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false
}
// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next */
get child (): Component | void {
return this.componentInstance
}
}
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
export function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
// #7975
// clone children array to avoid mutating original in case of cloning
// a child.
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
)
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
cloned.isComment = vnode.isComment
cloned.fnContext = vnode.fnContext
cloned.fnOptions = vnode.fnOptions
cloned.fnScopeId = vnode.fnScopeId
cloned.asyncMeta = vnode.asyncMeta
cloned.isCloned = true
return cloned
}
-
2.2Vnode类型
-
2.2.1注释节点
// 创建注释节点
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
从上面代码中可以看到,描述一个注释节点只需两个属性,分别是:text和isComment。其中text属性表示具体的注释信息,isComment是一个标志,用来标识一个节点是否是注释节点。
-
2.2.2克隆节点
// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
export function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
// #7975
// clone children array to avoid mutating original in case of cloning
// a child.
vnode.children && vnode.children.slice(),
vnode.text,
vnode.elm,
vnode.context,
vnode.componentOptions,
vnode.asyncFactory
)
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
cloned.isComment = vnode.isComment
cloned.fnContext = vnode.fnContext
cloned.fnOptions = vnode.fnOptions
cloned.fnScopeId = vnode.fnScopeId
cloned.asyncMeta = vnode.asyncMeta
cloned.isCloned = true
return cloned
}
从上面代码中可以看到,克隆节点就是把已有节点的属性全部复制到新节点中,而现有节点和新克隆得到的节点之间唯一的不同就是克隆得到的节点isCloned为true
-
2.2.3 文本节点
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
-
2.2.4元素节点
// 真实DOM节点
<div id='a'><span>难凉热血</span></div>
// VNode节点
{
tag:'div',
data:{},
children:[
{
tag:'span',
text:'难凉热血'
}
]
}
我们可以看到,真实DOM节点中:div标签里面包含了一个span标签,而span标签里面有一段文本。反应到VNode节点上就如上所示:tag表示标签名,data表示标签的属性id等,children表示子节点数组。
-
2.2.5组件节点
组件节点除了有元素节点具有的属性之外,它还有两个特有的属性:
-
componentOptions :组件的option选项,如组件的props等
-
componentInstance :当前组件节点对应的Vue实例
-
2.2.6函数式组件节点
函数式组件节点相较于组件节点,它又有两个特有的属性:
-
fnContext:函数式组件对应的Vue实例
-
fnOptions: 组件的option选项
-
2.3 VNode的作用
说了这么多,那么VNode在Vue的整个虚拟DOM过程起了什么作用呢?
其实VNode的作用是相当大的。我们在视图渲染之前,把写好的template模板先编译成VNode并缓存下来,等到数据发生变化页面需要重新渲染的时候,我们把数据发生变化后生成的VNode与前一次缓存下来的VNode进行对比,找出差异,然后有差异的VNode对应的真实DOM节点就是需要重新渲染的节点,最后根据有差异的VNode创建出真实的DOM节点再插入到视图中,最终完成一次视图更新
-
2.4DOM-diff
在Vue中,把 DOM-Diff过程叫做patch过程。patch,意为“补丁”,即指对旧的VNode修补,打补丁从而得到新的VNode,整个patch无非就是干三件事:
-
创建节点:新的VNode中有而旧的oldVNode中没有,就在旧的oldVNode中创建。
// 源码位置: /src/core/vdom/patch.js
function createElm (vnode, parentElm, refElm) {
const data = vnode.data
const children = vnode.children
const tag = vnode.tag
if (isDef(tag)) {
vnode.elm = nodeOps.createElement(tag, vnode) // 创建元素节点
createChildren(vnode, children, insertedVnodeQueue) // 创建元素节点的子节点
insert(parentElm, vnode.elm, refElm) // 插入到DOM中
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text) // 创建注释节点
insert(parentElm, vnode.elm, refElm) // 插入到DOM中
} else {
vnode.elm = nodeOps.createTextNode(vnode.text) // 创建文本节点
insert(parentElm, vnode.elm, refElm) // 插入到DOM中
}
}
从上面代码中,我们可以看出:
判断是否为元素节点只需判断该VNode节点是否有tag标签即可。如果有tag属性即认为是元素节点,则调用createElement方法创建元素节点,通常元素节点还会有子节点,那就递归遍历创建所有子节点,将所有子节点创建好之后insert插入到当前元素节点里面,最后把当前元素节点插入到DOM中。 判断是否为注释节点,只需判断VNode的isComment属性是否为true即可,若为true则为注释节点,则调用createComment方法创建注释节点,再插入到DOM中。 如果既不是元素节点,也不是注释节点,那就认为是文本节点,则调用createTextNode方法创建文本节点,再插入到DOM中。
-
删除节点:新的VNode中没有而旧的oldVNode中有,就从旧的oldVNode中删除。
function removeNode (el) {
const parent = nodeOps.parentNode(el) // 获取父节点
if (isDef(parent)) {
nodeOps.removeChild(parent, el) // 调用父节点的removeChild方法
}
}
如果某些节点再新的VNode中没有而在旧的oldVNode中有,那么就需要把这些节点从旧的oldVNode中删除。删除节点非常简单,只需在要删除节点的父元素上调用removeChild方法即可
-
更新节点:新的VNode和旧的oldVNode中都有,就以新的VNode为准,更新旧的oldVNode。
// 更新节点
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// vnode与oldVnode是否完全一样?若是,退出程序
if (oldVnode === vnode) {
return
}
const elm = vnode.elm = oldVnode.elm
// vnode与oldVnode是否都是静态节点?若是,退出程序
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
return
}
const oldCh = oldVnode.children
const ch = vnode.children
// vnode有text属性?若没有:
if (isUndef(vnode.text)) {
// vnode的子节点与oldVnode的子节点是否都存在?
if (isDef(oldCh) && isDef(ch)) {
// 若都存在,判断子节点是否相同,不同则更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
}
// 若只有vnode的子节点存在
else if (isDef(ch)) {
/**
* 判断oldVnode是否有文本?
* 若没有,则把vnode的子节点添加到真实DOM中
* 若有,则清空Dom中的文本,再把vnode的子节点添加到真实DOM中
*/
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
}
// 若只有oldnode的子节点存在
else if (isDef(oldCh)) {
// 清空DOM中的子节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
// 若vnode和oldnode都没有子节点,但是oldnode中有文本
else if (isDef(oldVnode.text)) {
// 清空oldnode文本
nodeOps.setTextContent(elm, '')
}
// 上面两个判断一句话概括就是,如果vnode中既没有text,也没有子节点,那么对应的oldnode中有什么就清空什么
}
// 若有,vnode的text属性与oldVnode的text属性是否相同?
else if (oldVnode.text !== vnode.text) {
// 若不相同:则用vnode的text替换真实DOM的文本
nodeOps.setTextContent(elm, vnode.text)
}
}
-
优化更新节点
/ 循环更新子节点
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // oldChildren开始索引
let oldEndIdx = oldCh.length - 1 // oldChildren结束索引
let oldStartVnode = oldCh[0] // oldChildren中所有未处理节点中的第一个
let oldEndVnode = oldCh[oldEndIdx] // oldChildren中所有未处理节点中的最后一个
let newStartIdx = 0 // newChildren开始索引
let newEndIdx = newCh.length - 1 // newChildren结束索引
let newStartVnode = newCh[0] // newChildren中所有未处理节点中的第一个
let newEndVnode = newCh[newEndIdx] // newChildren中所有未处理节点中的最后一个
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
// 以"新前"、"新后"、"旧前"、"旧后"的方式开始比对节点
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // 如果oldStartVnode不存在,则直接跳过,比对下一个
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 如果新前与旧前节点相同,就把两个节点进行patch更新
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 如果新后与旧后节点相同,就把两个节点进行patch更新
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 如果新后与旧前节点相同,先把两个节点进行patch更新,然后把旧前节点移动到oldChilren中所有未处理节点之后
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 如果新前与旧后节点相同,先把两个节点进行patch更新,然后把旧后节点移动到oldChilren中所有未处理节点之前
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 如果不属于以上四种情况,就进行常规的循环比对patch
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果在oldChildren里找不到当前循环的newChildren里的子节点
if (isUndef(idxInOld)) { // New element
// 新增节点并插入到合适位置
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// 如果在oldChildren里找到了当前循环的newChildren里的子节点
vnodeToMove = oldCh[idxInOld]
// 如果两个节点相同
if (sameVnode(vnodeToMove, newStartVnode)) {
// 调用patchVnode更新节点
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
// canmove表示是否需要移动节点,如果为true表示需要移动,则移动节点,如果为false则不用移动
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
/**
* 如果oldChildren比newChildren先循环完毕,
* 那么newChildren里面剩余的节点都是需要新增的节点,
* 把[newStartIdx, newEndIdx]之间的所有节点都插入到DOM中
*/
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
/**
* 如果newChildren比oldChildren先循环完毕,
* 那么oldChildren里面剩余的节点都是需要删除的节点,
* 把[oldStartIdx, oldEndIdx]之间的所有节点都删除
*/
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
3.模板编译篇
学习Vue内部是怎么把template模板编译成虚拟DOM,从而渲染出真实DOM
-
3.1模板编译过程图解
4.生命周期篇
学习Vue中组件的生命周期实现原理
-
4.1生命周期图解
从图中我们可以看到,Vue实例的生命周期大致可分为4个阶段:
- 初始化阶段:为Vue实例上初始化一些属性,事件以及响应式数据;
- 模板编译阶段:将模板编译成渲染函数;
- 挂载阶段:将实例挂载到指定的DOM上,即将模板渲染到真实DOM中;
- 销毁阶段:将实例自身从父组件中删除,并取消依赖追踪及事件监听器
5.实例方法篇
学习Vue中所有实例方法(即所有以$开头的方法)的实现原理
-
5.1数据相关的方法
与数据相关的实例方法有3个,分别是vm.$set、vm.$delete和vm.$watch。它们是在stateMixin函数中挂载到Vue原型上的,代码如下:
import {set,del} from '../observer/index'
export function stateMixin (Vue) {
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function (expOrFn,cb,options) {}
}
-
5.2事件相关的方法
与事件相关的实例方法有4个,分别是vm.$on、vm.$emit、vm.$off和vm.$once。它们是在eventsMixin函数中挂载到Vue原型上的,
export function eventsMixin (Vue) {
Vue.prototype.$on = function (event, fn) {}
Vue.prototype.$once = function (event, fn) {}
Vue.prototype.$off = function (event, fn) {}
Vue.prototype.$emit = function (event) {}
}
-
5.3生命周期相关的方法
与生命周期相关的实例方法有4个,分别是vm.$mount、vm.$forceUpdate、vm.$nextTick和vm.$destory。其中,$forceUpdate和$destroy方法是在lifecycleMixin函数中挂载到Vue原型上的,$nextTick方法是在renderMixin函数中挂载到Vue原型上的,而$mount方法是在跨平台的代码中挂载到Vue原型上的
export function lifecycleMixin (Vue) {
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function (fn) {}
}
export function renderMixin (Vue) {
Vue.prototype.$nextTick = function (fn) {}
}
6.全局API篇
学习Vue中所有全局API的实现原理
与实例方法不同,实例方法是将方法挂载到Vue的原型上,而全局API是直接在Vue上挂载方法。在Vue中,全局API一共有12个,分别是Vue.extend、Vue.nextTick、Vue.set、Vue.delete、Vue.directive、Vue.filter、Vue.component、Vue.use、Vue.mixin、Vue.observable、Vue.version。这12个API中有的是我们在日常业务开发中经常会用到的,有的是对Vue内部或外部插件提供的,我们在日常业务开发中几乎用不到。接下来我们就对这12个API逐个进行分析,看看其内部原理都是怎样的
7.过滤器篇
学习Vue中所有过滤器的实现原理
8.指令篇
学习Vue中所有指令的实现原理
9.内置组件篇
学习Vue中内置组件的实现原理
来源:oschina
链接:https://my.oschina.net/qinghuo111/blog/4284106