## VUE原理 ### 响应式原理 **检测data变化的核心API`Object.defindeProperty`** 基本使用 ```javascript 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 ``` ### 虚拟DOOM -- diff算法 1. 通过树进行比较 `diff` 算法用来比较两棵 `Virtual DOM` 树的差异,如果需要两棵树的完全比较,那么 `diff` 算法的时间复杂度为`O(n^3)`。但是在前端当中,你很少会跨越层级地移动 `DOM` 元素,所以 `Virtual DOM` 只会对同一个层级的元素进行对比,如下图所示, `div` 只会和同一层级的 `div` 对比,第二层级的只会跟第二层级对比,这样算法复杂度就可以达到 `O(n)`。 ![img](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/7/23/16c1e26a5ecf086e~tplv-t2oaga2asx-jj-mark:3024:0:0:0:q75.awebp) 2. 通过列表进行比较 ​ 子节点的对比算法,例如 `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源码 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 部分: * 用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象; * diff 算法 — 比较两棵虚拟 DOM 树的差异; * pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。 ### 构建过程 1. 初始化vue ```javascript 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) } } ``` 2. `Vue` 实例挂载 ```javascript 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 } ``` 3. 创建虚拟node ```javascript // 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 | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array { // 省略一系列非主线代码 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 数组; ``` ### diff过程 ```javascript // 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` 没有,那么就清空这个文本节点。 ### diff过程 --- 无key 1. 首先从第一个节点开始比较,不管是 `oldCh` 还是 `newCh` 的起始或者终止节点都不存在 `sameVnode` ,同时节点属性中是不带 `key`标记的,因此第一轮的 `diff` 完后,`newCh`的 `startVnode` 被添加到 `oldStartVnode`的前面,同时 `newStartIndex`前移一位; ![图片描述](assets/16c1e0e2878c44dctplv-t2oaga2asx-jj-mark3024000q75.awebp) 2. 第二轮的 `diff`中,满足 `sameVnode(oldStartVnode, newStartVnode)`,因此对这2个 `vnode` 进行`diff`,最后将 `patch` 打到 `oldStartVnode` 上,同时 `oldStartVnode`和 `newStartIndex` 都向前移动一位 ![图片描述](assets/16c1e0e28889eafftplv-t2oaga2asx-jj-mark3024000q75.awebp) 3. 第三轮的 `diff` 中,满足 `sameVnode(oldEndVnode, newStartVnode)`,那么首先对 `oldEndVnode`和`newStartVnode` 进行 `diff`,并对 `oldEndVnode`进行 `patch`,并完成 `oldEndVnode` 移位的操作,最后`newStartIndex`前移一位,`oldStartVnode` 后移一位; ![图片描述](assets/16c1e0e289a351b2tplv-t2oaga2asx-jj-mark3024000q75.awebp) 4. 第四轮的 `diff`中,过程同步骤3; ![图片描述](assets/16c1e0e289f9213etplv-t2oaga2asx-jj-mark3024000q75.awebp) 5. 第五轮的 `diff` 中,同过程1; ![图片描述](assets/16c1e0e28aee99a1tplv-t2oaga2asx-jj-mark3024000q75.awebp) 6. 遍历的过程结束后,`newStartIdx > newEndIdx`,说明此时 `oldCh` 存在多余的节点,那么最后就需要将这些多余的节点删除。 ![图片描述](assets/16c1e0e2ca893b49tplv-t2oaga2asx-jj-mark3024000q75.awebp) ### diff过程 -- 有key 1. 首先从第一个节点开始比较,不管是 `oldCh` 还是 `newCh` 的起始或者终止节点都不存在 `sameVnode`,但节点属性中是带 `key` 标记的, 然后在 `oldKeyToIndx` 中找到对应的节点,这样第一轮 `diff` 过后 `oldCh` 上的`B节点`被删除了,但是 `newCh` 上的`B节点`上 `elm` 属性保持对 `oldCh` 上 `B节点` 的`elm`引用。 ![图片描述](assets/16c1e0e2db1c4812tplv-t2oaga2asx-jj-mark3024000q75-16934721993069.awebp) 2. 第二轮的 `diff` 中,满足 `sameVnode(oldStartVnode, newStartVnode)`,因此对这2个 `vnode` 进行`diff`,最后将 `patch` 打到 `oldStartVnode`上,同时 `oldStartVnode` 和 `newStartIndex` 都向前移动一位 ![图片描述](assets/16c1e0e2d7df4fbftplv-t2oaga2asx-jj-mark3024000q75.awebp) 3. 第三轮的 `diff`中,满足 `sameVnode(oldEndVnode, newStartVnode)`,那么首先对 `oldEndVnode` 和`newStartVnode` 进行 `diff`,并对 `oldEndVnode` 进行 `patch`,并完成 `oldEndVnode` 移位的操作,最后`newStartIndex` 前移一位,`oldStartVnode`后移一位; ![图片描述](assets/16c1e0e2e2a2835etplv-t2oaga2asx-jj-mark3024000q75.awebp) 4. 第四轮的`diff`中,过程同步骤2; ![图片描述](assets/16c1e0e2e507aec0tplv-t2oaga2asx-jj-mark3024000q75.awebp) 5. 第五轮的`diff`中,因为此时 `oldStartIndex` 已经大于 `oldEndIndex`,所以将剩余的 `Vnode` 队列插入队列最后。 ![图片描述](assets/16c1e0e3178398fctplv-t2oaga2asx-jj-mark3024000q75.awebp) ## vue生命周期 ![在这里插入图片描述](assets/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3N1MjIzMTU5NTc0Mg==,size_16,color_FFFFFF,t_70-169347279895916.png) 生命周期: vue实例从创建到销毁的过程。 声明周期钩子: 就是生命周期事件的别名而已 主要的生命周期函数分类: * 创建期间的生命周期函数: * beforeCreate:实例刚在内存中被创建出来,此时,还没有初始化好data 和 methods 属性 * created:实例已经完成了模板的编译,但是还没有挂载到页面中 * beforeMount:此时已经完成了模板的翻译,但是还有完全挂载到页面中 * mounted:此时,已经将编译好的模板,挂载到了页面指定的容器中显示 * 运行期间的生命周期函数: * beforeUpdate:状态更新之前执行此函数,此时 data 中的状态值是最新的,但是界面上显示的数据还是旧的,因为此时还没有开始重新渲染DOM节点 * updated:实例更新完毕之后调用次函数,此时 data 中的状态值 和 界面上显示的数据,都已经完成了更新,界面已经被重新渲染好了 * 销毁期间的生命周期函数: * beforeDestroy:实例销毁之前调用,在这一步,实例仍然完全可用 当执行 beforeDestroy 钩子函数的时候,Vue实例就已经从运行阶段进入到了销毁阶段;当执行 beforeDestroy 的时候,实例身上所有的 data 和所有的 methods, 以及 过滤器、指令、、 都处于可用状态,此时,还没有真正执行销毁的过程 * destroyed:Vue 实例销毁后调用。调用后,vue 实例 指示的所有东西都会解绑,所有的事件监听器会被移除,所有的子实例也会被销毁