Давным-давно я написал статью, проанализировал и реализовал простой вариантvdom. Вы можете нажать, если хотите увидетьпортал
Давайте поговорим о том, почему я хочу написать такую статью еще раз.Это действительно в проекте, будь то вы или ваши коллеги, вы столкнетесь с этой ямой более или менее. Итак, вот краткое изложение для маленьких друзей, надеюсь, вы прочтете его и окажетесь правы.vueсерединаvdomиметь лучшее восприятие. Хорошо, давайте начнем
Во-первых, бросить проблему
Прежде чем я начну, позвольте мне задать вопрос, над которым вы можете сначала подумать, а затем прочитать следующие страницы. код сначала
<template>
<el-select
class="test-select"
multiple
filterable
remote
placeholder="请输入关键词"
:remote-method="remoteMethod"
:loading="loading"
@focus="handleFoucs"
v-model="items">
<!-- 这里 option 的 key 直接绑定 vfor 的 index -->
<el-option
v-for="(item, index) in options"
:key="index"
:label="item.label"
:value="item.value">
<el-checkbox
:label="item.value"
:value="isChecked(item.value)">
{{ item.label }}
</el-checkbox>
</el-option>
</el-select>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
@Component
export default class TestSelect extends Vue {
options: Array<{ label: string, value: string }> = []
items: Array<string> = []
list: Array<{ label: string, value: string }> = []
loading: boolean = false
states = ['Alabama', 'Alaska', 'Arizona',
'Arkansas', 'California', 'Colorado',
'Connecticut', 'Delaware', 'Florida',
'Georgia', 'Hawaii', 'Idaho', 'Illinois',
'Indiana', 'Iowa', 'Kansas', 'Kentucky',
'Louisiana', 'Maine', 'Maryland',
'Massachusetts', 'Michigan', 'Minnesota',
'Mississippi', 'Missouri', 'Montana',
'Nebraska', 'Nevada', 'New Hampshire',
'New Jersey', 'New Mexico', 'New York',
'North Carolina', 'North Dakota', 'Ohio',
'Oklahoma', 'Oregon', 'Pennsylvania',
'Rhode Island', 'South Carolina',
'South Dakota', 'Tennessee', 'Texas',
'Utah', 'Vermont', 'Virginia',
'Washington', 'West Virginia', 'Wisconsin',
'Wyoming']
mounted () {
this.list = this.states.map(item => {
return { value: item, label: item }
})
}
remoteMethod (query) {
if (query !== '') {
this.loading = true
setTimeout(() => {
this.loading = false
this.options = this.list.filter(item => {
return item.label.toLowerCase()
.indexOf(query.toLowerCase()) > -1
})
}, 200)
} else {
this.options = this.list
}
}
handleFoucs (e) {
this.remoteMethod(e.target.value)
}
isChecked (value: string): boolean {
let checked = false
this.items.forEach((item: string) => {
if (item === value) {
checked = true
}
})
return checked
}
}
</script>
После входной фильтрации диаграмма эффекта выглядит следующим образом
Затем я ищу другое ключевое слово, и результаты покажут следующие проблемы
Я не делал выбор, но значение, отображаемое в поле выбора, изменилось. Старые драйверы могут узнать о проблеме, как только прочитают код. На самом деле, поместите опцию внутриkeyМожно изменить привязку и заменить ее следующим
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value">
<el-checkbox
:label="item.value"
:value="isChecked(item.value)">
{{ item.label }}
</el-checkbox>
</el-option>
Итак, возникает вопрос, это позволяет избежать проблемы, но почему ее избегают? На самом деле, эта часть включает в себя контент, связанный с патчем, в vdom. Далее я отвезу вас снова забрать вдом.
Прежде чем приступить к работе, взгляните на несколько API, которые часто появляются ниже.
isDef()
export function isDef (v: any): boolean %checks {
return v !== undefined && v !== null
}
isUndef()
export function isUndef (v: any): boolean %checks {
return v === undefined || v === null
}
isTrue()
export function isTrue (v: any): boolean %checks {
return v === true
}
2. класс VNode
Прежде чем мы начнем, позвольте мне рассказать оVNode,vueсерединаvdomНа самом делеvnodeобъект.
правильноvdomСтуденты, которые немного знают об этом, должны знать, что,vdomСуть создания узла состоит в том, чтобы создать дерево объектов js, которое абстрагирует реальный дом, а затем выполнить ряд операций (о конкретных операциях я расскажу позже). В этой главе мы будем говорить только оvnodeреализация
1. Конструктор
Во-первых, мы можем посмотреть,VNodeКакие свойства этот класс предоставляет нам, пользователям, выберите некоторые из общих, которые мы видим
constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component
) {
this.tag = tag // 节点的标签名
this.data = data // 节点的数据信息,如 props,attrs,key,class,directives 等
this.children = children // 节点的子节点
this.text = text // 节点对应的文本
this.elm = elm // 节点对应的真实节点
this.context = context // 节点上下文,为 Vue Component 的定义
this.key = data && data.key // 节点用作 diff 的唯一标识
}
2. Например
Теперь давайте возьмем пример, если мне нужно разобрать следующий текст
<template>
<div class="vnode" :class={ 'show-node': isShow } v-show="isShow">
This is a vnode.
</div>
</template>
Абстракция с js такая
function render () {
return new VNode(
'div',
{
// 静态 class
staticClass: 'vnode',
// 动态 class
class: {
'show-node': isShow
},
/**
* directives: [
* {
* rawName: 'v-show',
* name: 'show',
* value: isShow
* }
* ],
*/
// 等同于 directives 里面的 v-show
show: isShow,
[ new VNode(undefined, undefined, undefined, 'This is a vnode.') ]
}
)
}
Выражение после преобразования в vnode выглядит следующим образом
{
tag: 'div',
data: {
show: isShow,
// 静态 class
staticClass: 'vnode',
// 动态 class
class: {
'show-node': isShow
},
},
text: undefined,
children: [
{
tag: undefined,
data: undefined,
text: 'This is a vnode.',
children: undefined
}
]
}
Затем я смотрю на немного более сложный пример
<span v-for="n in 5" :key="n">{{ n }}</span>
Если бы вы позволили всем использовать js для абстрагирования объектов, как бы вы это сделали? в основном внутриv-forИнструкции, вы можете сначала попробовать это со своим собственным мышлением.
Хорошо, давайте не будем об этом, давайте взглянем на абстрактную обработку функции рендеринга ниже, которая на самом деле является циклическим рендерингом!
function render (val, keyOrIndex, index) {
return new VNode(
'span',
{
directives: [
{
rawName: 'v-for',
name: 'for',
value: val
}
],
key: val,
[ new VNode(undefined, undefined, undefined, val) ]
}
)
}
function renderList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => VNode
): ?Array<VNode> {
// 仅考虑 number 的情况
let ret: ?Array<VNode>, i, l, keys, key
ret = new Array(val)
for (i = 0; i < val; i++) {
ret[i] = render(i + 1, i)
}
return ret
}
renderList(5)
Выражение после преобразования в vnode выглядит следующим образом
[
{
tag: 'span',
data: {
key: 1
},
text: undefined,
children: [
{
tag: undefined,
data: undefined,
text: 1,
children: undefined
}
]
}
// 依次循环
]
3. что-то еще
были сделаныVNode CtorНекоторые свойства , а также взглянем на форму преобразования реального dom vnode, здесь мы восполним небольшое упущение и посмотрим наVNodeСделайте некоторую инкапсуляцию, чтобы открыть нам некоторые методы
// 创建一个空节点
export const createEmptyVNode = (text: string = '') => {
const node = new VNode()
node.text = text
node.isComment = true
return node
}
// 创建一个文本节点
export function createTextVNode (val: string | number) {
return new VNode(undefined, undefined, undefined, String(val))
}
// 克隆一个节点,仅列举部分属性
export function cloneVNode (vnode: VNode): VNode {
const cloned = new VNode(
vnode.tag,
vnode.data,
vnode.children,
vnode.text
)
cloned.key = vnode.key
cloned.isCloned = true
return cloned
}
чистыйVNodeСвязанные методы, следующие главы, представятvueкак будетvnodeсделать как реальныйdom
Три, рендер
1. создатьЭлемент
Прежде чем рассматривать реализацию createElement в vue, давайте взглянем на приватные методы в том же файле._createElementреализация. Среди них есть некоторые конкретные логические суждения для тегов
- tagName привязан к параметру данных
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
- Если tagName не существует, вернуть пустой узел
if (!tag) {
return createEmptyVNode()
}
- Когда tagName имеет тип string, он напрямую возвращает объект vnode, соответствующий тегу.
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
- Когда tagName не является строковым типом, выполните
createComponent()СоздаватьComponentобъект
vnode = createComponent(tag, data, context, children)
- Определите тип vnode и верните его соответствующим образом.
if (Array.isArray(vnode)) {
return vnode
} else if (isDef(vnode)) {
// namespace 相关处理
if (isDef(ns)) applyNS(vnode, ns)
// 进行 Observer 相关绑定
if (isDef(data)) registerDeepBindings(data)
return vnode
} else {
return createEmptyVNode()
}
createElement()заключается в выполнении_createElement()вернуть vnode
return _createElement(context, tag, data, children, normalizationType)
2. функции рендеринга
i. renderHelpers
Вот, давайте посмотрим на это в целом, и смонтируем его вVue.prototypeКакие методы связаны с рендерингом
export function installRenderHelpers (target: any) {
target._o = markOnce // v-once render 处理
target._n = toNumber // 值转换 Number 处理
target._s = toString // 值转换 String 处理
target._l = renderList // v-for render 处理
target._t = renderSlot // slot 槽点 render 处理
target._q = looseEqual // 判断两个对象是否大体相等
target._i = looseIndexOf // 对等属性索引,不存在则返回 -1
target._m = renderStatic // 静态节点 render 处理
target._f = resolveFilter // filters 指令 render 处理
target._k = checkKeyCodes // checking keyCodes from config
target._b = bindObjectProps // v-bind render 处理,将 v-bind="object" 的属性 merge 到VNode属性中
target._v = createTextVNode // 创建文本节点
target._e = createEmptyVNode // 创建空节点
target._u = resolveScopedSlots // scopeSlots render 处理
target._g = bindObjectListeners // v-on render 处理
}
затем вrenderMixin()метод, даVue.prototypeвыполнить операцию инициализации
export function renderMixin (Vue: Class<Component>) {
// render helps init 操作
installRenderHelpers(Vue.prototype)
// 定义 vue nextTick 方法
Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
Vue.prototype._render = function (): VNode {
// 此处定义 vm 实例,以及 return vnode。具体代码此处忽略
}
}
ii. Абстрактное синтаксическое дерево AST
Что мы видели до сих порrenderВсе связанные операции возвращаютvnodeобъект, и перед рендерингом реального узла vue проанализирует строку в шаблоне шаблона и преобразует ее в абстрактное синтаксическое дерево AST для облегчения последующих операций. Что касается этого, давайте посмотрим непосредственно на то, как vue определяется в типе потока.ASTElementТип интерфейса, так как проблема, возникшая в начале, вызванаv-forВызвали, то этот кусок, мы просто смотримASTElementДля его определения после прочтения не забудьте сделать выводы из исходного кода, чтобы понять другие определения 💪
declare type ASTElement = {
tag: string; // 标签名
attrsMap: { [key: string]: any }; // 标签属性 map
parent: ASTElement | void; // 父标签
children: Array<ASTNode>; // 子节点
for?: string; // 被 v-for 的对象
forProcessed?: boolean; // v-for 是否需要被处理
key?: string; // v-for 的 key 值
alias?: string; // v-for 的参数
iterator1?: string; // v-for 第一个参数
iterator2?: string; // v-for 第二个参数
};
iii. генерировать преобразование строки
renderList
заглянутьrender functionПеред преобразованием строк посмотрите наrenderListПараметры удобны для последующего чтения
export function renderList (
val: any,
render: (
val: any,
keyOrIndex: string | number,
index?: number
) => VNode
): ?Array<VNode> {
// 此处为 render 相关处理,具体细节这里就不列出来了,上文中有列出 number 情况的处理
}
genFor
Прочитав определение выше, давайте еще раз посмотрим,generateКак преобразовать AST в строку функции рендеринга, чтобы мы могли видеть правильноv-forсвязанная обработка
function genFor (
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
const exp = el.for // v-for 的对象
const alias = el.alias // v-for 的参数
const iterator1 = el.iterator1 ? `,${el.iterator1}` : '' // v-for 第一个参数
const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' // v-for 第二个参数
el.forProcessed = true // 指令需要被处理
// return 出对应 render function 字符串
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
genElement
Этот блок объединяет логику преобразования, соответствующую каждой инструкции.
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.staticRoot && !el.staticProcessed) { // 静态节点
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) { // v-once 处理
return genOnce(el, state)
} else if (el.for && !el.forProcessed) { // v-for 处理
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { // v-if 处理
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget) { // template 根节点处理
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') { // slot 节点处理
return genSlot(el, state)
} else {
// component or element 相关处理
}
}
generate
generateЭто интегрировать все вышеперечисленные методы в объект, гдеrenderНедвижимость соответствуетgenElementсопутствующие операции,staticRenderFnsСоответствующий массив строк.
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`, // render
staticRenderFns: state.staticRenderFns // render function 字符串数组
}
}
3. визуализировать каштаны
Прочитав так много выше, некоторые друзья, которые мало что знают о vue, могут почувствовать легкое головокружение.v-forПример рендеринга для понимания всеми.
i. demo
<div class="root">
<span v-for="n in 5" :key="n">{{ n }}</span>
</div>
Этот блок сначала будет преобразован в html-строку.
let html = `<div class="root">
<span v-for="n in 5" :key="n">{{ n }}</span>
</div>`
ii. Соответствующие правила
После получения html-строки в шаблоне она будет проанализирована. Конкретные связанные регулярные выражения находятся вsrc/compiler/parser/html-parser.jsВ нем упоминается, ниже приведены некоторые связанные регулярные выражения иdecoding mapОпределение.
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/
const decodingMap = {
'<': '<',
'>': '>',
'"': '"',
'&': '&',
' ': '\n',
'	': '\t'
}
const encodedAttr = /&(?:lt|gt|quot|amp);/g
const encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#10|#9);/g
iii. parseHTML
vueИспользуется шаблон парсингаwhileПри выполнении сопоставления строк в цикле каждый раз, когда строка анализируется, совпадающая часть будет удалена, а затемindexИндекс будет по-прежнему напрямую соответствовать остальным частям. конкретно оparseHTMLОпределение следующее.Поскольку статья уже довольно длинная, я опустил некоторую логику обычного указателя сопоставления циклов.Те, кто хочет знать больше, могут провести собственное исследование или подождать, пока я опубликую статью в следующий раз, чтобы обсудить это подробно логика.
export function parseHTML (html, options) {
const stack = [] // 用来存储解析好的标签头
const expectHTML = options.expectHTML
const isUnaryTag = options.isUnaryTag || no
const canBeLeftOpenTag = options.canBeLeftOpenTag || no
let index = 0 // 匹配指针索引
let last, lastTag
while (html) {
// 此处是对标签进行正则匹配的逻辑
}
// 清理剩余的 tags
parseEndTag()
// 循环匹配相关处理
function advance (n) {
index += n
html = html.substring(n)
}
// 起始标签相关处理
function parseStartTag () {
let match = {
tagName: start[1],
attrs: [],
start: index
}
// 一系列匹配操作,然后对 match 进行赋值
return match
}
function handleStartTag (match) {}
// 结束标签相关处理
function parseEndTag (tagName, start, end) {}
}
проходить черезparseHTML()После серии регулярных сопоставлений строка html будет преобразована в следующее содержимое AST.
{
'attrsMap': {
'class': 'root'
},
'staticClass': 'root', // 标签的静态 class
'tag': 'div', // 标签的 tag
'children': [{ // 子标签数组
'attrsMap': {
'v-for': "n in 5",
'key': n
},
'key': n,
'alias': "n", // v-for 参数
'for': 5, // 被 v-for 的对象
'forProcessed': true,
'tag': 'span',
'children': [{
'expression': '_s(item)', // toString 操作(上文有提及)
'text': '{{ n }}'
}]
}]
}
Здесь в сочетании с вышеизложеннымgenerateПреобразованиеrenderЭта часть логики.
В-четвертых, diff и patch
Упс, наконец-то пришло время дифов и патчей, об этом еще очень холодно думать.
1. Некоторые операции DOM API
Прежде чем рассматривать конкретный diff, давайте взглянем наplatforms/web/runtime/node-ops.jsНекоторые методы создания реального дома, определенные в , просто для ознакомленияdomAPI для связанных операций
-
createElement()СозданtagNameуказанный элемент HTML
export function createElement (tagName: string, vnode: VNode): Element {
const elm = document.createElement(tagName)
if (tagName !== 'select') {
return elm
}
if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
elm.setAttribute('multiple', 'multiple')
}
return elm
}
-
createTextNode()Создайте текстовый узел
export function createTextNode (text: string): Text {
return document.createTextNode(text)
}
-
createComment()Создать узел комментария
export function createComment (text: string): Comment {
return document.createComment(text)
}
-
insertBefore()Вставьте дочерний узел с указанным родительским узлом перед ссылочным узлом
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
parentNode.insertBefore(newNode, referenceNode)
}
-
removeChild()Удалить дочерний узел из DOM
export function removeChild (node: Node, child: Node) {
node.removeChild(child)
}
-
appendChild()Добавляет узел в конец списка дочерних узлов указанного родительского узла.
export function appendChild (node: Node, child: Node) {
node.appendChild(child)
}
-
parentNode()вернуться к родительскому узлу
export function parentNode (node: Node): ?Node {
return node.parentNode
}
-
nextSibling()вернуть родственный узел
export function nextSibling (node: Node): ?Node {
return node.nextSibling
}
-
tagName()Возвращает имя метки узла
export function tagName (node: Element): string {
return node.tagName
}
-
setTextContent()Установите текстовое содержимое узла
export function setTextContent (node: Node, text: string) {
node.textContent = text
}
2. Работа API в некоторых патчах
Совет: все API, которые мы перечислили выше, связаны со следующимиnodeOpsобъект
-
createElm()создать узел
function createElm (vnode, parentElm, refElm) {
if (isDef(vnode.tag)) { // 创建标签节点
vnode.elm = nodeOps.createElement(tag, vnode)
} else if (isDef(vnode.isComment)) { // 创建注释节点
vnode.elm = nodeOps.createComment(vnode.text)
} else { // 创建文本节点
vnode.elm = nodeOps.createTextNode(vnode.text)
}
insert(parentElm, vnode.elm, refElm)
}
-
insert()Вставить дочерний узел под указанный родительский узел
function insert (parent, elm, ref) {
if (isDef(parent)) {
if (isDef(ref)) { // 插入到指定 ref 的前面
if (ref.parentNode === parent) {
nodeOps.insertBefore(parent, elm, ref)
}
} else { // 直接插入到父节点后面
nodeOps.appendChild(parent, elm)
}
}
}
-
addVnodes()пакетный вызовcreateElm()создать узел
function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
createElm(vnodes[startIdx], parentElm, refElm)
}
}
-
removeNode()удалить узел
function removeNode (el) {
const parent = nodeOps.parentNode(el)
if (isDef(parent)) {
nodeOps.removeChild(parent, el)
}
}
-
removeNodes()Удалять узлы пакетами
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (isDef(ch)) {
removeNode(ch.elm)
}
}
}
-
sameVnode()Это тот же узел
function sameVnode (a, b) {
return (
a.key === b.key &&
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
)
}
-
sameInputType()иметь тот же тип ввода
function sameInputType (a, b) {
if (a.tag !== 'input') return true
let i
const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type
const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type
return typeA === typeB
}
3. Разница узлов
I. Соответствующая блок-схема
Говоря об этом, первый ход (украсть) используйте две связанные картинки в моей предыдущей статье.
2. Операции сравнения и исправления объединены в одну.
Друзья, читавшие мои предыдущие статьи, должны знать, что diff и patch в моих предыдущих статьях реализованы в два этапа. иvueВ нем операции diff и patch объединены в одну. Теперь давайте посмотрим,vueкак с этим бороться
function patch (oldVnode, vnode) {
// 如果老节点不存在,则直接创建新节点
if (isUndef(oldVnode)) {
if (isDef(vnode)) createElm(vnode)
// 如果老节点存在,新节点却不存在,则直接移除老节点
} else if (isUndef(vnode)) {
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
removeVnodes(parentElm, , 0, [oldVnode].length -1)
} else {
const isRealElement = isDef(oldVnode.nodeType)
// 如果新旧节点相同,则进行具体的 patch 操作
if (isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
// 否则创建新节点,移除老节点
createElm(vnode, parentElm, nodeOps.nextSibling(oldElm))
removeVnodes(parentElm, [oldVnode], 0, 0)
}
}
}
Затем мы смотрим наpatchVnodeЛогика, связанная с серединой, сначала посмотрите на вышеупомянутоеkeyполезность здесь
function patchVnode (oldVnode, vnode) {
// 新旧节点完全一样,则直接 return
if (oldVnode === vnode) {
return
}
// 如果新旧节点都被标注静态节点,且节点的 key 相同。
// 则直接将老节点的 componentInstance 直接拿过来便OK了
if (
isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
}
Далее, давайте посмотрим, как сравнивается текстовое содержимое на vnode
- если vnode нетекстовый узел
const elm = vnode.elm = oldVnode.elm
const oldCh = oldVnode.children
const ch = vnode.children
if (isUndef(vnode.text)) {
// 如果 oldCh,ch 都存在且不相同,则执行 updateChildren 函数更新子节点
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch)
// 如果只有 ch 存在
} else if (isDef(ch)) {
// 老节点为文本节点,先将老节点的文本清空,然后将 ch 批量插入到节点 elm 下
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1)
// 如果只有 oldCh 存在,则直接清空老节点
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 如果 oldCh,ch 都不存在,且老节点为文本节点,则只将老节点文本清空
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
}
- Если vnode является текстовым узлом, а текст старого и нового узлов отличается, он будет напрямую установлен на текстовое содержимое vnode.
if (isDef(vnode.text) && oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
iii. updateChildren
Во-первых, давайте посмотрим на определение начального и конечного индексов старого и нового узлов в методе.
function updateChildren (parentElm, oldCh, newCh) {
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, idxInOld, vnodeToMove, refElm
}
Нарисуйте картинку прямо, чтобы понять
за которым следуетwhileЦикл заставляет начальный и конечный индексы старого и нового узлов перемещаться ближе к середине.
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)
какoldStartVnodeилиoldEndVnodeЕсли его нет, переместитесь ближе к середине
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
}
ДалееoldStartVnode,newStartVnode,oldEndVnode,newEndVnodeЧетыре случая попарного сравнения
// oldStartVnode 和 newStartVnode 为 sameVnode,进行 patchVnode
// oldStartIdx 和 newStartIdx 向后移动一位
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode 和 newEndVnode 为 sameVnode,进行 patchVnode
// oldEndIdx 和 newEndIdx 向前移动一位
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode 和 newEndVnode 为 sameVnode,进行 patchVnode
// 将 oldStartVnode.elm 插入到 oldEndVnode.elm 节点后面
// oldStartIdx 向后移动一位,newEndIdx 向前移动一位
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode)
nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 同理,oldEndVnode 和 newStartVnode 为 sameVnode,进行 patchVnode
// 将 oldEndVnode.elm 插入到 oldStartVnode.elm 前面
// oldEndIdx 向前移动一位,newStartIdx 向后移动一位
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode)
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
Используйте диаграмму, чтобы обобщить описанный выше процесс.
Когда вышеуказанные условия не выполняются, выполняются другие операции.
Прежде чем рассматривать другие операции, давайте рассмотрим функцииcreateKeyToOldIdx, его роль в основномreturnвнеoldChсерединаkeyиindexединственный соответствующийmapтаблица, судя поkey, легко найти соответствующиеkeyсоответствующий индекс в массиве
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
Кроме того, у этого блока есть еще одна вспомогательная функцияfindIdxInOld, выяснитьnewStartVnodeсуществуетoldChсоответствующий индекс в массиве
function findIdxInOld (node, oldCh, start, end) {
for (let i = start; i < end; i++) {
const c = oldCh[i]
if (isDef(c) && sameVnode(node, c)) return i
}
}
Далее рассмотрим конкретную обработку, не отвечающую указанным выше условиям.
else {
// 如果 oldKeyToIdx 不存在,则将 oldCh 转换成 key 和 index 对应的 map 表
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果 idxInOld 不存在,即老节点中不存在与 newStartVnode 对应 key 的节点,直接创建一个新节点
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
// 在 oldCh 找到了对应 key 的节点,且该节点与 newStartVnode 为 sameVnode,则进行 patchVnode
// 将 oldCh 该位置的节点清空掉,并在 parentElm 中将 vnodeToMove 插入到 oldStartVnode.elm 前面
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode)
oldCh[idxInOld] = undefined
nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 找到了对应的节点,但是却属于不同的 element 元素,则创建一个新节点
createElm(newStartVnode, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
// newStartIdx 向后移动一位
newStartVnode = newCh[++newStartIdx]
}
После этой серии операций связь между узлами завершается.diffиpatchоперация завершенаoldVnodeВ направленииnewVnodeконверсионная операция.
На этом статья подойдет к концу.Увидев это, я считаю, что у всехvueсерединаvdomУ этого произведения должно быть свое понимание.
Итак, вернемся к вопросу, который мы задали в начале статьи: знаете ли вы, почему возникает эта проблема?
эммм, если вы хотите продолжить общение по этому вопросу, то добро пожаловать в группу для обсуждения, передняя сборная солянка: 731175396. Друзья, не забудьте вступить в группу, даже если вы вместе придете в водную группу, это хорошо~ (Примечание: в группе действительно много красивых одиноких девушек, и конечно много красивых парней, таких как... меня)
Лично я готов снова забрать свой официальный аккаунт.После этого гарантирую качественную и хорошую статью каждую неделю.Заинтересованные друзья могут обратить на нее внимание.