Vue 基础结构
Vue 的生命周期
Vue 语法和概念-上
Vue 语法和概念-下
VueRouter-构造函数
VueRouter-createRouteMap
VueRouter-router-link
VueRouter-render
VueRouter-router-view
VueRouter-initEvent
let _Vue = null
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() {
if (this.$options.router) {
_Vue.prototype.$router = this.$options.router
this.$options.router.init()
}
}
})
}
constructor(options) {
this.options = options
this.routeMap = {}
this.data = _Vue.observable({
current: '/'
})
}
init() {
this.createRouteMap()
this.initComponents(_Vue)
this.initEvent()
}
createRouteMap() {
// 遍历所有的路由规则,把路由规则解析成键值对的形式 存储到routeMap中
this.options.routes.forEach(route => {
this.routeMap[route.path] = route.component
})
}
initComponents(Vue) {
Vue.component('router-link', {
props: {
to: String
},
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
console.log(22)
e.preventDefault()
}
}
// template: ' ' 必须使用完整版才能进行编译
})
const self = this
Vue.component('router-view', {
render(h) {
const component = self.routeMap[self.data.current]
return h(component)
}
})
}
initEvent() {
window.addEventListener('popstate', () => {
this.data.current = window.location.pathname
})
}
}
//
// hello
//
//
// 模拟 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 中的 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)
// 事件触发器
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')
// 发布者-目标
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')
}
}
// 测试
let dep = new Dep()
let watcher = new Watcher()
dep.addSub(watcher)
dep.notify()
<div id="app">
<h1>差值表达式h1>
<h3>{{ msg }}h3>
<h3>{{ count }}h3>
<h1>v-texth1>
<div v-text="msg">div>
<h1>v-modelh1>
<input type="text" v-model="msg">
<input type="text" v-model="count">
div>
<script src="./js/dep.js">script>
<script src="./js/watcher.js">script>
<script src="./js/compiler.js">script>
<script src="./js/observer.js">script>
<script src="./js/vue.js">script>
<script>
let vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue',
count: 100,
person: { name: 'zs' }
}
})
console.log(vm.msg)
// vm.msg = { test: 'Hello' }
vm.test = 'abc'
script>
class Vue {
constructor (options) {
// 1. 通过属性保存选项的数据
this.$options = options || {}
this.$data = options.data || {}
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
// 2. 把data中的成员转换成getter和setter,注入到vue实例中
this._proxyData(this.$data)
// 3. 调用observer对象,监听数据的变化
new Observer(this.$data)
// 4. 调用compiler对象,解析指令和差值表达式
new Compiler(this)
}
_proxyData (data) {
// 遍历data中的所有属性
Object.keys(data).forEach(key => {
// 把data的属性注入到vue实例中
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get () {
return data[key]
},
set (newValue) {
if (newValue === data[key]) {
return
}
data[key] = newValue
}
})
})
}
}
class Observer {
constructor (data) {
this.walk(data)
}
walk (data) {
// 1. 判断data是否是对象
if (!data || typeof data !== 'object') {
return
}
// 2. 遍历data对象的所有属性
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive (obj, key, val) {
let that = this
// 负责收集依赖,并发送通知
let dep = new Dep()
// 如果val是对象,把val内部的属性转换成响应式数据
this.walk(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get () {
// 收集依赖
Dep.target && dep.addSub(Dep.target)
return val
},
set (newValue) {
if (newValue === val) {
return
}
val = newValue
that.walk(newValue)
// 发送通知
dep.notify()
}
})
}
class Compiler {
constructor (vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 编译模板,处理文本节点和元素节点
compile (el) {
let childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
// 处理文本节点
if (this.isTextNode(node)) {
this.compileText(node)
} else if (this.isElementNode(node)) {
// 处理元素节点
this.compileElement(node)
}
// 判断node节点,是否有子节点,如果有子节点,要递归调用compile
if (node.childNodes && node.childNodes.length) {
this.compile(node)
}
})
}
// 编译元素节点,处理指令
compileElement (node) {
// console.log(node.attributes)
// 遍历所有的属性节点
Array.from(node.attributes).forEach(attr => {
// 判断是否是指令
let attrName = attr.name
if (this.isDirective(attrName)) {
// v-text --> text
attrName = attrName.substr(2)
let key = attr.value
this.update(node, key, attrName)
}
})
}
update (node, key, attrName) {
let updateFn = this[attrName + 'Updater']
updateFn && updateFn.call(this, node, this.vm[key], key)
}
// 处理 v-text 指令
textUpdater (node, value, key) {
node.textContent = value
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
// v-model
modelUpdater (node, value, key) {
node.value = value
new Watcher(this.vm, key, (newValue) => {
node.value = newValue
})
// 双向绑定
node.addEventListener('input', () => {
this.vm[key] = node.value
})
}
// 编译文本节点,处理差值表达式
compileText (node) {
// console.dir(node)
// {{ msg }}
let reg = /\{\{(.+?)\}\}/
let value = node.textContent
if (reg.test(value)) {
let key = RegExp.$1.trim()
node.textContent = value.replace(reg, this.vm[key])
// 创建watcher对象,当数据改变更新视图
new Watcher(this.vm, key, (newValue) => {
node.textContent = newValue
})
}
}
// 判断元素属性是否是指令
isDirective (attrName) {
return attrName.startsWith('v-')
}
// 判断节点是否是文本节点
isTextNode (node) {
return node.nodeType === 3
}
// 判断节点是否是元素节点
isElementNode (node) {
return node.nodeType === 1
}
}
Compiler-compile
Compiler-compileText
Compiler-compileElement
Compiler复习
Dep
class Dep {
constructor () {
// 存储所有的观察者
this.subs = []
}
// 添加观察者
addSub (sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知
notify () {
this.subs.forEach(sub => {
sub.update()
})
}
}
class Watcher {
constructor (vm, key, cb) {
this.vm = vm
// data中的属性名称
this.key = key
// 回调函数负责更新视图
this.cb = cb
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this
// 触发get方法,在get方法中会调用addSub
this.oldValue = vm[key]
Dep.target = null
}
// 当数据发生变化的时候更新视图
update () {
let newValue = this.vm[this.key]
if (this.oldValue === newValue) {
return
}
this.cb(newValue)
}
}
创建watcher对象1
创建watcher对象2
双向绑定
调试-首次渲染
调试-数据改变
总结-问题
创建项目,并安装
# 创建项目目录
md snabbdom-demo
# 进入项目目录
cd snabbdom-demo
# 创建 package.json
npm init -y
# 本地安装 parcel
npm install parcel-bundler -D
配置 package.json 的 scripts
"scripts": {
"dev": "parcel index.html --open",
"build": "parcel build index.html"
}
创建目录结构
│ index.html
│ package.json
└─src
01-basicusage.js
文档地址
# --depth 表示克隆深度, 1 表示只克隆最新的版本. 因为如果项目迭代的版本很多, 克隆会很慢
git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git
安装 Snabbdom
npm install [email protected]
Snabbdom 的两个核心函数 init 和 h()
init() 是一个高阶函数,返回 patch()
h() 返回虚拟节点 VNode,这个函数我们在使用 Vue.js 的时候见过
import { init } from 'snabbdom/init'
import { h } from 'snabbdom/h'
const patch = init([])
注意:此时运行的话会告诉我们找不到 init / h 模块,因为模块路径并不是 snabbdom/int,这个路径是在 package.json 中的 exports 字段设置的,而我们使用的打包工具不支持 exports 这个字段,webpack 4 也不支持,webpack 5 支持该字段(或者nodejs 12中开始支持)。该字段在导入 snabbdom/init 的时候会补全路径成 snabbdom/build/package/init.js
setAttribute
()attributes
模块相似,设置 DOM 元素的属性 element[attr] = value
sel
选择器data-*
的自定义属性import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
// 1. 导入模块
import { styleModule } from 'snabbdom/build/package/modules/style'
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
// 2. 注册模块
const patch = init([
styleModule,
eventListenersModule
])
// 3. 使用h() 函数的第二个参数传入模块中使用的数据(对象)
let vnode = h('div', [
h('h1', { style: { backgroundColor: 'red' } }, 'Hello World'),
h('p', { on: { click: eventHandler } }, 'Hello P')
])
function eventHandler () {
console.log('别点我,疼')
}
let app = document.querySelector('#app')
patch(app, vnode)
h() 函数介绍
在使用 Vue 的时候见过 h() 函数
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
h() 函数最早见于 hyperscript,使用 JavaScript 创建超文本
Snabbdom 中的 h() 函数不是用来创建超文本,而是创建 VNode
重载
源码位置:src/package/h.ts
// 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 = {}
var children: any
var text: any
var i: number
// 处理参数,实现重载的机制
if (c !== undefined) {
// 处理三个参数的情况
// sel、data、children/text
if (b !== null) {
data = b
}
if (is.array(c)) {
children = c
} else if (is.primitive(c)) {
text = c
} else if (c && c.sel) {
children = [c]
}
} else if (b !== undefined && b !== null) {
if (is.array(b)) {
children = b
} else if (is.primitive(b)) {
// 如果 c 是字符串或者数字
text = b
} else if (b && b.sel) {
// 如果 b 是 VNode
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
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: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number
let j: number
const cbs: ModuleHooks = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: []
}
// 初始化 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 any[]).push(hook)
}
}
}
……
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
……
}
}
**功能:**init(modules, domApi),返回 patch() 函数(高阶函数)
为什么要使用高阶函数?
init() 在返回 patch() 之前,首先收集了所有模块中的钩子函数存储到 cbs 对象中
执行过程:
pre
create
钩子函数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) as Node
// 触发 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]()
return vnode
}
function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any
let data = vnode.data
if (data !== undefined) {
// 执行用户设置的 init 钩子函数
const init = data.hook?.init
if (isDef(init)) {
init(vnode)
data = vnode.data
}
}
const children = vnode.children
const 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))
}
const 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
}
调试 createElm 函数
removeVnodes 和 addvnodes
patchVnode
功能:
执行过程:
oldVnode.children
和 vnode.children
都有值
updateChildren()
vnode.children
有值,oldVnode.children
无值
addVnodes()
,批量添加子节点oldVnode.children
有值,vnode.children
无值
removeVnodes()
,批量移除子节点vnode.text
并且和和 oldVnode.text
不等
textContent
为 vnode.text
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OzuT6hm6-1626339777718)(./img/1626265957956.jpg)]
源码位置:src/package/init.ts
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
const hook = vnode.data?.hook
// 首先执行用户设置的 prepatch 钩子函数
hook?.prepatch?.(oldVnode, vnode)
const elm = vnode.elm = oldVnode.elm!
const oldCh = oldVnode.children as VNode[]
const 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)) {
// 调用 updateChildren 对比子节点,更新子节点
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)
}
功能:
执行过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XIClwzmC-1626339777718)(./img/image-20200102103653779.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JVtGzUXI-1626339777719)(./img/image-20200109184608649.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-308FnApV-1626339777720)(./img/image-20200103121812840.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gjqz3rvO-1626339777720)(./img/image-20200103125428541.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l3id3hci-1626339777721)(./img/image-20200103125735048.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nFhnua1q-1626339777722)(./img/image-20200109184822439.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yviyewni-1626339777722)(./img/image-20200103150918335.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2d5E9vyI-1626339777723)(./img/image-20200109194751093.png)]
源码位置:src/package/init.ts
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0
let 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!)
} 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)
}
}
}
updateChildren
调试 updateChildren
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
let patch = init([])
// 首次渲染
let vnode = h('ul', [
h('li', '首页'),
h('li', '视频'),
h('li', '微博')
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)
// updateChildren 的执行过程
vnode = h('ul', [
h('li', '首页'),
h('li', '微博'),
h('li', '视频')
])
patch(oldVnode, vnode)
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
let patch = init([])
// 首次渲染
let vnode = h('ul', [
h('li', { key: 'a' }, '首页'),
h('li', { key: 'b' }, '视频'),
h('li', { key: 'c' }, '微博')
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)
// updateChildren 的执行过程
vnode = h('ul', [
h('li', { key: 'a' }, '首页'),
h('li', { key: 'c' }, '微博'),
h('li', { key: 'b' }, '视频')
])
patch(oldVnode, vnode)
import { init } from 'snabbdom/build/package/init'
import { h } from 'snabbdom/build/package/h'
import { attributesModule } from 'snabbdom/build/package/modules/attributes'
import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners'
let patch = init([attributesModule, eventListenersModule])
const data = [1, 2, 3, 4]
let oldVnode = null
function view (data) {
let arr = []
data.forEach(item => {
// 不设置 key
// arr.push(h('li', [h('input', { attrs: { type: 'checkbox' } }), h('span', item)]))
// 设置key
arr.push(h('li', { key: item }, [h('input', { attrs: { type: 'checkbox' } }), h('span', item)]))
})
let vnode = h('div', [ h('button', { on: { click: function () {
data.unshift(100)
vnode = view(data)
oldVnode = patch(oldVnode, vnode)
} } }, '按钮') , h('ul', arr)])
return vnode
}
let app = document.querySelector('#app')
// 首次渲染
oldVnode = patch(app, view(data))