检测data变化的核心APIObject.defindeProperty
基本使用
const data = {};
let name = "张三";
Object.defineProperty(data,'name',{
get:function(){
console.log('触发get')
return name
},
set:function(newVal){
console.log('触发set')
name=newVal
}
})
//测试
console.log(data.name) // 触发get 张三
data.name = '李四' // 触发set
diff
算法用来比较两棵 Virtual DOM
树的差异,如果需要两棵树的完全比较,那么 diff
算法的时间复杂度为O(n^3)
。但是在前端当中,你很少会跨越层级地移动 DOM
元素,所以 Virtual DOM
只会对同一个层级的元素进行对比,如下图所示, div
只会和同一层级的 div
对比,第二层级的只会跟第二层级对比,这样算法复杂度就可以达到 O(n)
。
子节点的对比算法,例如 p, ul, div
的顺序换成了 div, p, ul
。这个该怎么对比?如果按照同层级进行顺序对比的话,它们都会被替换掉。如 p
和 div
的 tagName
不同,p
会被 div
所替代。最终,三个节点都会被替换,这样 DOM
开销就非常大。而实际上是不需要替换节点,而只需要经过节点移动就可以达到,我们只需知道怎么进行移动。
将这个问题抽象出来其实就是字符串的最小编辑距离问题(Edition Distance
),最常见的解决方法是 Levenshtein Distance
, Levenshtein Distance
是一个度量两个字符序列间差异的字符串度量标准,两个单词之间的 Levenshtein Distance
是将一个单词转换为另一个单词所需的单字符编辑(插入、删除或替换)的最小数量。Levenshtein Distance
是1965年由苏联数学家 Vladimir Levenshtein 发明的。Levenshtein Distance
也被称为编辑距离(Edit Distance
),通过动态规划求解,时间复杂度为 O(M*N)
。
vue的virtual dom借鉴了开源库snabbdom,其实主要属性
tag
属性即这个vnode
的标签属性data
属性包含了最后渲染成真实dom
节点后,节点上的class
,attribute
,style
以及绑定的事件children
属性是vnode
的子节点text
属性是文本属性elm
属性为这个vnode
对应的真实dom
节点key
属性是vnode
的标记,在diff
过程中可以提高diff
的效率虚拟dom原理流程
模板 ==> 渲染函数 ==> 虚拟DOM树 ==> 真实DOM
vuejs通过编译将模板(template)转成渲染函数(render),执行渲染函数可以得到一个虚拟节点树
在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。
虚拟 DOM 的实现原理主要包括以下 3 部分:
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)
}
// 通过查看 Vue 的 function,我们知道 Vue 只能通过 new 关键字初始化,然后调用 this._init 方法,该方法在 src/core/instance/init.js 中定义。
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// 省略一系列其它初始化的代码
if (vm.$options.el) {
console.log('vm.$options.el:',vm.$options.el);
vm.$mount(vm.$options.el)
}
}
Vue
实例挂载const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
// 省略一系列初始化以及逻辑判断代码
return mount.call(this, el, hydrating)
}
// 我们发现最终还是调用用原先原型上的 $mount 方法挂载 ,原先原型上的 $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)
}
// 我们发现$mount 方法实际上会去调用 mountComponent 方法,这个方法定义在 src/core/instance/lifecycle.js 文件中
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 省略一系列其它代码
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// 生成虚拟 vnode
const vnode = vm._render()
// 更新 DOM
vm._update(vnode, hydrating)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
return vm
}
// Vue 的 _render 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node。它的定义在 src/core/instance/render.js 文件中:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
let vnode
try {
// 省略一系列代码
currentRenderingInstance = vm
// 调用 createElement 方法来返回 vnode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`){}
}
// set parent
vnode.parent = _parentVnode
console.log("vnode...:",vnode);
return vnode
}
// Vue.js 利用 _createElement 方法创建 VNode,它定义在 src/core/vdom/create-elemenet.js 中:
export function _createElement (
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
// 省略一系列非主线代码
if (normalizationType === ALWAYS_NORMALIZE) {
// 场景是 render 函数不是编译生成的
children = normalizeChildren(children)
} else if (normalizationType === SIMPLE_NORMALIZE) {
// 场景是 render 函数是编译生成的
children = simpleNormalizeChildren(children)
}
let vnode, ns
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// 创建虚拟 vnode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
vnode = createComponent(tag, data, context, children)
}
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
if (isDef(ns)) applyNS(vnode, ns)
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
}
// _createElement 方法有 5 个参数,context 表示 VNode 的上下文环境,它是 Component 类型;tag表示标签,它可以是一个字符串,也可以是一个 Component;data 表示 VNode 的数据,它是一个 VNodeData 类型,可以在 flow/vnode.js 中找到它的定义;children 表示当前 VNode 的子节点,它是任意类型的,需要被规范为标准的 VNode 数组;
// Vue.js 源码实例化了一个 watcher,这个 ~ 被添加到了在模板当中所绑定变量的依赖当中,一旦 model 中的响应式的数据发生了变化,这些响应式的数据所维护的 dep 数组便会调用 dep.notify() 方法完成所有依赖遍历执行的工作,这包括视图的更新,即 updateComponent 方法的调用。watcher 和 updateComponent方法定义在 src/core/instance/lifecycle.js 文件中 。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 省略一系列其它代码
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// 生成虚拟 vnode
const vnode = vm._render()
// 更新 DOM
vm._update(vnode, hydrating)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
return vm
}
// 完成视图的更新工作事实上就是调用了vm._update方法,这个方法接收的第一个参数是刚生成的Vnode,调用的vm._update方法定义在 src/core/instance/lifecycle.js中。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
if (!prevVnode) {
// 第一个参数为真实的node节点,则为初始化
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 如果需要diff的prevVnode存在,那么对prevVnode和vnode进行diff
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
}
// 在这个方法当中最为关键的就是 vm.__patch__ 方法,这也是整个 virtual-dom 当中最为核心的方法,主要完成了prevVnode 和 vnode 的 diff 过程并根据需要操作的 vdom 节点打 patch,最后生成新的真实 dom 节点并完成视图的更新工作。接下来,让我们看下 vm.__patch__的逻辑过程, vm.__patch__ 方法定义在 src/core/vdom/patch.js 中。
function patch (oldVnode, vnode, hydrating, removeOnly) {
......
if (isUndef(oldVnode)) {
// 当oldVnode不存在时,创建新的节点
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 对oldVnode和vnode进行diff,并对oldVnode打patch
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
......
}
}
// 在 patch 方法中,我们看到会分为两种情况,一种是当 oldVnode 不存在时,会创建新的节点;另一种则是已经存在 oldVnode ,那么会对 oldVnode 和 vnode 进行 diff 及 patch 的过程。其中 patch 过程中会调用 sameVnode 方法来对对传入的2个 vnode 进行基本属性的比较,只有当基本属性相同的情况下才认为这个2个vnode 只是局部发生了更新,然后才会对这2个 vnode 进行 diff,如果2个 vnode 的基本属性存在不一致的情况,那么就会直接跳过 diff 的过程,进而依据 vnode 新建一个真实的 dom,同时删除老的 dom节点。
function sameVnode (a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
}
// diff 过程中主要是通过调用 patchVnode 方法进行的:
function patchVnode (oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly) {
......
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
// 如果vnode没有文本节点
if (isUndef(vnode.text)) {
// 如果oldVnode的children属性存在且vnode的children属性也存在
if (isDef(oldCh) && isDef(ch)) {
// updateChildren,对子节点进行diff
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
// 如果oldVnode的text存在,那么首先清空text的内容,然后将vnode的children添加进去
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 删除elm下的oldchildren
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// oldVnode有子节点,而vnode没有,那么就清空这个节点
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 如果oldVnode和vnode文本属性不同,那么直接更新真是dom节点的文本元素
nodeOps.setTextContent(elm, vnode.text)
}
......
}
从以上代码得知,
diff
过程中又分了好几种情况,oldCh
为 oldVnode
的子节点,ch
为 Vnode
的子节点:
oldVnode.text !== vnode.text
,那么就会直接进行文本节点的替换;vnode
没有文本节点的情况下,进入子节点的 diff
;oldCh
和 ch
都存在且不相同的情况下,调用 updateChildren
对子节点进行 diff
;oldCh
不存在,ch
存在,首先清空 oldVnode
的文本节点,同时调用 addVnodes
方法将 ch
添加到elm
真实 dom
节点当中;oldCh
存在,ch
不存在,则删除 elm
真实节点下的 oldCh
子节点;oldVnode
有文本节点,而 vnode
没有,那么就清空这个文本节点。oldCh
还是 newCh
的起始或者终止节点都不存在 sameVnode
,同时节点属性中是不带 key
标记的,因此第一轮的 diff
完后,newCh
的 startVnode
被添加到 oldStartVnode
的前面,同时 newStartIndex
前移一位;diff
中,满足 sameVnode(oldStartVnode, newStartVnode)
,因此对这2个 vnode
进行diff
,最后将 patch
打到 oldStartVnode
上,同时 oldStartVnode
和 newStartIndex
都向前移动一位diff
中,满足 sameVnode(oldEndVnode, newStartVnode)
,那么首先对 oldEndVnode
和newStartVnode
进行 diff
,并对 oldEndVnode
进行 patch
,并完成 oldEndVnode
移位的操作,最后newStartIndex
前移一位,oldStartVnode
后移一位;diff
中,过程同步骤3;diff
中,同过程1;newStartIdx > newEndIdx
,说明此时 oldCh
存在多余的节点,那么最后就需要将这些多余的节点删除。oldCh
还是 newCh
的起始或者终止节点都不存在 sameVnode
,但节点属性中是带 key
标记的, 然后在 oldKeyToIndx
中找到对应的节点,这样第一轮 diff
过后 oldCh
上的B节点
被删除了,但是 newCh
上的B节点
上 elm
属性保持对 oldCh
上 B节点
的elm
引用。diff
中,满足 sameVnode(oldStartVnode, newStartVnode)
,因此对这2个 vnode
进行diff
,最后将 patch
打到 oldStartVnode
上,同时 oldStartVnode
和 newStartIndex
都向前移动一位diff
中,满足 sameVnode(oldEndVnode, newStartVnode)
,那么首先对 oldEndVnode
和newStartVnode
进行 diff
,并对 oldEndVnode
进行 patch
,并完成 oldEndVnode
移位的操作,最后newStartIndex
前移一位,oldStartVnode
后移一位;diff
中,过程同步骤2;diff
中,因为此时 oldStartIndex
已经大于 oldEndIndex
,所以将剩余的 Vnode
队列插入队列最后。生命周期: vue实例从创建到销毁的过程。
声明周期钩子: 就是生命周期事件的别名而已
主要的生命周期函数分类: