- 本阶段围绕当下国内最主流的前端核心框架 Vue.js 展开,深入框架内部,通过解读源码或者手写实现的方式,剖析 Vue.js 框架的内部实现原理,让你做到知其所以然。同时我们还会介绍 Vue.js 的进阶用法、周边生态以及性能优化,让你轻松应对更加复杂的项目业务需求。
模块一 手写 Vue Router、手写响应式实现、虚拟 DOM 和 Diff 算法
- 首先回顾 Vue Router 的基本使用,以及 Hash 模式和 History 模式的区别,然后自己手写一个实现基 History 模式的前端路由,了解路由内部实现的原理;接下来在数据响应式实现原理分析中,自己动手一个简易版本的 Vue;最后掌握虚拟 DOM 的作用,通过一个虚拟 DOM 库 Snabbdom 真正了解什么是虚拟 DOM,以及 Diff 算法的实现和 key 的作用。
任务一:Vue.js 基础回顾
- 阶段内容
- Vue.js 基础语法
- Vue Router 原理分析与实现
- 虚拟 DOM 可以 Snabbdom 源码解析
- 响应式原理分析与实现
- Vue.js 源码分析
-
Vue 基础结构
-
Vue 的生命周期
-
Vue 语法和概念-上
-
Vue 语法和概念-下
- 组件
- 插槽
- 插件
- 混入 minxin
- 深入响应式原理
- 不同构件版本的Vue
任务二:Vue-Router 原理实现
- 课程介绍
- Hash模式和History模式
- Vue Router 基础回顾-使用步骤
- 使用步骤
- 创建一些和路由相关的组件(视图)
- 注册路由插件,调用Vue.use 注册 route
- 创建一个route 对象,创建路由对象配置规则
- 注册路由对象
- 通过 route-view 标签占位,路径匹配成功会替换掉这个标签
- 通过rout-link 创建链接
- 会给Vue 实例注入 r o u t e ( 路 由 规 则 ) route(路由规则) route(路由规则)router(路由对象) 在组件中,可以通过 $router.currentRoute 拿到当前页面的路由规则
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6SKVZHV9-1626339777695)(./img/1626143996749.jpg)]
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ROhcBU2S-1626339777699)(./img/1626144106142.jpg)]
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DgPZWrHR-1626339777700)(./img/1626144151593.jpg)]
- 动态路由
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IiLZZtpA-1626339777701)(./img/1626144332026.jpg)]
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XmkTBlsW-1626339777702)(./img/1626144404675.jpg)]
- 推荐使用第二种方式,设置props:true。通过组件的props获取
- 嵌套路由
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U7x1p4xi-1626339777703)(./img/1626144655457.jpg)]
- layout 嵌套路由
- 编程式导航
- replace 不会记录当前路由,push会
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SKuLfLGf-1626339777704)(./img/1626144893218.jpg)]
- go(-2) 表示后退几个页面
- Hash 模式和 History 模式的区别
- Hash url带#号
- 原理的区别
- Hash 模式是基于锚点,以及 onhashchange 事件
- History 模式是基于HTML5中的History API
- history.pushState() IE10 以后才支持 和history.push 方法的区别是,不会向服务器发送请求,只会改变地址,并把这个地址记录到历史记录中
- history.replaceState()
- History 模式
- History 模式的使用
- History 需要服务器的支持
- 单页应用中,服务端不存在 http://www.test.com/login 这样的地址会返回找不到该页面
- 在服务器端应该除了静态资源外都返回单页应用的 index.html
- const router = new VueRouter({mode: ‘history’, routes}) 默认是hash模式
- vue-cli 自带的服务器已经配置了返回单页应用,所以刷新不会出现404(而会走到vue项目中配置的404异常页)
- History 模式 - Node.js
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JXUi2fbD-1626339777705)(./img/1626157791595.jpg)]
- History 模式 - nginx.
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vj0MwEgt-1626339777705)(./img/1626157894481.jpg)]
- start nginx 启动 nginx -s reload 重启 nginx -s stop 停止
- nginx-1.18.0 > html 网站根目录 conf
- 默认80 端口,启动后直接 localhost 访问就可以了
- 修改配置重启服务器
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fDZNT7aG-1626339777706)(./img/1626158460413.jpg)]
- Vue Router 实现原理
- Hash 模式
- URL 中# 后面的内容作为路径地址
- 监听hashchange事件
- 根据当前路由地址找到对应组件重新渲染
- History 模式
- 通过history.pushState()方法改变地址栏
- 监听popState事件
- 根据当前路由地址找到对应组件重新渲染
- window.history.pushState 不会触发 popstate 事件,当历史状态被激活的时候才会触发 popstate 事件
- VueRouter 模拟实现-分析
- 类图
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4RNhFRIA-1626339777707)(./img/1626159203905.jpg)]
- VueRouter-install
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-v2jUcdPd-1626339777707)(./img/1626159804831.jpg)]
-
VueRouter-构造函数
-
VueRouter-createRouteMap
-
VueRouter-router-link
- Vue 的构建版本
- 运行时版:不支持template 模版,需要打包的时候提前编译
- 完整版:包含运行时和编译器,体积比运行时版大10k左右,程序运行的时候把模版转换成render 函数
- VueRouter-完整版的 Vue
- vue cli 官方文档》参考配置 》 vue.config.js > module.exports = { runtimeCompiler: true } 默认是运行时版本
-
VueRouter-render
-
VueRouter-router-view
-
VueRouter-initEvent
- 解决点击浏览器前进后退按钮,url变化,页面不改变的问题
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: '<a :href="to"><slot></slot></a>' 必须使用完整版才能进行编译
})
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
})
}
}
任务三:模拟 Vue.js 响应式原理
- 课程目标
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O9Vtq8AA-1626339777708)(./img/1626179384272.jpg)]
- 数据驱动
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6J11HE0H-1626339777709)(./img/1626179681122.jpg)]
- 数据响应式核心原理-Vue2
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GfvtXfi3-1626339777710)(./img/1626180156460.jpg)]
// <div id="app">
// hello
// </div>
// <script>
// 模拟 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)
// </script>
- 数据响应式核心原理-Vue3
- 直接监听对象,而非属性
- ES 6中新增,性能由浏览器优化
// 模拟 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)这个信号,从而知道什么时候直接可以开始执行。这就叫“发布订阅模式”
// 事件触发器
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')
- 观察者模式
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x8Dt3WBI-1626339777710)(./img/1626183388281.jpg)]
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yFjBzQUx-1626339777711)(./img/1626183904816.jpg)]
// 发布者-目标
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()
- 拟Vue响应式原理-分析
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OC00IXfz-1626339777712)(./img/1626184314915.jpg)]
- Vue
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i4onXWWl-1626339777712)(./img/1626184452978.jpg)]
<div id="app">
<h1>差值表达式</h1>
<h3>{{ msg }}</h3>
<h3>{{ count }}</h3>
<h1>v-text</h1>
<div v-text="msg"></div>
<h1>v-model</h1>
<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
}
})
})
}
}
- Observer
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iFfhVvp3-1626339777713)(./img/1626184874727.jpg)]
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()
}
})
}
- Observer-defineReactive1
- 解释了defineReactive(obj, key, val) 为什么要使用第三个参数的问题
- 因为直接用obj[key]会触发这个key对应的get方法,造成无限循环调用的异常
- Observer-defineReactive2
- 解决两个问题:
- data中的属性是对象,把这个对象内部的数据转换为响应式数据。
- data中的属性,重新赋值为一个对象,该对象的属性也会被转换为响应式的
- Compiler
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G2pJIgEv-1626339777714)(./img/1626224481337.jpg)]
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
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4qJQeHHF-1626339777714)(./img/1626226189004.jpg)]
class Dep {
constructor () {
// 存储所有的观察者
this.subs = []
}
// 添加观察者
addSub (sub) {
if (sub && sub.update) {
this.subs.push(sub)
}
}
// 发送通知
notify () {
this.subs.forEach(sub => {
sub.update()
})
}
}
- Watcher
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gM9B9GFz-1626339777715)(./img/1626226548554.jpg)]
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
-
双向绑定
-
调试-首次渲染
- 调试页面首次渲染的过程
- 调试数据改变更新视图的过程
-
调试-数据改变
-
总结-问题
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H5g0gMCY-1626339777716)(./img/1626228962620.jpg)]
- 给属性重新赋值成对象,是响应式的,因为这个属性在vue初始化时已经设置了get和set属性
- 给Vue 实例新增一个成员不是响应式的,因为只有在vue 初始化时候才会给属性设置get和set方法。vue可以通过Vue.set方法解决
- 总结-整体流程
任务四:Virtual DOM 的实现原理
- 课程目标
- 了解什么是虚拟DOM,以及虚拟DOM的作用
- Snabbdom 的基本使用
- Snabbdom 的源码解析
- 什么是虚拟DOM
- Virtual DDM(虚拟DOM)是有普通的JS对象来描述DOM对象
- 真是DOM对象成员(属性)非常多,创建起来成本是非常高的。虚拟DOM成员很少,所以创建成本比真实DOM小很多
- 为什么使用虚拟DOM
- MVVM 框架解决视图和状态同步问题
- 模版引擎可以简化视图操作,没把跟踪状态。比如一个列表的一个元素修改来,那么整个列表都要重新渲染一次
- 虚拟 DOM 跟踪状态变化
- 参考 github 上 virtual-dom 的描述
- 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
- 通过比较前后两次状态的差异更新真实 DOM
- 虚拟DOM的作用和虚拟DOM库
- 虚拟 DOM 的作用
- 维护视图和状态的关系
- 复杂视图情况下提升渲染性能
- 除了渲染 DOM 以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等
- 虚拟DOM库
- Snabbdom
- Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom
- 大约 200 SLOC(single line of code)
- 通过模块可扩展
- 源码使用 TypeScript 开发
- 最快的 Virtual DOM 之一
- virtual-dom
- Snabbdom
- 创建项目
-
创建项目,并安装
# 创建项目目录 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
- 导入 Snabbdom
-
文档地址
- https://github.com/snabbdom/snabbdom
- 当前版本 v2.1.0
# --depth 表示克隆深度, 1 表示只克隆最新的版本. 因为如果项目迭代的版本很多, 克隆会很慢 git clone -b v2.1.0 --depth=1 https://github.com/snabbdom/snabbdom.git
-
安装 Snabbdom
npm install snabbdom@2.1.0
-
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
- 案例1
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NFi3Mnxa-1626339777716)(./img/1626247531102.jpg)]
- 案例2
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jfpvh8jV-1626339777717)(./img/1626248032342.jpg)]
- 模块的使用
- Snabbdom 的核心库并不能处理DOM 元素的属性/样式,事件等,可以通过支持Snabbdom默认提供的模块来实现
- Snabbdom 中的模块可以用来扩展Snabbdom的功能
- Snabbdom 中的模块的实现是通过注册全局的钩子函数来实现的
- 官方提供了 6 个模块
- attributes
- 设置 DOM 元素的属性,使用
setAttribute
() - 处理布尔类型的属性
- 设置 DOM 元素的属性,使用
- props
- 和
attributes
模块相似,设置 DOM 元素的属性element[attr] = value
- 不处理布尔类型的属性
- 和
- class
- 切换类样式
- 注意:给元素设置类样式是通过
sel
选择器
- dataset
- 设置
data-*
的自定义属性
- 设置
- eventlisteners
- 注册和移除事件
- style
- 设置行内样式,支持动画
- delayed/remove/destroy
- attributes
- 模块使用步骤:
- 导入需要的模块
- init() 中注册模块
- 使用 h() 函数创建 VNode 的时候,可以把第二个参数设置为对象,其他参数往后移
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)
- 概述
- init() 设置模块,创建 patch()
- 使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
- patch() 比较新旧两个 VNode
- 把变化的内容更新到真实 DOM 树上
- h 函数
-
h() 函数介绍
-
在使用 Vue 的时候见过 h() 函数
new Vue({ router, store, render: h => h(App) }).$mount('#app')
-
h() 函数最早见于 hyperscript,使用 JavaScript 创建超文本
-
Snabbdom 中的 h() 函数不是用来创建超文本,而是创建 VNode
-
-
重载
- 参数个数或类型不同的函数
- JavaScript 中没有重载的概念
- TypeScript 中有重载,不过重载的实现还是通过代码调整参数
-
源码位置: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
- 一个 VNode 就是一个虚拟节点用来描述一个 DOM 元素,如果这个 VNode 有 children 就是 Virtual DOM
- 源码位置:src/package/vnode.ts
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 }
}
- patch 整体过程分析
- patch(oldVnode, newVnode)
- 打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
- 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
- 如果不是相同节点,删除之前的内容,重新渲染
- 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
- 如果新的 VNode 有 children,判断子节点是否有变化,判断子节点的过程使用的就是 diff 算法
- diff 过程只进行同层级比较
- 源码位置:src/package/init.ts
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
-
**功能:**init(modules, domApi),返回 patch() 函数(高阶函数)
- domApi 指当前平台的真实dom对象提供的api,为了跨平台做的实现,默认是 浏览器的dom对象api
-
为什么要使用高阶函数?
- 因为 patch() 函数再外部会调用多次,每次调用依赖一些参数,比如:modules/domApi/cbs
- 通过高阶函数让 init() 内部形成闭包,返回的 patch() 可以访问到 modules/domApi/cbs,而不需要重新创建
-
init() 在返回 patch() 之前,首先收集了所有模块中的钩子函数存储到 cbs 对象中
- patch
-
执行过程:
- 首先执行模块中的钩子函数
pre
- 如果 oldVnode 和 vnode 相同(key 和 sel 相同)
- 调用 patchVnode(),找节点的差异并更新 DOM
- 如果 oldVnode 是 DOM 元素
- 把 DOM 元素转换成 oldVnode
- 调用 createElm() 把 vnode 转换为真实 DOM,记录到 vnode.elm
- 把刚创建的 DOM 元素插入到 parent 中
- 移除老节点
- 触发用户设置的
create
钩子函数
- 首先执行模块中的钩子函数
- 调试 patch 函数
- 源码位置:src/package/init.ts
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
}
- createElm
- 功能:
- createElm(vnode, insertedVnodeQueue),返回创建的 DOM 元素
- 创建 vnode 对应的 DOM 元素
- 执行过程:
- 首先触发用户设置的 init 钩子函数
- 如果选择器是!,创建评论节点
- 如果选择器为空,创建文本节点
- 如果选择器不为空
- 解析选择器,设置标签的 id 和 class 属性
- 执行模块的 create 钩子函数
- 如果 vnode 有 children,创建子 vnode 对应的 DOM,追加到 DOM 树
- 如果 vnode 的 text 值是 string/number,创建文本节点并追击到 DOM 树
- 执行用户设置的 create 钩子函数
- 如果有用户设置的 insert 钩子函数,把 vnode 添加到队列中
- 源码位置:src/package/init.ts
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
-
功能:
- patchVnode(oldVnode, vnode, insertedVnodeQueue)
- 对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM
-
执行过程:
- 首先执行用户设置的 prepatch 钩子函数
- 执行 create 钩子函数
- 首先执行模块的 create 钩子函数
- 然后执行用户设置的 create 钩子函数
- 如果 vnode.text 未定义
- 如果
oldVnode.children
和vnode.children
都有值- 调用
updateChildren()
- 使用 diff 算法对比子节点,更新子节点
- 调用
- 如果
vnode.children
有值,oldVnode.children
无值- 清空 DOM 元素
- 调用
addVnodes()
,批量添加子节点
- 如果
oldVnode.children
有值,vnode.children
无值- 调用
removeVnodes()
,批量移除子节点
- 调用
- 如果 oldVnode.text 有值
- 清空 DOM 元素的内容
- 如果
- 如果设置了
vnode.text
并且和和oldVnode.text
不等- 如果老节点有子节点,全部移除
- 设置 DOM 元素的
textContent
为vnode.text
- 最后执行用户设置的 postpatch 钩子函数
-
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)
}
- updateChildren 整体过程
-
功能:
- diff 算法的核心,对比新旧节点的 children,更新 DOM
-
执行过程:
- 要对比两棵树的差异,我们可以取第一棵树的每一个节点依次和第二课树的每一个节点比较,但是这样的时间复杂度为 O(n^3)
- 在DOM 操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点
- 因此只需要找同级别的子节点依次比较,然后再找下一级别的节点比较,这样算法的时间复杂度为 O(n)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XIClwzmC-1626339777718)(./img/image-20200102103653779.png)]
- 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引
- 在对开始和结束节点比较的时候,总共有四种情况
- oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
- oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
- oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点)
- oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JVtGzUXI-1626339777719)(./img/image-20200109184608649.png)]
- 开始节点和结束节点比较,这两种情况类似
- oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
- oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
- 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同)
- 调用 patchVnode() 对比和更新节点
- 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-308FnApV-1626339777720)(./img/image-20200103121812840.png)]
- oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同
- 调用 patchVnode() 对比和更新节点
- 把 oldStartVnode 对应的 DOM 元素,移动到右边
- 更新索引
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gjqz3rvO-1626339777720)(./img/image-20200103125428541.png)]
- oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同
- 调用 patchVnode() 对比和更新节点
- 把 oldEndVnode 对应的 DOM 元素,移动到左边
- 更新索引
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l3id3hci-1626339777721)(./img/image-20200103125735048.png)]
- 如果不是以上四种情况
- 遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点
- 如果没有找到,说明 newStartNode 是新节点
- 创建新节点对应的 DOM 元素,插入到 DOM 树中
- 如果找到了
- 判断新节点和找到的老节点的 sel 选择器是否相同
- 如果不相同,说明节点被修改了
- 重新创建对应的 DOM 元素,插入到 DOM 树中
- 如果相同,把 elmToMove 对应的 DOM 元素,移动到左边
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nFhnua1q-1626339777722)(./img/image-20200109184822439.png)]
- 循环结束
- 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束
- 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束
- 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yviyewni-1626339777722)(./img/image-20200103150918335.png)]
- 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批量删除
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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)
- 调试带 key 的情况
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)
- Key 的意义
- 给具有相同父元素的子元素设置唯一key,否则的话可能造成渲染错误。因为同级子元素,比较的是sel选择器 和 key(优化),sel可能出现重复
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))