Vue 原理
其他扩展:vue面试中常见的面试题
原理的意义
知其然知其所以然——各行业通用的道理
了解原理,才能应用的更好
大厂造轮子(业务定制、技术 KPI)
如何考察
考察重点,而不是考察细节。掌握好 2/8 原则
和使用相关联的原理,例如:vdom、模板渲染
整体流程是否全面?热门技术是否有深度?
重要的原理
组件化
响应式
vdom 和 diff
模板编译
渲染过程
前端路由
原理题
为何 v-for 中要用 key
描述组件渲染和更新的过程
双向数据绑定 v-model 的实现原理
如何理解 MVVM
“很久以前” 的组件化
asp、jsp、php 已经有组件化了
nodejs 中也有类似的组件化
数据驱动视图(MVVM、setState)
传统组件,只是静态渲染,更新还要依赖于操作 DOM
数据驱动视图——Vue MVVM
数据驱动视图——React setState
Vue 响应式(数据拦截)
推荐文章:简单手写实现Vue2.x
组件 data 数据一旦变化,立刻触发视图的更新
实现数据驱动视图的第一步
数据响应式原理
源码级别可以参考: Vue.js 技术揭秘——深入响应式原理
在 JavaScript 的对象 Object 中有一个属性叫访问器属性,其中有 [[Get]]
和 [[Set]]
特性,它们分别是获取函数和设置函数
核心 API :Object.defineProperty
问题
深度监听,需要递归到底,一次性计算量大
无法监听新增属性/删除属性(Vue.set
、Vue.delete
)
不能监听数组变化(重新定义原型,重写 push
、pop
等方法)
Object.defineProperty
实现响应式
observe
的功能:给非 VNode 的对象类型数据添加一个 Observer
,用来监听数据的变化
Observer
的作用:给对象的属性添加 getter 和 setter,用来依赖收集和派发更新。对于数组会调用 observeArray
方法,对于对象会对对象的 key 调用 defineReactive
方法
defineReactive
的功能:定义一个响应式对象,给对象动态添加 getter 和 setter。对子对象递归调用 observe
方法,这样就保证了无论 obj
的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj
中一个嵌套较深的属性,也能触发 getter 和 setter
setter 的时候,会通知所有的订阅者
arrayMethods
首先继承了 Array
,然后对数组中的所有能改变数组自身的方法进行重写,重写后的方法会先执行它们本身原有的逻辑,然后把新添加的值
简单实现 Vue 中的 defineReactive
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 function updateView ( ) { console .log ('视图更新' ) } const arrayProto = Array .prototype const arrayMethods = Object .create (arrayProto);['push' , 'pop' , 'shift' , 'unshift' , 'splice' , 'sort' , 'reverse' ].forEach (method => { arrayMethods[method] = function ( ) { arrayProto[method].call (this , ...arguments ) updateView () } }) function defineReactive (target, key, value ) { observer (value) const property = Object .getOwnPropertyDescriptor (target, key) if (property && property.configurable === false ) return Object .defineProperty (target, key, { get ( ) { return value }, set (newValue ) { if (newValue !== value) { observer (newValue) value = newValue updateView () } }, }) } function observer (target ) { if (typeof target !== 'object' || target === null ) { return target } if (Array .isArray (target)) { target.__proto__ = arrayMethods } for (let key in target) { if (!Object .hasOwnProperty .call (target, key)) return defineReactive (target, key, target[key]) } } const data = { name : 'zhangsan' , age : 20 , info : { address : '北京' , }, nums : [10 , 20 , 30 ], } observer (data)data.name = 'lisi' data.age = 21 data.info .address = '上海' data.nums .push (4 )
虚拟 DOM
DOM 操作非常耗费性能
以前用 jQuery,可以自行控制 DOM 操作的时机,手动调整
Vue 和 React 是数据驱动视图,如何有效控制 DOM 操作?
vdom
有了一定复杂度,想减少计算次数比较难
能不能把计算,更多的转移为 JS 计算?因为 JS 执行速度很快
vdom——用 JS 模拟 DOM 结构,计算出最小的变更,操作 DOM
用 JS 模拟 DOM 结构
1 2 3 4 5 6 <div id ="div1" class ="container" > <p > vdom</p > <ul style ="font-size: 20px;" > <li > a</li > </ul > </div >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 { tag : 'div' , props : { className : 'container' , id : 'div1' , }, children : [ { tag : 'p' , children : 'dom' , }, { tag : 'ul' , props : { style : 'font-size: 20px' }, children : [{ tag : 'li' , children : 'a' }], }, ], }
1 2 3 <div id ="app" > <p class ="text" > HelloWorld</p > </div >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 { "tag" : "div" , "key" : undefined , "elm" : div#app, "text" : undefined , "data" : {attrs : {id :"app" }}, "children" : [{ "tag" : "p" , "key" : undefined , "elm" : p.text , "text" : undefined , "data" : {attrs : {class : "text" }}, "children" : [{ "tag" : undefined , "key" : undefined , "elm" : text, "text" : "helloWorld" , "data" : undefined , "children" : [] }] }] }
snabbdom 使用
通过 snabbdom 学习 vdom
核心概念:h、vnode、patch、diff、key 等
vdom 价值:数据驱动视图,控制 DOM 操作
patch(elem, vnode)
和 patch(vnode, newVnode)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <!DOCTYPE html > <html > <body > <div id ="container" > </div > <button id ="btn-change" > change</button > </body > </html > <script src ="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js" > </script > <script src ="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js" > </script > <script src ="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js" > </script > <script src ="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js" > </script > <script src ="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js" > </script > <script src ="https://cdn.bootcss.com/snabbdom/0.7.3/h.js" > </script > <script > const snabbdom = window .snabbdom const patch = snabbdom.init ([ snabbdom_class, snabbdom_props, snabbdom_style, snabbdom_eventlisteners, ]) const h = snabbdom.h const container = document .getElementById ('container' ) const vnode = h ('ul#list' , {}, [h ('li.item' , {}, 'Item 1' ), h ('li.item' , {}, 'Item 2' )]) patch (container, vnode) document .getElementById ('btn-change' ).addEventListener ('click' , () => { const newVnode = h ('ul#list' , {}, [ h ('li.item' , {}, 'Item 1' ), h ('li.item' , {}, 'Item B' ), h ('li.item' , {}, 'Item 3' ), ]) patch (vnode, newVnode) }) </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 <!DOCTYPE html > <html > <body > <div id ="container" > </div > <button id ="btn-change" > change</button > </body > </html > <script type ="text/javascript" src ="https://cdn.bootcss.com/jquery/3.2.0/jquery.js" > </script > <script type ="text/javascript" > const data = [ { name : '张三' , age : '20' , address : '北京' , }, { name : '李四' , age : '21' , address : '上海' , }, { name : '王五' , age : '22' , address : '广州' , }, ] function render (data ) { const $container = $('#container' ) $container.html ('' ) const $table = $('<table>' ) $table.append ($('<tr><td>name</td><td>age</td><td>address</td>/tr>' )) data.forEach (item => { $table.append ( $( '<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address + '</td>/tr>' ) ) }) $container.append ($table) } $('#btn-change' ).click (() => { data[1 ].age = 30 data[2 ].address = '深圳' render (data) }) render (data) </script >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 <!DOCTYPE html > <html > <body > <div id ="container" > </div > <button id ="btn-change" > change</button > </body > </html > <script src ="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js" > </script > <script src ="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js" > </script > <script src ="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js" > </script > <script src ="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js" > </script > <script src ="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js" > </script > <script src ="https://cdn.bootcss.com/snabbdom/0.7.3/h.js" > </script > <script type ="text/javascript" > const snabbdom = window .snabbdom const patch = snabbdom.init ([ snabbdom_class, snabbdom_props, snabbdom_style, snabbdom_eventlisteners, ]) const h = snabbdom.h const data = [ { name : '张三' , age : '20' , address : '北京' , }, { name : '李四' , age : '21' , address : '上海' , }, { name : '王五' , age : '22' , address : '广州' , }, ] data.unshift ({ name : '姓名' , age : '年龄' , address : '地址' , }) const container = document .getElementById ('container' ) let vnode function render (data ) { const newVnode = h ( 'table' , {}, data.map (item => { const tds = [] for (let i in item) { if (item.hasOwnProperty (i)) { tds.push (h ('td' , {}, item[i] + '' )) } } return h ('tr' , {}, tds) }) ) if (vnode) { patch (vnode, newVnode) } else { patch (container, newVnode) } vnode = newVnode } render (data) const btnChange = document .getElementById ('btn-change' ) btnChange.addEventListener ('click' , () => { data[1 ].age = 30 data[2 ].address = '深圳' render (data) }) </script >
diff 算法
推荐文章:图文并茂地来详细讲讲Vue Diff算法
diff 算法是 vdom 中最核心、最关键的部分
diff 算法能在日常使用 Vue、React 中体现出来(如:key)
diff 算法概述:
diff 即对比。是一个广泛的概念,如 linux diff 命令、git diff 等
两个 js 对象也可以做 diff,**jiff **
两棵树做 diff,如这里的 vdom diff
树 diff 的时间复杂度 O(n^3)
对于旧树上的点 E 来说,它要和新树上的所有点比较,复杂度为 O(n)
点 E 在新树上没有找到,点 E 会被删除,然后遍历新树上的所有点找到对应点(X)去填空,复杂度增加到 O(n^2)
这样的操作会在旧树的每个点进行,最终复杂度为 O(n^3)
1000 个节点,要计算 1 亿次,算法不可用
优化时间复杂度到 O(n)
只比较同一层级,不跨级比较
tag 不相同,则直接删掉重建,不再深度比较
tag 和 key,两者都相同,则认为是相同节点,不再深度比较
snabbdom 源码
snabbdom
1 2 3 4 5 6 7 8 9 10 11 12 export function h (sel: string ): VNode ;export function h (sel: string, data: VNodeData | null ): VNode ;export function h (sel: string, children: VNodeChildren ): VNode ;export function h ( sel: string, data: VNodeData | null , children: VNodeChildren ): VNode ;export function h (sel: any, b?: any, c?: any ): VNode { return vnode (sel, data, children, text, undefined ); }
src\vnode.ts
最后返回一个 JS 对象
1 2 3 4 5 6 7 8 9 10 export function vnode ( sel: string | undefined , data: any | undefined , children: Array <VNode | string> | undefined , text: string | undefined , elm: Element | DocumentFragment | Text | undefined ): VNode { const key = data === undefined ? undefined : data.key ; return { sel, data, children, text, elm, key }; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 return function patch ( oldVnode: VNode | Element | DocumentFragment, vnode: VNode ): VNode { let i : number, elm : Node , parent : Node ; const insertedVnodeQueue : VNodeQueue = []; for (i = 0 ; i < cbs.pre .length ; ++i) cbs.pre [i](); if (isElement (api, oldVnode)) { oldVnode = emptyNodeAt (oldVnode); } else if (isDocumentFragment (api, oldVnode)) { oldVnode = emptyDocumentFragmentAt (oldVnode); } if (sameVnode (oldVnode, vnode)) { patchVnode (oldVnode, vnode, insertedVnodeQueue); } else { elm = oldVnode.elm !; parent = api.parentNode (elm) as Node ; createElm (vnode, insertedVnodeQueue); if (parent !== null ) { api.insertBefore (parent, vnode.elm !, api.nextSibling (elm)); removeVnodes (parent, [oldVnode], 0 , 0 ); } } for (i = 0 ; i < insertedVnodeQueue.length ; ++i) { insertedVnodeQueue[i].data !.hook !.insert !(insertedVnodeQueue[i]); } for (i = 0 ; i < cbs.post .length ; ++i) cbs.post [i](); return vnode; };
如果 patch
第一个参数不是 vnode,创建一个空的 vnode
1 2 3 4 5 6 7 8 9 10 11 12 function emptyNodeAt (elm: Element ) { const id = elm.id ? "#" + elm.id : "" ; const classes = elm.getAttribute ("class" ); const c = classes ? "." + classes.split (" " ).join ("." ) : "" ; return vnode ( api.tagName (elm).toLowerCase () + id + c, {}, [], undefined , elm ); }
比较 vnode 是否相同(比较 key 和 selector)
1 2 3 4 5 6 7 8 function sameVnode (vnode1: VNode, vnode2: VNode ): boolean { const isSameKey = vnode1.key === vnode2.key ; const isSameIs = vnode1.data ?.is === vnode2.data ?.is ; const isSameSel = vnode1.sel === vnode2.sel ; return isSameSel && isSameKey && isSameIs; }
patchVnode vnode 相同的话执行 patchVnode
两者都有 children -> 进行 children 对比,之后更新 children(updateChildren)
新 children 有,旧 children 无 -> 清空旧 text,添加 children(addVnodes)
新 children 无,旧 children 有 -> 移除旧 children(removeVnodes)
新 children 无,旧 children 无,旧 text 有 -> 清空旧 text
新旧 text 不一样 -> 移除旧 children(removeVnodes),设置 text
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 function patchVnode ( oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue ) { const hook = vnode.data ?.hook ; hook?.prepatch ?.(oldVnode, vnode); const elm = (vnode.elm = oldVnode.elm )!; const oldCh = oldVnode.children as VNode []; const ch = vnode.children as VNode []; if (oldVnode === vnode) return ; if (vnode.data !== undefined ) { for (let i = 0 ; i < cbs.update .length ; ++i) cbs.update [i](oldVnode, vnode); vnode.data .hook ?.update ?.(oldVnode, vnode); } if (isUndef (vnode.text )) { if (isDef (oldCh) && isDef (ch)) { if (oldCh !== ch) updateChildren (elm, oldCh, ch, insertedVnodeQueue); } else if (isDef (ch)) { if (isDef (oldVnode.text )) api.setTextContent (elm, "" ); addVnodes (elm, null , ch, 0 , ch.length - 1 , insertedVnodeQueue); } else if (isDef (oldCh)) { removeVnodes (elm, oldCh, 0 , oldCh.length - 1 ); } else if (isDef (oldVnode.text )) { api.setTextContent (elm, "" ); } } else if (oldVnode.text !== vnode.text ) { if (isDef (oldCh)) { removeVnodes (elm, oldCh, 0 , oldCh.length - 1 ); } api.setTextContent (elm, vnode.text !); } hook?.postpatch ?.(oldVnode, vnode); }
addVnodes removeVnodes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function addVnodes ( parentElm: Node, before: Node | null , vnodes: VNode[], startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue ) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx]; if (ch != null ) { api.insertBefore (parentElm, createElm (ch, insertedVnodeQueue), before); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function removeVnodes ( parentElm: Node, vnodes: VNode[], startIdx: number, endIdx: number ): void { for (; startIdx <= endIdx; ++startIdx) { let listeners : number; let rm : () => void ; const ch = vnodes[startIdx]; if (ch != null ) { if (isDef (ch.sel )) { invokeDestroyHook (ch); listeners = cbs.remove .length + 1 ; rm = createRmCb (ch.elm !, listeners); for (let i = 0 ; i < cbs.remove .length ; ++i) cbs.remove [i](ch, rm); const removeHook = ch?.data ?.hook ?.remove ; if (isDef (removeHook)) { removeHook (ch, rm); } else { rm (); } } else { api.removeChild (parentElm, ch.elm !); } } } }
updateChildren 新旧都有 children,会对 children 进行 updateChildren
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 function updateChildren ( parentElm: Node, oldCh: VNode[], newCh: VNode[], insertedVnodeQueue: VNodeQueue ) { while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode == null ) { } } else if (sameVnode (oldStartVnode, newStartVnode)) { } else if (sameVnode (oldEndVnode, newEndVnode)) { } else if (sameVnode (oldStartVnode, newEndVnode)) { } else if (sameVnode (oldEndVnode, newStartVnode)) { } else { idxInOld = oldKeyToIdx[newStartVnode.key as string]; if (isUndef (idxInOld)) { api.insertBefore ( parentElm, createElm (newStartVnode, insertedVnodeQueue), oldStartVnode.elm ! ); } else { elmToMove = oldCh[idxInOld]; if (elmToMove.sel !== newStartVnode.sel ) { api.insertBefore ( parentElm, createElm (newStartVnode, insertedVnodeQueue), oldStartVnode.elm ! ); } else { patchVnode (elmToMove, newStartVnode, insertedVnodeQueue); oldCh[idxInOld] = undefined as any; api.insertBefore (parentElm, elmToMove.elm !, oldStartVnode.elm !); } } newStartVnode = newCh[++newStartIdx]; } } }
不使用 key 就会全部删掉然后插入;使用 key ,key 相同会直接移动过来,不用做销毁然后重新渲染的过程
总结 在 Vue 中,主要是 patch()
、patchVnode()
和 updateChildren
这三个方法来实现 Diff
的
当 Vue 中的响应式数据发生变化时,就会触发 updateCompoent()
updateComponent()
会调用 patch()
方法,在该方法中进行比较,调用 sameVnode
判断是否为相同节点(判断 key
、tag
等静态属性),如果是相同节点的话执行 patchVnode
方法,开始比较节点差异,如果不是相同节点的话,则进行替换操作
patch()
接收新旧虚拟 DOM,即 oldVnode
、vnode
首先判断 vnode
是否存在,如果不存在,删除旧节点
如果 vnode
存在,再判断 oldVnode
,如果不存在,只需新增整个 vnode
即可
如果 vnode
和 oldVnode
都存在,判断两者是不是相同节点,如果是,调用 patchVnode()
,对两个节点进行详细比较
如果两者不是相同节点,只需将 vnode
转换为真实 DOM 替换 oldVnode
patchVnode
同样接收新旧虚拟 DOM,即 oldVnode
、vnode
首先判断两个虚拟 DOM 是不是全等,即没有任何变动,是的话直接结束函数,否者继续执行
其次更新节点的属性,接着判断 vnode.text
是否存在,存在的话只需更新节点文本即可,否则继续执行
判断 vnode
和 oldVnode
是否有孩子节点
如果两者都有孩子节点,执行 updateChildren()
,进行比较更新
如果 vnode
有孩子,oldVnode
没有,则直接删除所有孩子节点,并将该文本属性设为空
如果 oldVnode
有孩子,vnode
没有,则直接删除所有孩子节点
如果两者都没有孩子节点,就判断 oldVnode.text
是否有内容,有的话情况内容即可
updateChildren
接收三个参数:parentElm
父级真实节点、oldCh
为 oldVnode
的孩子节点、newCh
为 Vnode
的孩子节点
oldCh
和 newCh
都是一个数组。正常我们想到的方法就是对这两个数组一一比较,时间复杂度为 O(NM)
。Vue 中是通过四个指针实现的
首先是 oldStartVnode
和 newStartVnode
进行比较(两头比较),如果比较相同的话,就可以执行 patchVnode
如果 oldStartVnode
和 newStartVnode
匹配不上的话,接下来就是 oldEndVnode
和 newEndVnode
做比较了(两尾比较)
如果两头和两尾比较都不是相同节点的话,就开始交叉比较,首先是 oldStartVnode
和 newEndVnode
做比较(头尾比较)
如果 oldStartVnode
和 newEndVnode
匹配不上的话,就 oldEndVnode
和 newStartVnode
进行比较(尾头比较)
如果这四种比较方法都匹配不到相同节点,才是用暴力解法,针对 newStartVnode
去遍历 oldCh
中剩余的节点,一一匹配
在 sameVnode
中,比较两个节点是否相同时,第一个判断条件就是 vnode.key
,并且在后面是用暴力解法时,第一选择也是通过 key
去匹配
1 2 3 4 5 6 7 8 9 10 11 12 13 <div > <p > A</p > <p > B</p > <p > C</p > </div > <div > <p > B</p > <p > C</p > <p > A</p > </div >
如果没有设置 key
值,通过 diff
需要操作 DOM 次数会很多,因为 key
为 undefined
,即使每个标签都是相同节点,也会一一进行替换,需要操作 3 次 DOM
如果分别给对应添加了 key
值,通过 diff
只需操作 1 次 DOM
图片来源:图文并茂地来详细讲讲Vue Diff算法
模板编译
with 语法
模板到 render 函数,再到 vnode,再到渲染和更新
vue 组件可以用 render 代替 template
with 语法
使用 with
,能改变 {}
内自由变量的查找规则
如果找不到匹配的 obj
属性,就会报错
with 要慎用,它打破了作用域规则,易读性变差
1 2 3 4 5 6 7 8 9 const obj = { a : 100 }console .log (obj.a )console .log (obj.b ) with (obj) { console .log (a) console .log (b) }
vue 模板被编译成什么
html 是标签语言,只有 JS 才能实现判断、循环,因此模板一定是转换为某种 JS 代码,即编译模板
使用 webpack vue-loader
,会在开发环境下编译模板
1 2 3 4 5 6 7 8 9 10 11 12 const compiler = require ('vue-template-compiler' )const template = `<p>{{message}}</p>` const res = compiler.compile (template)console .log (res.render )
1 2 3 4 5 6 const template = `<p>{{flag ? message : 'no message found'}}</p>` with (this ) { return createElement ('p' , [createTextVNode (toString (flag ? message : 'no message found' ))]) }
1 2 3 4 5 6 7 8 9 10 11 const template = ` <div id="div1" class="container"> <img :src="imgUrl"/> </div>` with (this ) { return createElement ('div' , { staticClass : 'container' , attrs : { id : 'div1' } }, [ createElement ('img' , { attrs : { src : imgUrl } }), ]) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const template = ` <div> <p v-if="flag === 'a'">A</p> <p v-else>B</p> </div>` with (this ) { return createElement ('div' , [ flag === 'a' ? createElement ('p' , [createTextVNode ('A' )]) : createElement ('p' , [createTextVNode ('B' )]), ]) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const template = `<p v-show="flag === 'a'">A</p>` with (this ) { return createElement ( 'p' , { directives : [ { name : 'show' , rawName : 'v-show' , value : flag === 'a' , expression : "flag === 'a'" }, ], }, [createTextVNode ('A' )] ) } bind (el : any, { value }: VNodeDirective , vnode : VNodeWithData ) { vnode = locateNode (vnode) const transition = vnode.data && vnode.data .transition const originalDisplay = el.__vOriginalDisplay = el.style .display === 'none' ? '' : el.style .display if (value && transition) { vnode.data .show = true enter (vnode, () => { el.style .display = originalDisplay }) } else { el.style .display = value ? originalDisplay : 'none' } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const template = ` <ul> <li v-for="item in list" :key="item.id">{{item.title}}</li> </ul>` with (this ) { return createElement ( 'ul' , renderList (list, function (item ) { return createElement ('li' , { key : item.id }, [createTextVNode (toString (item.title ))]) }), 0 ) }
1 2 3 4 5 6 const template = `<button @click="clickHandler">submit</button>` with (this ) { return createElement ('button' , { on : { click : clickHandler } }, [createTextVNode ('submit' )]) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const template = `<input type="text" v-model="name">` with (this ) { return createElement ('input' , { directives : [{ name : 'model' , rawName : 'v-model' , value : name, expression : 'name' }], attrs : { type : 'text' }, domProps : { value : name }, on : { input : function ($event ) { if ($event.target .composing ) return name = $event.target .value }, }, }) }
vue 组件使用 render 函数
有些复杂情况中,不能用 template,可以考虑用 render
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Vue .component ('heading' , { render : function (createElement ) { return createElement ('h' + this .level , [ createElement ( 'a' , { attrs : { name : 'headerId' , href : '#' + 'headerId' , }, }, 'this is a tag' ), ]) }, })
Vue 组件渲染和更新 初次渲染过程:
解析模板为 render
函数(或在开发环境已完成,vue-loader)
触发响应式,监听 data
属性 getter
和 setter
执行 render
函数,生成 vnode
,patch(elem, vnode)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <template > <p > {{ message }}</p > </template > <script > export default { data ( ) { return { message : 'hello' , city : '北京' , } }, } </script >
更新过程:
修改 data
,触发 setter
(此前在 getter
中已被监听)
重新执行 render
函数,生成 newVnode
patch(vnode, newVnode)
生成 render
函数,其生成一个 vnode
,它会 touch
触发 getter
进行收集依赖
在模板中哪个被引用了就会将其用 Watcher
观察起来,发生了 setter
也会将其 Watcher
起来
如果之前已经被 Watcher
观察起来,发生更新进行重新渲染
异步渲染
源码级别可以参考:Vue.js 技术揭秘——nextTick
$nextTick
汇总 data
的修改,一次性更新视图
减少 DOM 操作次数,提高性能
前端路由原理 hash 路由 hash 的特点:
hash
变化会触发网页跳转,即浏览器的前进、后退
hash
变化不会刷新页面,SPA
必需的特点
hash
永远不会提交到 server
端(前端自生自灭)
1 2 3 4 5 6 7 8 http :location.protocol location.hostname location.host location.port location.pathname location.search location.hash
hash 变化:
JS 修改 url
手动修改 url
的 hash
浏览器前进、后退
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <!DOCTYPE html > <html > <body > <p > hash test</p > <button id ="btn1" > 修改 hash</button > </body > </html > <script > window .onhashchange = event => { console .log ('old url' , event.oldURL ) console .log ('new url' , event.newURL ) console .log ('hash:' , location.hash ) } document .addEventListener ('DOMContentLoaded' , () => { console .log ('hash:' , location.hash ) }) document .getElementById ('btn1' ).addEventListener ('click' , () => { location.href = '#/user' }) </script >
history 路由
用 url
规范的路由,但跳转时不刷新页面
需要 server
端配合,可参考:后端配置例子
pushState
不会触发 hashchange
事件,popstate
事件只会在浏览器某些行为下触发,比如点击后退、前进按钮
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <!DOCTYPE html > <html lang ="en" > <body > <p > history API test</p > <button id ="btn1" > 修改 url</button > </body > </html > <script > document .addEventListener ('DOMContentLoaded' , () => { console .log ('load' , location.pathname ) }) document .getElementById ('btn1' ).addEventListener ('click' , () => { const state = { name : 'page1' } console .log ('切换路由到' , 'page1' ) history.pushState (state, '' , 'page1' ) }) window .onpopstate = event => { console .log ('onpopstate' , event.state , location.pathname ) } </script >
两者选择
to B
的系统推荐用 hash
,简单易用,对 url
规范不敏感
to C
的系统,可以考虑选择 H5 history
,但需要服务端支持
能选择简单的,就别用复杂的,要考虑成本和收益