插值表达式
插值表达式相当于一个占位符,只会替换掉其中的占位置的内容。
v-text只能显示Vue对象传递过来的数据,会替换掉节点里已有的内容。
插值表达式和v-text不能够解析html标签,v-html能够解析html标签。
结论:
指令
VUE提供的14个指令:
自定义指令
计算属性和侦听器
Class和Style绑定
当绑定样式时,我们可以使用Class和Style。它们分别可以绑定数组或者对象,实际开发中,我们推荐使用Class绑定,可以实现样式复用。
条件渲染/列表渲染
表单输入绑定
当我们使用v-model绑定表单元素时,它负责去监听用户的输入事件,以及更新数据,即双向绑定。
组件
组件是可复用的Vue实例,一个组件封装了html,css,js,它可以实现页面上的一个功能区域,可以无限次的被重用。
插槽
插槽一般用于在自定义组件中挖坑,使用这个组件时去填坑。这样可以使组件更灵活。
插件
如vuex、vue-router都是插件。也可以自己开发插件。
混入mixin
如果多个组件都有相同的选项,就可以使用mixin方式,把相同的选项进行合并,让代码重用。是让组件重用的一种方式。
深入响应式原理
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。
当多个路由组件都有相同的内容,我们可以把这个相同的内容提取到一个公共的组件当中。
常用方法:
$router.push()
this.$router.push('/')
this.$router.push({ name: 'Detail', params: { id: 1 } })
push方法会记录本次历史。
$router.replace()
this.$router.replace('/login')
注意:和push方法有些类似,都可以跳转到指定的路径,参数形式也是一样的。但是,replace方法不会记录本次历史,它会把我们当前的历史改变为我们指定的路径。
$router.go()
go是跳转到历史中的某一次,负数为后退。
this.$router.go(-2)
这两种模式都是客户端路由的实现方式,即当路径发生变化时,不会像服务器发送请求,是由JS监视路径的变化,然后根据不同的地址渲染不同的内容。如果需要服务端内容,会发送Ajax请求去获取。
表现形式的区别
History模式
History模式是一个正常的url。要用好History模式,还需要服务端配置支持。
通过调用history.pushState()改变地址栏;监听popstate事件;根据当前路由地址找到对应组件重新渲染。
原理的区别
history.pushState() //IE10以后才支持
history.replaceState()
history.go()
History模式的使用
const router = new VueRouter({
// mode: 'hash',
mode: 'history',
routes
})
const path = require('path')
// 导入处理 history 模式的模块
const history = require('connect-history-api-fallback')
// 导入 express
const express = require('express')
const app = express()
// 注册处理 history 模式的中间件
app.use(history())
// 处理静态资源的中间件,网站根目录 ../web
app.use(express.static(path.join(__dirname, '../web')))
// 开启服务器,端口是 3000
app.listen(3000, () => {
console.log('服务器开启,端口:3000')
})
History模式-nginx服务器配置:
location / {
root html;
index index.html index.htm;
#新添加内容
#尝试读取$uri(当前请求的路径),如果读取不到读取$uri/这个文件夹下的首页
#如果都获取不到返回根目录中的 index.html
try_files $uri $uri/ /index.html;
}
#
启动
start nginx
# 重启
nginx -s reload
# 停止
nginx -s stop
Vue Router 的核心代码
//router/index.js
// 注册插件
// Vue.use() 内部调用传入对象的 install 方法
Vue.use(VueRouter)
// 创建路由对象
const router = new VueRouter({
routes: [
{ name: 'home', path: '/', component: homeComponent }
]
})
// 创建 Vue 实例,注册 router 对象
new Vue({
router,
render: h => h(App)
}).$mount('#app')
实现代码(History模式)
let _Vue = null
//创建 VueRouter 插件,即实现 VueRouter 类
export default class VueRouter {
static install(Vue) {
// 1. 判断插件是否已经被加载/安装
// 如果插件已经安装直接返回
if (VueRouter.install.installed) {
return
}
VueRouter.install.installed = true
// 2. 把Vue构造函数记录到全局变量
_Vue = Vue
// 3. 把创建Vue实例时传入的 router 对象注入/挂载到 Vue 实例上(注意:只执行一次)
//混入
_Vue.mixin({
beforeCreate() {
// 插件的 install() 方法中调用 init() 初始化
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
// 初始化插件的时候,调用 init
this.$options.router.init()
}
}
})
}
//构造函数
constructor(options) {
this.options = options
// 记录路径和对应的组件
this.routeMap = {}
this.data = _Vue.observable({
// 当前的默认路径
current: '/'
})
}
//初始化
init() {
this.initRouteMap()
this.initComponents()
this.initEvent()
}
//解析路由规则成键值对形式
initRouteMap() {
// routes => [{ name: '', path: '', component: }]
//遍历所有的路由规则,把路由规则解析成键值对的形式,存储到routeMap中
this.options.routes.forEach(route => {
// 记录路径和组件的映射关系
this.routeMap[route.path] = route.component
})
}
//创建 router-link 和 router-view 组件
initComponents() {
_Vue.component('router-link', {
props: {
to: String
},
// template: ' '
//运行时版本不支持template,需要使用render
render(h) {
return h('a', {
attrs: {
href: this.to
},
on: {
click: this.clickHandler
}
}, [this.$slots.default])
},
methods: {
clickHandler(e) {
history.pushState({}, '', this.to)
this.$router.data.current = this.to
e.preventDefault()
}
}
})
const self = this
_Vue.component('router-view', {
render(h) {
// 根据当前路径找到对应的组件,注意 this 的问题
const component = self.routeMap[self.data.current]
return h(component)
}
})
}
//注册事件
initEvent() {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
}
Vue的构建版本
注意:
vue-cli 创建的项目默认使用的是运行时版本的 Vue.js
module.exports = {
runtimeCompiler: true
}
数据响应式、双向绑定、数据驱动。
Vue 2.x(基于Object.defineProperty)
一个对象中一个属性需要转换 getter/setter ,处理方式:
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello'
}
// 模拟 Vue 的实例
let vm = {}
// 数据劫持:当访问或者设置 vm 中的成员的时候,做一些干预操作
Object.defineProperty(vm, 'msg', {
// 可枚举(可遍历)
enumerable: true,
// 可配置(可以使用 delete 删除,可以通过 defineProperty 重新定义)
configurable: true,
// 当获取值的时候执行
get () {
console.log('get: ', data.msg)
return data.msg
},
// 当设置值的时候执行
set (newValue) {
console.log('set: ', newValue)
if (newValue === data.msg) {
return
}
data.msg = newValue
// 数据更改,更新 DOM 的值
document.querySelector('#app').textContent = data.msg
}
})
// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
一个对象中多个属性需要转换 getter/setter ,处理方式:
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello',
count: 10
}
// 模拟 Vue 的实例
let vm = {}
proxyData(data)
function proxyData(data) {
// 遍历 data 对象的所有属性
Object.keys(data).forEach(key => {
// 把 data 中的属性,转换成 vm 的 setter/setter
Object.defineProperty(vm, key, {
enumerable: true,
configurable: true,
get () {
console.log('get: ', key, data[key])
return data[key]
},
set (newValue) {
console.log('set: ', key, newValue)
if (newValue === data[key]) {
return
}
data[key] = newValue
// 数据更改,更新 DOM 的值
document.querySelector('#app').textContent = data[key]
}
})
})
}
// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
Vue 3.x(基于Proxy)
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello',
count: 0
}
// 模拟 Vue 实例
let vm = new Proxy(data, {
// 执行代理行为的函数
// 当访问 vm 的成员会执行
get (target, key) {
console.log('get, key: ', key, target[key])
return target[key]
},
// 当设置 vm 的成员会执行
set (target, key, newValue) {
console.log('set, key: ', key, newValue)
if (target[key] === newValue) {
return
}
target[key] = newValue
document.querySelector('#app').textContent = target[key]
}
})
// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
发布订阅模式
我们假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做"发布/订阅模式"(publish-subscribe pattern)。
// Vue 自定义事件
let vm = new Vue()
// { 'click': [fn1, fn2], 'change': [fn] }
// 注册事件(订阅消息)
vm.$on('dataChange', () => {
console.log('dataChange')
})
vm.$on('dataChange', () => {
console.log('dataChange1')
})
// 触发事件(发布消息)
vm.$emit('dataChange')
// eventBus.js
// 事件中心
let eventHub = new Vue()
// ComponentA.vue
// 发布者
addTodo: function () {
// 发布消息(事件)
eventHub.$emit('add-todo', { text: this.newTodoText })
this.newTodoText = ''
}
// ComponentB.vue
// 订阅者
created: function () {
// 订阅消息(事件)
eventHub.$on('add-todo', this.addTodo)
}
//事件触发器
class EventEmitter {
constructor() {
//{ 'click': [fn1,fn2], "change": [fn] }
this.subs = Object.create(null)
}
//注册事件
$on(eventType, handler) {
this.subs[eventType] = this.subs[eventType] || []
this.subs[eventType].push(handler)
}
//触发事件
$emit(eventType) {
if (this.subs[eventType]) {
this.subs[eventType].forEach(handler => {
handler()
})
}
}
}
//测试
let em = new EventEmitter()
em.$on('click', () => {
console.log('click1');
})
em.$on('click', () => {
console.log('click2');
})
em.$emit('click')
//click1
//click2
观察者模式
//发布者-目标
class Dep {
constructor() {
//存储所有的观察者
this.subs = []
}
//添加观察者
addSub(sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
//发布通知
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
//订阅者-观察者
class Watcher {
update() {
console.log('update');
}
}
//测试
const dep = new Dep()
const watcher = new Watcher()
dep.addSub(watcher)
dep.notify()
//update
发布/订阅模式与观察者模式区别
Vue
什么是 Virtual DOM
Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,
所以叫 Virtual DOM。
创建虚拟DOM的开销要比创建真实DOM开销小很多。
如真实DOM成员:
let element = document.querySelector('#app')
let s = ''
for (var key in element) {
s += key + ','
} c
onsole.log(s)
// 打印结果
align,title,lang,translate,dir,hidden,accessKey,draggable,spellcheck,aut
ocapitalize,contentEditable,isContentEditable,inputMode,offsetParent,off
setTop,offsetLeft,offsetWidth,offsetHeight,style,innerText,outerText,onc
opy,oncut,onpaste,onabort,onblur,oncancel,oncanplay,oncanplaythrough,onc
hange,onclick,onclose,oncontextmenu,oncuechange,ondblclick,ondrag,ondrag
end,ondragenter,ondragleave,ondragover,ondragstart,ondrop,ondurationchan
ge,onemptied,onended,onerror,onfocus,oninput,oninvalid,onkeydown,onkeypr
ess,onkeyup,onload,onloadeddata,onloadedmetadata,onloadstart,onmousedown
,onmouseenter,onmouseleave,onmousemove,onmouseout,onmouseover,onmouseup,
onmousewheel,onpause,onplay,onplaying,onprogress,onratechange,onreset,on
resize,onscroll,onseeked,onseeking,onselect,onstalled,onsubmit,onsuspend
,ontimeupdate,ontoggle,onvolumechange,onwaiting,onwheel,onauxclick,ongot
pointercapture,onlostpointercapture,onpointerdown,onpointermove,onpointe
rup,onpointercancel,onpointerover,onpointerout,onpointerenter,onpointerl
eave,onselectstart,onselectionchange,onanimationend,onanimationiteration
,onanimationstart,ontransitionend,dataset,nonce,autofocus,tabIndex,click
,focus,blur,enterKeyHint,onformdata,onpointerrawupdate,attachInternals,n
amespaceURI,prefix,localName,tagName,id,className,classList,slot,part,at
tributes,shadowRoot,assignedSlot,innerHTML,outerHTML,scrollTop,scrollLef
t,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight
,attributeStyleMap,onbeforecopy,onbeforecut,onbeforepaste,onsearch,eleme
ntTiming,previousElementSibling,nextElementSibling,children,firstElement
Child,lastElementChild,childElementCount,onfullscreenchange,onfullscreen
error,onwebkitfullscreenchange,onwebkitfullscreenerror,setPointerCapture
,releasePointerCapture,hasPointerCapture,hasAttributes,getAttributeNames
,getAttribute,getAttributeNS,setAttribute,setAttributeNS,removeAttribute
,removeAttributeNS,hasAttribute,hasAttributeNS,toggleAttribute,getAttrib
uteNode,getAttributeNodeNS,setAttributeNode,setAttributeNodeNS,removeAtt
ributeNode,closest,matches,webkitMatchesSelector,attachShadow,getElement
sByTagName,getElementsByTagNameNS,getElementsByClassName,insertAdjacentE
lement,insertAdjacentText,insertAdjacentHTML,requestPointerLock,getClien
tRects,getBoundingClientRect,scrollIntoView,scroll,scrollTo,scrollBy,scr
ollIntoViewIfNeeded,animate,computedStyleMap,before,after,replaceWith,re
move,prepend,append,querySelector,querySelectorAll,requestFullscreen,web
kitRequestFullScreen,webkitRequestFullscreen,createShadowRoot,getDestina
tionInsertionPoints,ELEMENT_NODE,ATTRIBUTE_NODE,TEXT_NODE,CDATA_SECTION_
NODE,ENTITY_REFERENCE_NODE,ENTITY_NODE,PROCESSING_INSTRUCTION_NODE,COMME
NT_NODE,DOCUMENT_NODE,DOCUMENT_TYPE_NODE,DOCUMENT_FRAGMENT_NODE,NOTATION
_NODE,DOCUMENT_POSITION_DISCONNECTED,DOCUMENT_POSITION_PRECEDING,DOCUMEN
T_POSITION_FOLLOWING,DOCUMENT_POSITION_CONTAINS,DOCUMENT_POSITION_CONTAI
NED_BY,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,nodeType,nodeName,baseU
RI,isConnected,ownerDocument,parentNode,parentElement,childNodes,firstCh
ild,lastChild,previousSibling,nextSibling,nodeValue,textContent,hasChild
Nodes,getRootNode,normalize,cloneNode,isEqualNode,isSameNode,compareDocu
mentPosition,contains,lookupPrefix,lookupNamespaceURI,isDefaultNamespace
,insertBefore,appendChild,replaceChild,removeChild,addEventListener,remo
veEventListener,dispatchEvent
如 Virtual DOM成员:
{
sel: "div",
data: {},
children: undefined,
text: "Hello Virtual DOM",
elm: undefined,
key: undefined
}
为什么使用 Virtual DOM
虚拟 DOM 的作用
Snabbdom 文档
看文档的意义
安装 Snabbdom
yarn add snabbdom
导入 Snabbdom
var snabbdom = require('snabbdom');
import { init, h, thunk } from 'snabbdom'
注意:导入时候不能使用 import snabbdom from ‘snabbdom’
原因:node_modules/src/snabbdom.ts 末尾导出使用的语法是 export 导出 API,没有使用export default 导出默认输出。
关于模块化的语法请参考:
模块
Snabbdom 的核心库并不能处理元素的属性/样式/事件等,如果需要处理的话,可以使用模块。
常用模块
官方提供了 6 个模块
模块使用
模块使用步骤:
如何学习源码
看源码必备快捷键
Snabbdom 的核心
Snabbdom 源码
h() 函数介绍:
在使用 Vue 的时候见过 h() 函数
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
h() 函数最早见于hyperscript,使用 JavaScript 创建超文本。
Snabbdom 中的 h() 函数不是用来创建超文本,而是创建 VNode。
函数重载
function add (a, b) {
console.log(a + b)
} f
unction add (a, b, c) {
console.log(a + b + c)
} a
dd(1, 2)
add(1, 2, 3)
源码:
// h 函数的重载
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 {
var data: VNodeData = {}, children: any, text: any, i: number;
// 处理参数,实现重载的机制
if (c !== undefined) {
// 处理三个参数的情况
// sel、data、children/text
if (b !== null) { data = b; }
if (is.array(c)) { children = c; }
// 如果 c 是字符串或者数字
else if (is.primitive(c)) { text = c; }
// 如果 c 是 VNode
else if (c && c.sel) { children = [c]; }
} else if (b !== undefined && b !== null) {
// 处理两个参数的情况
// 如果 b 是数组
if (is.array(b)) { children = b; }
// 如果 b 是字符串或者数字
else if (is.primitive(b)) { text = b; }
// 如果 b 是 VNode
else if (b && b.sel) { children = [b]; }
else { data = b; }
} if (children !== undefined) {
// 处理 children 中的原始值(string/number)
for (i = 0; i < children.length; ++i) {
// 如果 child 是 string/number,创建文本节点
if (is.primitive(children[i])) children[i] = vnode(undefined,
undefined, undefined, children[i], undefined);
}
} if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
// 如果是 svg,添加命名空间
addNS(data, children, sel);
}
// 返回 VNode
return vnode(sel, data, children, text, undefined);
};
一个 VNode 就是一个虚拟节点用来描述一个 DOM 元素,如果这个 VNode 有 children 就是
Virtual DOM。
源码:
export interface VNode {
//选择器
sel: string | undefined
//节点数据:属性/样式/事件等
data: VNodeData | undefined
//子节点:和text只能互斥
children: Array<VNode | string> | undefined
//记录vnode对应的真实DOM
elm: Node | undefined
//节点中的内容,和children只能互斥
text: string | undefined
//优化用
key: Key | undefined
}
export function vnode(sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
const key = data === undefined ? undefined : data.key
return { sel, data, children, text, elm, key }
}
const hooks: (keyof Module)[] = ['create', 'update', 'remove',
'destroy', 'pre', 'post'];
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number, j: number, cbs = ({} as ModuleHooks);
// 初始化 api
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
// 把传入的所有模块的钩子方法,统一存储到 cbs 对象中
// 最终构建的 cbs 对象的形式 cbs = [ create: [fn1, fn2], update: [], ...
]
for (i = 0; i < hooks.length; ++i) {
// cbs['create'] = []
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
// const hook = modules[0]['create']
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
(cbs[hooks[i]] as Array<any>).push(hook);
}
}
} …
…
……
……
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { }
}
patch函数
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
// 保存新插入节点的队列,为了触发钩子函数
const insertedVnodeQueue: VNodeQueue = [];
// 执行模块的 pre 钩子函数
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 如果 oldVnode 不是 VNode,创建 VNode 并设置 elm
if (!isVnode(oldVnode)) {
// 把 DOM 元素转换成空的 VNode
oldVnode = emptyNodeAt(oldVnode);
}
// 如果新旧节点是相同节点(key 和 sel 相同)
if (sameVnode(oldVnode, vnode)) {
// 找节点的差异并更新 DOM
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
// 如果新旧节点不同,vnode 创建对应的 DOM
// 获取当前的 DOM 元素
elm = oldVnode.elm!;
parent = api.parentNode(elm);
// 触发 init/create 钩子函数,创建 DOM
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
// 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
// 移除老节点
removeVnodes(parent, [oldVnode], 0, 0);
}
}
// 执行用户设置的 insert 钩子函数
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
}
// 执行模块的 post 钩子函数
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
// 返回 vnode
return vnode;
};
createElm函数
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any, data = vnode.data;
if (data !== undefined) {
// 执行用户设置的 init 钩子函数
const init = data.hook?.init;
if (isDef(init)) {
init(vnode);
data = vnode.data;
}
}
let children = vnode.children, sel = vnode.sel;
if (sel === '!') {
// 如果选择器是!,创建评论节点
if (isUndef(vnode.text)) {
vnode.text = '';
}
vnode.elm = api.createComment(vnode.text!);
} else if (sel !== undefined) {
// 如果选择器不为空
// 解析选择器
// Parse selector
const hashIdx = sel.indexOf('#');
const dotIdx = sel.indexOf('.', hashIdx);
const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length;
const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0,
Math.min(hash, dot)) : sel;
const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
? api.createElementNS(i, tag)
: api.createElement(tag);
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot +
1).replace(/\./g, ' '));
// 执行模块的 create 钩子函数
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode,
vnode);
// 如果 vnode 中有子节点,创建子 vnode 对应的 DOM 元素并追加到 DOM 树上
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode,
insertedVnodeQueue));
}
}
} else if (is.primitive(vnode.text)) {
// 如果 vnode 的 text 值是 string/number,创建文本节点并追加到 DOM 树
api.appendChild(elm, api.createTextNode(vnode.text));
} c
onst hook = vnode.data!.hook;
if (isDef(hook)) {
// 执行用户传入的钩子 create
hook.create?.(emptyNode, vnode);
if (hook.insert) {
// 把 vnode 添加到队列中,为后续执行 insert 钩子做准备
insertedVnodeQueue.push(vnode);
}
}
} else {
// 如果选择器为空,创建文本节点
vnode.elm = api.createTextNode(vnode.text!);
}
// 返回新创建的 DOM
return vnode.elm;
}
patchVnode函数
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue:
VNodeQueue) {
const hook = vnode.data?.hook;
// 首先执行用户设置的 prepatch 钩子函数
hook?.prepatch?.(oldVnode, vnode);
const elm = vnode.elm = oldVnode.elm!;
let oldCh = oldVnode.children as VNode[];
let ch = vnode.children as VNode[];
// 如果新老 vnode 相同返回
if (oldVnode === vnode) return;
if (vnode.data !== undefined) {
// 执行模块的 update 钩子函数
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode,
vnode);
// 执行用户设置的 update 钩子函数
vnode.data.hook?.update?.(oldVnode, vnode);
} /
/ 如果 vnode.text 未定义
if (isUndef(vnode.text)) {
// 如果新老节点都有 children
if (isDef(oldCh) && isDef(ch)) {
// 使用 diff 算法对比子节点,更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch,
insertedVnodeQueue);
} else if (isDef(ch)) {
// 如果新节点有 children,老节点没有 children
// 如果老节点有text,清空dom 元素的内容
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
// 批量添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 如果老节点有children,新节点没有children
// 批量移除子节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 如果老节点有 text,清空 DOM 元素
api.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// 如果没有设置 vnode.text
if (isDef(oldCh)) {
// 如果老节点有 children,移除
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} /
/ 设置 DOM 元素的 textContent 为 vnode.text
api.setTextContent(elm, vnode.text!);
}
// 最后执行用户设置的 postpatch 钩子函数
hook?.postpatch?.(oldVnode, vnode);
}
updateChildren函数
function updateChildren(parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0, newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 索引变化后,可能会把节点设置为空
if (oldStartVnode == null) {
// 节点为空移动索引
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been
moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
// 比较开始和结束节点的四种情况
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 1. 比较老开始节点和新的开始节点
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 2. 比较老结束节点和新的结束节点
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 3. 比较老开始节点和新的结束节点
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode
moved left
// 4. 比较老结束节点和新的开始节点
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!,
oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 开始节点和结束节点都不相同
// 使用 newStartNode 的 key 再老节点数组中找相同节点
// 先设置记录 key 和 index 的对象
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx,
oldEndIdx);
} /
/ 遍历 newStartVnode, 从老的节点中找相同 key 的 oldVnode 的索引
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 如果是新的vnode
if (isUndef(idxInOld)) { // New element
// 如果没找到,newStartNode 是新节点
// 创建元素插入 DOM 树
api.insertBefore(parentElm, createElm(newStartVnode,
insertedVnodeQueue), oldStartVnode.elm!);
// 重新给 newStartVnode 赋值,指向下一个新节点
newStartVnode = newCh[++newStartIdx];
} else {
// 如果找到相同 key 相同的老节点,记录到 elmToMove 遍历
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
// 如果新旧节点的选择器不同
// 创建新开始节点对应的 DOM 元素,插入到 DOM 树中
api.insertBefore(parentElm, createElm(newStartVnode,
insertedVnodeQueue), oldStartVnode.elm!);
} else {
// 如果相同,patchVnode()
// 把 elmToMove 对应的 DOM 元素,移动到左边
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!,
oldStartVnode.elm!);
} /
/ 重新给 newStartVnode 赋值,指向下一个新节点
newStartVnode = newCh[++newStartIdx];
}
}
} /
/ 循环结束,老节点数组先遍历完成或者新节点数组先遍历完成
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
// 如果老节点数组先遍历完成,说明有新的节点剩余
// 把剩余的新节点都插入到右边
before = newCh[newEndIdx + 1] == null ? null :
newCh[newEndIdx + 1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx,
insertedVnodeQueue);
} else {
// 如果新节点数组先遍历完成,说明老节点有剩余
// 批量删除老节点
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
Hooks
export interface Hooks {
// patch 函数开始执行的时候触发
pre?: PreHook;
// createElm 函数开始之前的时候触发
// 在把 VNode 转换成真实 DOM 之前触发
init?: InitHook;
// createElm 函数末尾调用
// 创建完真实 DOM 后触发
create?: CreateHook;
// patch 函数末尾执行
// 真实 DOM 添加到 DOM 树中触发
insert?: InsertHook;
// patchVnode 函数开头调用
// 开始对比两个 VNode 的差异之前触发
prepatch?: PrePatchHook;
// patchVnode 函数开头调用
// 两个 VNode 对比过程中触发,比 prepatch 稍晚
update?: UpdateHook;
// patchVnode 的最末尾调用
// 两个 VNode 对比结束执行
postpatch?: PostPatchHook;
// removeVnodes -> invokeDestroyHook 中调用
// 在删除元素之前触发,子节点的 destroy 也被触发
destroy?: DestroyHook;
// removeVnodes 中调用
// 元素被删除的时候触发
remove?: RemoveHook;
// patch 函数的最后调用
// patch 全部执行完毕触发
post?: PostHook;
}
Modules
模块文件的定义
Snabbdom 提供的所有模块在:src/modules 文件夹下,主要模块有: