petite-vue源码剖析-v-if和v-for的工作原理
深入v-if
的工作原理
<div v-scope="App"></div>
<script type="module">
import { createApp } from 'https://unpkg.com/petite-vue?module'
createApp({
App: {
$template: `
<span v-if="status === 'offline'"> OFFLINE </span>
<span v-else-if="status === 'UNKOWN'"> UNKOWN </span>
<span v-else> ONLINE </span>
`,
}
status: 'online'
}).mount('[v-scope]')
</script>
人肉单步调试:
- 调用
createApp
根据入参生成全局作用域rootScope
,创建根上下文rootCtx
; - 调用
mount
为<div v-scope="App"></div>
构建根块对象rootBlock
,并将其作为模板执行解析处理; - 解析时识别到
v-scope
属性,以全局作用域rootScope
为基础运算得到局部作用域scope
,并以根上下文rootCtx
为蓝本一同构建新的上下文ctx
,用于子节点的解析和渲染; - 获取
$template
属性值并生成HTML元素; - 深度优先遍历解析子节点(调用
walkChildren
); - 解析
<span v-if="status === 'offline'"> OFFLINE </span>
解析<span v-if="status === 'offline'"> OFFLINE </span>
书接上一回,我们继续人肉单步调试:
- 识别元素带上
v-if
属性,调用_if
原指令对元素及兄弟元素进行解析; - 将附带
v-if
和跟紧其后的附带v-else-if
和v-else
的元素转化为逻辑分支记录; - 循环遍历分支,并为逻辑运算结果为
true
的分支创建块对象并销毁原有分支的块对象(首次渲染没有原分支的块对象),并提交渲染任务到异步队列。
// 文件 ./src/walk.ts
// 为便于理解,我对代码进行了精简
export const walk = (node: Node, ctx: Context): ChildNode | null | void {
const type = node.nodeType
if (type == 1) {
// node为Element类型
const el = node as Element
let exp: string | null
if ((exp = checkAttr(el, 'v-if'))) {
return _if(el, exp, ctx) // 返回最近一个没有`v-else-if`或`v-else`的兄弟节点
}
}
}
// 文件 ./src/directives/if.ts
interface Branch {
exp?: string | null // 该分支逻辑运算表达式
el: Element // 该分支对应的模板元素,每次渲染时会以该元素为模板通过cloneNode复制一个实例插入到DOM树中
}
export const _if = (el: Element, exp: string, ctx: Context) => {
const parent = el.parentElement!
/* 锚点元素,由于v-if、v-else-if和v-else标识的元素可能在某个状态下都不位于DOM树上,
* 因此通过锚点元素标记插入点的位置信息,当状态发生变化时则可以将目标元素插入正确的位置。
*/
const anchor = new Comment('v-if')
parent.insertBefore(anchor, el)
// 逻辑分支,并将v-if标识的元素作为第一个分支
const branches: Branch[] = [
{
exp,
el
}
]
/* 定位v-else-if和v-else元素,并推入逻辑分支中
* 这里没有控制v-else-if和v-else的出现顺序,因此我们可以写成
* <span v-if="status=0"></span><span v-else></span><span v-else-if="status === 1"></span>
* 但效果为变成<span v-if="status=0"></span><span v-else></span>,最后的分支永远没有机会匹配。
*/
let elseEl: Element | null
let elseExp: string | null
while ((elseEl = el.nextElementSibling)) {
elseExp = null
if (
checkAttr(elseEl, 'v-else') === '' ||
(elseExp = checkAttr(elseEl, 'v-else-if'))
) {
// 从在线模板移除分支节点
parent.removeChild(elseEl)
branches.push({ exp: elseExp, el: elseEl })
}
else {
break
}
}
// 保存最近一个不带`v-else`和`v-else-if`节点作为下一轮遍历解析的模板节点
const nextNode = el.nextSibling
// 从在线模板移除带`v-if`节点
parent.removeChild(el)
let block: Block | undefined // 当前逻辑运算结构为true的分支对应块对象
let activeBranchIndex: number = -1 // 当前逻辑运算结构为true的分支索引
// 若状态发生变化导致逻辑运算结构为true的分支索引发生变化,则需要销毁原有分支对应块对象(包含中止旗下的副作用函数监控状态变化,执行指令的清理函数和递归触发子块对象的清理操作)
const removeActiveBlock = () => {
if (block) {
// 重新插入锚点元素来定位插入点
parent.insertBefore(anchor, block.el)
block.remove()
// 解除对已销毁的块对象的引用,让GC回收对应的JavaScript对象和detached元素
block = undefined
}
}
// 向异步任务对立压入渲染任务,在本轮Event Loop的Micro Queue执行阶段会执行一次
ctx.effect(() => {
for (let i = 0; i < branches.length; i++) {
const { exp, el } = branches[i]
if (!exp || evaluate(ctx.scope, exp)) {
if (i !== activeBranchIndex) {
removeActiveBlock()
block = new Block(el, ctx)
block.insert(parent, anchor)
parent.removeChild(anchor)
activeBranchIndex = i
}
return
}
}
activeBranchIndex = -1
removeActiveBlock()
})
return nextNode
}
下面我们看看子块对象的构造函数和insert
、remove
方法
// 文件 ./src/block.ts
export class Block {
constuctor(template: Element, parentCtx: Context, isRoot = false) {
if (isRoot) {
// ...
}
else {
// 以v-if、v-else-if和v-else分支的元素作为模板创建元素实例
this.template = template.cloneNode(true) as Element
}
if (isRoot) {
// ...
}
else {
this.parentCtx = parentCtx
parentCtx.blocks.push(this)
this.ctx = createContext(parentCtx)
}
}
// 由于当前示例没有用到<template>元素,因此我对代码进行了删减
insert(parent: Element, anchor: Node | null = null) {
parent.insertBefore(this.template, anchor)
}
// 由于当前示例没有用到<template>元素,因此我对代码进行了删减
remove() {
if (this.parentCtx) {
// TODO: function `remove` is located at @vue/shared
remove(this.parentCtx.blocks, this)
}
// 移除当前块对象的根节点,其子孙节点都一并被移除
this.template.parentNode!.removeChild(this.template)
this.teardown()
}
teardown() {
// 先递归调用子块对象的清理方法
this.ctx.blocks.forEach(child => {
child.teardown()
})
// 包含中止副作用函数监控状态变化
this.ctx.effects.forEach(stop)
// 执行指令的清理函数
this.ctx.cleanups.forEach(fn => fn())
}
}
深入v-for
的工作原理
<div v-scope="App"></div>
<script type="module">
import { createApp } from 'https://unpkg.com/petite-vue?module'
createApp({
App: {
$template: `
<select>
<option v-for="val of values" v-key="val">
I'm the one of options
</option>
</select>
`,
}
values: [1,2,3]
}).mount('[v-scope]')
</script>
人肉单步调试:
- 调用
createApp
根据入参生成全局作用域rootScope
,创建根上下文rootCtx
; - 调用
mount
为<div v-scope="App"></div>
构建根块对象rootBlock
,并将其作为模板执行解析处理; - 解析时识别到
v-scope
属性,以全局作用域rootScope
为基础运算得到局部作用域scope
,并以根上下文rootCtx
为蓝本一同构建新的上下文ctx
,用于子节点的解析和渲染; - 获取
$template
属性值并生成HTML元素; - 深度优先遍历解析子节点(调用
walkChildren
); - 解析
<option v-for="val in values" v-key="val">I'm the one of options</option>
解析<option v-for="val in values" v-key="val">I'm the one of options</option>
书接上一回,我们继续人肉单步调试:
- 识别元素带上
v-for
属性,调用_for
原指令对该元素解析; - 通过正则表达式提取
v-for
中集合和集合元素的表达式字符串,和key
的表达式字符串; - 基于每个集合元素创建独立作用域,并创建独立的块对象渲染元素。
// 文件 ./src/walk.ts
// 为便于理解,我对代码进行了精简
export const walk = (node: Node, ctx: Context): ChildNode | null | void {
const type = node.nodeType
if (type == 1) {
// node为Element类型
const el = node as Element
let exp: string | null
if ((exp = checkAttr(el, 'v-for'))) {
return _for(el, exp, ctx) // 返回最近一个没有`v-else-if`或`v-else`的兄弟节点
}
}
}
// 文件 ./src/directives/for.ts
/* [\s\S]*表示识别空格字符和非空格字符若干个,默认为贪婪模式,即 `(item, index) in value` 就会匹配整个字符串。
* 修改为[\s\S]*?则为懒惰模式,即`(item, index) in value`只会匹配`(item, index)`
*/
const forAliasRE = /([\s\S]*?)\s+(?:in)\s+([\s\S]*?)/
// 用于移除`(item, index)`中的`(`和`)`
const stripParentRE= /^\(|\)$/g
// 用于匹配`item, index`中的`, index`,那么就可以抽取出value和index来独立处理
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
type KeyToIndexMap = Map<any, number>
// 为便于理解,我们假设只接受`v-for="val in values"`的形式,并且所有入参都是有效的,对入参有效性、解构等代码进行了删减
export const _for = (el: Element, exp: string, ctx: Context) => {
// 通过正则表达式抽取表达式字符串中`in`两侧的子表达式字符串
const inMatch = exp.match(forAliasRE)
// 保存下一轮遍历解析的模板节点
const nextNode = el.nextSibling
// 插入锚点,并将带`v-for`的元素从DOM树移除
const parent = el.parentElement!
const anchor = new Text('')
parent.insertBefore(anchor, el)
parent.removeChild(el)
const sourceExp = inMatch[2].trim() // 获取`(item, index) in value`中`value`
let valueExp = inMatch[1].trim().replace(stripParentRE, '').trim() // 获取`(item, index) in value`中`item, index`
let indexExp: string | undefined
let keyAttr = 'key'
let keyExp =
el.getAttribute(keyAttr) ||
el.getAttribute(keyAttr = ':key') ||
el.getAttribute(keyAttr = 'v-bind:key')
if (keyExp) {
el.removeAttribute(keyExp)
// 将表达式序列化,如`value`序列化为`"value"`,这样就不会参与后面的表达式运算
if (keyAttr === 'key') keyExp = JSON.stringify(keyExp)
}
let match
if (match = valueExp.match(forIteratorRE)) {
valueExp = valueExp.replace(forIteratorRE, '').trim() // 获取`item, index`中的item
indexExp = match[1].trim() // 获取`item, index`中的index
}
let mounted = false // false表示首次渲染,true表示重新渲染
let blocks: Block[]
let childCtxs: Context[]
let keyToIndexMap: KeyToIndexMap // 用于记录key和索引的关系,当发生重新渲染时则复用元素
const createChildContexts = (source: unknown): [Context[], KeyToIndexMap] => {
const map: KeyToIndexMap = new Map()
const ctxs: Context[] = []
if (isArray(source)) {
for (let i = 0; i < source.length; i++) {
ctxs.push(createChildContext(map, source[i], i))
}
}
return [ctxs, map]
}
// 以集合元素为基础创建独立的作用域
const createChildContext = (
map: KeyToIndexMap,
value: any, // the item of collection
index: number // the index of item of collection
): Context => {
const data: any = {}
data[valueExp] = value
indexExp && (data[indexExp] = index)
// 为每个子元素创建独立的作用域
const childCtx = createScopedContext(ctx, data)
// key表达式在对应子元素的作用域下运算
const key = keyExp ? evaluate(childCtx.scope, keyExp) : index
map.set(key, index)
childCtx.key = key
return childCtx
}
// 为每个子元素创建块对象
const mountBlock = (ctx: Conext, ref: Node) => {
const block = new Block(el, ctx)
block.key = ctx.key
block.insert(parent, ref)
return block
}
ctx.effect(() => {
const source = evaluate(ctx.scope, sourceExp) // 运算出`(item, index) in items`中items的真实值
const prevKeyToIndexMap = keyToIndexMap
// 生成新的作用域,并计算`key`,`:key`或`v-bind:key`
;[childCtxs, keyToIndexMap] = createChildContexts(source)
if (!mounted) {
// 为每个子元素创建块对象,解析子元素的子孙元素后插入DOM树
blocks = childCtxs.map(s => mountBlock(s, anchor))
mounted = true
}
// 由于我们示例只研究静态视图,因此重新渲染的代码,我们后面再深入了解吧
})
return nextNode
}
总结
我们看到在v-if
和v-for
的解析过程中都会生成块对象,而且是v-if
的每个分支都对应一个块对象,而v-for
则是每个子元素都对应一个块对象。其实块对象不单单是管控DOM操作的单元,而且它是用于表示树结构不稳定的部分。如节点的增加和删除,将导致树结构的不稳定,把这些不稳定的部分打包成独立的块对象,并封装各自构建和删除时执行资源回收等操作,这样不仅提高代码的可读性也提高程序的运行效率。
v-if
的首次渲染和重新渲染采用同一套逻辑,但v-for
在重新渲染时会采用key
复用元素从而提高效率,可以重新渲染时的算法会复制不少。下一篇我们将深入了解v-for
在重新渲染时的工作原理,敬请期待:)
petite-vue源码剖析-v-if和v-for的工作原理的更多相关文章
- jdk源码剖析二: 对象内存布局、synchronized终极原理
很多人一提到锁,自然第一个想到了synchronized,但一直不懂源码实现,现特地追踪到C++层来剥开synchronized的面纱. 网上的很多描述大都不全,让人看了不够爽,看完本章,你将彻底了解 ...
- Spark源码剖析(七):Job触发流程原理与源码剖析
引言 我们知道在application中每存在一个action操作就会触发一个job,那么spark底层是怎样触发job的呢?接下来我们用一个wordcount程序来剖析一下job的触发机制. 解析w ...
- Spring源码剖析5:JDK和cglib动态代理原理详解
AOP的基础是Java动态代理,了解和使用两种动态代理能让我们更好地理解 AOP,在讲解AOP之前,让我们先来看看Java动态代理的使用方式以及底层实现原理. 转自https://www.jiansh ...
- spring源码分析之@ImportSelector、@Import、ImportResource工作原理分析
1. @importSelector定义: /** * Interface to be implemented by types that determine which @{@link Config ...
- jdk源码剖析:Synchronized
开启正文之前,先说一下源码剖析这一系列,就以"死磕到底"的精神贯彻始终,最少追踪到JVM指令(再往下C语言实现了). =========正文分割线=========== Sync ...
- jdk源码剖析三:锁Synchronized
一.Synchronized作用 (1)确保线程互斥的访问同步代码 (2)保证共享变量的修改能够及时可见 (3)有效解决重排序问题.(Synchronized同步中的代码JVM不会轻易优化重排序) 二 ...
- petite-vue源码剖析-逐行解读@vue/reactivity之reactive
在petite-vue中我们通过reactive构建上下文对象,并将根据状态渲染UI的逻辑作为入参传递给effect,然后神奇的事情发生了,当状态发生变化时将自动触发UI重新渲染.那么到底这是怎么做到 ...
- 逐行剖析Vue源码(一)——写在最前面
1. 前言 博主作为一名前端开发,日常开发的技术栈是Vue,并且用Vue开发也有一年多了,对其用法也较为熟练了,但是对各种用法和各种api使用都是只知其然而不知其所以然,因此,有时候在排查bug的时候 ...
- socket_server源码剖析、python作用域、IO多路复用
本节内容: 课前准备知识: 函数嵌套函数的使用方法: 我们在使用函数嵌套函数的时候,是学习装饰器的时候,出现过,由一个函数返回值是一个函数体情况. 我们在使用函数嵌套函数的时候,最好也这么写. def ...
- 菜鸟nginx源码剖析 框架篇(一) 从main函数看nginx启动流程(转)
俗话说的好,牵牛要牵牛鼻子 驾车顶牛,处理复杂的东西,只要抓住重点,才能理清脉络,不至于深陷其中,不能自拔.对复杂的nginx而言,main函数就是“牛之鼻”,只要能理清main函数,就一定能理解其中 ...
随机推荐
- python函数位置实参传参
#!/usr/bin/python #coding=utf-8 #好好学习,天天向上 def describe_pet(type,name): print(f"i have a {type} ...
- 微服务架构 | 5.4 Sentinel 流控、统计和熔断的源码分析
目录 前言 1. Sentinel 的自动装配 1.2 依赖引入 1.3 SentinelWebAutoConfiguration 配置类 1.4 CommonFilter 过滤器 1.5 小结 2. ...
- 「JSOI2018」机器人
在本题当中为了方便,我们将坐标范围改至 \((0 \sim n - 1, 0 \sim m - 1)\),行走即可视作任意一维在模意义下 \(+1\). 同时,注意到一个位置只能经过一次,则可以令 \ ...
- Atcoder ARC-070
A 可以发现的是,次数的下界一定是使得 \(\frac{n(n + 1)}{2} \ge X\) 最小的 \(n\). 稍加思考可以发现,只需要在某一时刻停一下一定能在下界的次数内跳到恰好 \(X\) ...
- uniapp自定义顶部搜索框兼容微信小程序
zhuanzai: uniapp自定义顶部搜索框兼容微信小程序 自定义组件 navbarvue (胶囊底部高度 - 状态栏的高度) + (胶囊顶部高度 - 状态栏内的高度) = 导航栏的高度 < ...
- 使用ajax上传文件
1. XMLHttpRequest(原生ajax) [](javascript:void(0) <input class="file" type="file&quo ...
- 用代码创建并实例化在storyboard中声明的ViewController
我们的项目最早是基于storyboard开发的,所以一开始所有的ViewController都通过storyboard创建,并通过segue连接跳转 但是今天其中一个controller的view,我 ...
- LAMP架构—源码编译安装 (爱情受过伤,为爱跳过鸭绿江)
LAMP架构--源码编译安装 1.LAMP架构概述 2.编译安装Apache httpd 服务 3.编译安装mysql 服务 4.编译安装PHP 解析服务 5.利用LAMP搭建论坛 1.LAMP架构概 ...
- DNS域名解析之正向解析
DNS域名解析之正向解析 1.DNS介绍 2.DNS正向解析实验 1.DNS定义:DNS是"域名系统"的英文缩写.它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地 ...
- 使用污点分析检查log4j问题
摘要:log4j问题的余波还在继续,为什么这个问题潜伏了这么长时间,大家一直没有发现?这里从静态分析的角度谈下log4j问题的发现. 本文分享自华为云社区<使用污点分析检查log4j问题> ...