第八章、挂载与更新

元素可以有多个子元素,及 children 属性会是数组。

const vnode = {
  type: 'div',
  children: [
    {
      type: 'p',
      children: 'hello',
    },
  ],
}

function mountElement(vnode, container) {
  const el = createElement(vnode.type)

  if (typeof vnode.children === 'string') {
    setElementText(el, vnode.children)
  } else if (Array.isArray(vnode.children)) {
    // 当children属性是一个数组的时候,表示有多个子元素,遍历调用patch进行挂载
    vnode.children.forEach((child) => {
      // 因为mountElement是挂载阶段,所以没有旧vnode,n1传null
      patch(null, child, el)
    })
  }

  insert(el, container)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

vnode 节点可以通过 props 属性描述它的属性。但是元素的属性有 HTML 属性和 DOM 属性之分,HTML 属性的作用是用来设置与之对应的 DOM 属性的初始值,但是在某些特殊属性的时候要做对应的处理。

// 某些属性是只读的,不需要更新,例如input框上的form属性,做特殊处理
function shouldSetAsProps(el, key, value) {
  if (key === 'form' && el.tagName === 'INPUT') return false

  return key in el
}

function mountElement(vnode, container) {
  const el = createElement(vnode.type)

  if (vnode.props) {
    for (const key in vnode.props) {
      const value = vnode.props[key]
      // 判断当前属性是否需要更新
      if (shouldSetAsProps(el, key, value)) {
        const type = typeof el[key]
        // 有些属性是布尔值类型的,例如 <button disabled>按钮</button>
        // 这些属性在模板上可以只写名称不写值,会被编译为 disabled: ""
        // 要对这些属性做特殊处理,将编译后的空字符串转为布尔值
        if (type === 'boolean' && value === '') {
          el[key] = true
        } else {
          el[key] = value
        }
      } else {
        // 没有对应的DOM属性的时候就使用setAttribute更新属性
        el.setAttribute(key, value)
      }
    }
  }

  if (typeof vnode.children === 'string') {
    setElementText(el, vnode.children)
  } else if (Array.isArray(vnode.children)) {
    vnode.children.forEach((child) => {
      patch(null, child, el)
    })
  }

  insert(el, container)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

同样设置属性的函数也可以提取出来有渲染器传入。

const renderer = createRenderer({
  patchProps(el, key, prevValue, nextValue) {
    if (shouldSetAsProps(el, key, nextValue)) {
      const type = typeof el[key]
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      // 没有对应的DOM属性的时候就使用setAttribute更新属性
      el.setAttribute(key, nextValue)
    }
  },
})

function mountElement(vnode, container) {
  // ----

  if (vnode.props) {
    for (const key in vnode.props) {
      patchProps(el, key, null, vnode.props[key])
    }
  }

  // ----
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

class 也是一个特殊的属性,vue 的 class 支持字符串,数组,对象多种格式,会做统一的转换。style 属性也是类似。

// 统一格式化class
function normalizeClass(value) {
  let res = ''
  if (typeof value === 'string') {
    res = value
  } else if (Array.isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      const normalized = normalizeClass(value[i])
      if (normalized) {
        res += normalized + ' '
      }
    }
  } else if (value !== null && typeof value === 'object') {
    for (const name in value) {
      if (value[name]) {
        res += name + ' '
      }
    }
  }
  return res.trim()
}

// 编译成vnode的时候调用格式化class
const vnode = {
  type: 'div',
  props: {
    class: normalizeClass(['foo bar', { baz: true }]), // 格式化的结果将是 'foo bar baz'
  },
  children: [
    {
      type: 'p',
      children: 'hello',
    },
  ],
}

// 更新props的时候对class特殊处理
const renderer = createRenderer({
  patchProps(el, key, prevValue, nextValue) {
    if (key === 'class') {
      // el.className 的方式设置class性能最优
      el.className = nextValue || ''
    } else if (key === 'style') {
      // key 为 style 时做跟class类似的特殊处理
    } else if (shouldSetAsProps(el, key, nextValue)) {
      const type = typeof el[key]
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      el.setAttribute(key, nextValue)
    }
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

当后续渲染的 n2 传入的值为 null 时,表示本次什么都不需要渲染,所以需要将之前的渲染内容卸载,封装统一的卸载方法。

function mountElement(vnode, container) {
  // 将创建好的节点挂载在vnode.el上,方便之后引用
  const el = (vnode.el = createElement(vnode.type))

  // ----
}

const renderer = createRenderer({
  // 自定义卸载方法传入
  unmount(vnode) {
    // 找到父节点,调用父节点的removeChild进行删除
    const parent = vnode.el.parentNode
    if (parent) {
      parent.removeChild(vnode.el)
    }
  },
})

function render(vnode, container) {
  if (vnode) {
    patch(container._vnode, vnode, container)
  } else {
    // 执行我们传入的卸载节点的方法
    if (container._vnode) {
      unmount(container._vnode)
    }
  }
  container._vnode = vnode
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

现在我们需要对 vnode 的 type 属性作区分,对不同类型的 vnode 要做不同的处理。

function patch(n1, n2, container) {
  // 如果新旧节点的type不一样,表示两个节点已经完全不一样了,例如从p标签改为了div标签。
  // 节点类型不同没有对比的意义,直接卸载旧的节点再挂载新的节点就好
  if (n1 && n1.type !== n2.type) {
    unmount(n1)
    n1 = null
  }

  // 因为我们在之前的render函数已经判断了新节点是否存在,所以执行到这里的时候表示n2一定是存在的
  // 判断新节点的类型,根据不同类型做不同的处理
  const { type } = n2

  // string 类型表示n2是普通的标签元素
  if (typeof type === 'string') {
    // 旧节点存在则执行更新操作,不存在就直接绑定新节点
    if (!n1) {
      mountElement(n2, container)
    } else {
      // 更新节点
      patchElement(n1, n2)
    }
  } else if (typeof type === 'object') {
    // object 类型表示新节点是一个组件
  } else if (typeof type === 'xxx') {
    // 执行其他类型的操作
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

我们约定以 on 开头的属性表示事件,当碰到以 on 开头的属性时,要给元素绑定事件。

// 编译成vnode的时候调用格式化class
const vnode = {
  type: 'div',
  props: {
    // on开头的属性是一个事件
    onClick: () => {
      console.log('click')
    },
  },
  children: 'text',
}

const renderer = createRenderer({
  patchProps(el, key, prevValue, nextValue) {
    // 判断当前属性是否为一个事件
    if (/^on/.test(key)) {
      // 取出事件名称 onClick --> click
      const name = key.slice(2).toLowerCase()
      // 绑定事件
      el.addEventListener(name, nextValue)
    } else if (key === 'class') {
      el.className = nextValue || ''
    } else if (key === 'style') {
    } else if (shouldSetAsProps(el, key, nextValue)) {
      const type = typeof el[key]
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      el.setAttribute(key, nextValue)
    }
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

当卸载节点的时候我们要移除之前绑定的事件,频繁的绑定和移除事件太消耗性能了,需要一种更优雅的方式来处理。

const renderer = createRenderer({
  patchProps(el, key, prevValue, nextValue) {
    if (/^on/.test(key)) {
      // 定义一个 invoker 来伪造我们的事件处理函数,直接从el._vei上去值,我们之后会给这个属性赋值,第一次绑定函数这个值会不存在
      let invoker = el._vei
      const name = key.slice(2).toLowerCase()
      if (nextValue) {
        // 新值存在且伪造函数不存在,表示我们是第一次绑定函数,就为我们的伪造函数赋值并储存在el._vei上
        if (!invoker) {
          invoker = el._vei = (e) => {
            invoker.value(e)
          }
          // invoker.value是我们最终执行的事件处理函数,每次更新的时候只要更新
          invoker.value = nextValue
          el.addEventListener(name, invoker)
        } else {
          // 这里表示我们之前绑定过伪造的事件处理函数,只要用新值更新我们的伪造函数就好,避免了频繁的绑定和移除事件的操作
          invoker.value = nextValue
        }
      } else if (invoker) {
        // 这里表示新值不存在,但是之前有绑定过伪造的事件处理函数,直接移除就好
        el.removeEventListener(name, invoker)
      }
    } else if (key === 'class') {
      el.className = nextValue || ''
    } else if (key === 'style') {
    } else if (shouldSetAsProps(el, key, nextValue)) {
      const type = typeof el[key]
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      el.setAttribute(key, nextValue)
    }
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

目前我们的 el._vel 属性只能储存单一的事件处理函数,但是元素节点是可以有多个事件,每个事件都可以有多个事件处理函数的。

const vnode = {
  type: 'div',
  // 多个事件,且每个事件都可能有多个事件处理函数
  props: {
    onClick: [
      () => {
        console.log('click1')
      },
      () => {
        console.log('click2')
      },
    ],
    onContextmenu: () => {
      console.log('contextmenu')
    },
  },
  children: 'text',
}

const renderer = createRenderer({
  patchProps(el, key, prevValue, nextValue) {
    if (/^on/.test(key)) {
      // 定义invokers表示绑定的所有事件集合
      const invokers = el._vei || (el._vei = {})
      // 根据当前key取出对应的事件处理函数
      let invoker = invokers[key]
      const name = key.slice(2).toLowerCase()
      if (nextValue) {
        if (!invoker) {
          // 事件处理函数储存在对应的key上
          invoker = el._vei[key] = (e) => {
            // 当前事件如果有多个事件处理函数,则遍历执行
            if (Array.isArray(invoker.value)) {
              invoker.value.forEach((fn) => fn(e))
            } else {
              invoker.value(e)
            }
          }
          invoker.value = nextValue
          el.addEventListener(name, invoker)
        } else {
          invoker.value = nextValue
        }
      } else if (invoker) {
        el.removeEventListener(name, invoker)
      }
    } else if (key === 'class') {
      el.className = nextValue || ''
    } else if (key === 'style') {
    } else if (shouldSetAsProps(el, key, nextValue)) {
      const type = typeof el[key]
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      el.setAttribute(key, nextValue)
    }
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

有时候事件的冒泡和渲染函数的执行会导致绑定的事件错误执行,需要对触发事件的时间和绑定事件的时间做处理。

const bol = ref(false)

effect(() => {
  const vnode = {
    type: 'div',
    // 初始时 bol.value 值为false,div不会被绑定click事件,子元素p绑定了click事件
    // 触发p元素的click事件,会发现div元素的事件也触发了,因为p元素改变了bol.value的值,触发了重新渲染
    // 重新渲染给div元素绑定了事件,当p元素的事件冒泡到div元素时,div元素已经被绑定了事件处理函数了
    // 需要对 触发事件 和 绑定事件 两者的时间做处理
    props: bol.value
      ? {
          onClick: () => {
            console.log('div')
          },
        }
      : {},
    children: [
      {
        type: 'p',
        props: {
          onClick: () => {
            bol.value = true
          },
        },
        children: 'text',
      },
    ],
  }

  renderer.render(vnode, document.querySelector('#app'))
})

const renderer = createRenderer({
  patchProps(el, key, prevValue, nextValue) {
    if (/^on/.test(key)) {
      const invokers = el._vei || (el._vei = {})
      let invoker = invokers[key]
      const name = key.slice(2).toLowerCase()
      if (nextValue) {
        if (!invoker) {
          invoker = el._vei[key] = (e) => {
            // 当前任务触发的时间如果早于绑定的时间,就不执行事件回调函数了
            if (e.timeStamp < invoker.attached) return
            if (Array.isArray(invoker.value)) {
              invoker.value.forEach((fn) => fn(e))
            } else {
              invoker.value(e)
            }
          }
          invoker.value = nextValue
          // 记录一下当前事件绑定的时间
          invoker.attached = performance.now()
          el.addEventListener(name, invoker)
        } else {
          invoker.value = nextValue
        }
      } else if (invoker) {
        el.removeEventListener(name, invoker)
      }
    } else if (key === 'class') {
      el.className = nextValue || ''
    } else if (key === 'style') {
    } else if (shouldSetAsProps(el, key, nextValue)) {
      const type = typeof el[key]
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      el.setAttribute(key, nextValue)
    }
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

更新子节点的时候,有 没有子节点 文本子节点 一组子节点 3 种情况(单一子节点也属于一组子节点,只是子节点数量为 1),这 3 种情况互相转换会产生 3 * 3 = 9 种结果,但实际代码中并不需要完全覆盖 9 种结果。

// 更新节点
function patchElement(n1, n2) {
  // 取出旧节点,
  const el = (n2.el = n1.el)
  const oldProps = n1.props
  const newProps = n2.props
  // 先遍历新属性,依照新属性的key对旧属性进行更新
  for (const key in newProps) {
    if (newProps[key] !== oldProps[key]) {
      patchProps(el, key, oldProps[key], newProps[key])
    }
  }
  // 然后根据旧属性,把不存在于新属性上的属性给移除掉
  for (const key in oldProps) {
    if (!(key in newProps)) {
      patchProps(el, key, oldProps[key], null)
    }
  }

  // 更新子节点
  patchChildren(n1, n2, el)
}

function patchChildren(n1, n2, container) {
  // 新子节点为文本节点的时候
  if (typeof n2.children === 'string') {
    // 旧子节点会有3种可能,没有节点、文本节点、一组子节点,但是只有是一组子节点的时候,需要逐个移除
    if (Array.isArray(n1.children)) {
      n1.children.forEach((child) => {
        unmount(child)
      })
    }
    // 其他的情况直接设置文本就可以了
    setElementText(container, n2.children)
  } else if (Array.isArray(n2.children)) {
    // 新子节点是一组子节点的时候
    if (Array.isArray(n1.children)) {
      // 如果旧子节点也是一组子节点,这里需要进入核心的diff逻辑
    } else {
      // 其他的情况只需要清空文本,然后挂载新子节点
      setElementText(container, '')
      n2.children.forEach((child) => {
        patch(null, child, container)
      })
    }
  } else {
    // 运行到这一步说明新子节点不存在
    if (Array.isArray(n1.children)) {
      // 如果旧子节点是一组节点,就逐个卸载
      n1.children.forEach((child) => {
        unmount(child)
      })
    } else if (typeof n1.children === 'string') {
      // 如果旧子节点是文本,则清空文本
      setElementText(container, '')
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

目前我们只处理了普通标签类型的 vnode,vnode 的 type 会有各种各样的,下面处理一下文本节点和注释节点的情况

// 定义新的Symbol值来表示节点的类型
const Text = Symbol()
const Comment = Symbol()

// type属性表示当前vnode的类型
const textVnode = {
  type: Text,
  children: '文本内容',
}
const commentVnode = {
  type: Comment,
  children: '注释内容',
}

// 同样操作节点的方法由外部传入
const renderer = createRenderer({
  createText(text) {
    return document.createTextNode(text)
  },
  setText(el, text) {
    el.nodeValue = text
  },
})

function patch(n1, n2, container) {
  if (n1 && n1.type !== n2.type) {
    unmount(n1)
    n1 = null
  }

  const { type } = n2

  if (typeof type === 'string') {
    if (!n1) {
      mountElement(n2, container)
    } else {
      patchElement(n1, n2)
    }
  } else if (typeof type === 'object') {
  } else if (type === Text) {
    // Text 类型表示文本节点
    if (!n1) {
      // 旧节点不存在则直接挂载新节点
      const el = (n2.el = createText(n2.children))
      insert(container, el)
    } else {
      // 旧节点存在,且新旧文本内容不一致,则修改节点文本
      const el = (n2.el = n1.el)
      if (n2.children !== n1.children) {
        setText(el, n2.children)
      }
    }
  }
}

// 注释节点类型操作与文本节点类似,只不过创建注释节点是用 document.createComment() 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

vue3 在一个 template 下可以有多个子节点了,需要我们用一个 Fragment 类型来表示这种情况。

const Fragment = Symbol()

const fragmentVnode = {
  type: Fragment,
  children: [
    { type: 'li', children: 'text1' },
    { type: 'li', children: 'text2' },
    { type: 'li', children: 'text3' },
  ],
}

function patch(n1, n2, container) {
  if (n1 && n1.type !== n2.type) {
    unmount(n1)
    n1 = null
  }
  const { type } = n2
  if (typeof type === 'string') {
    if (!n1) {
      mountElement(n2, container)
    } else {
      patchElement(n1, n2)
    }
  } else if (typeof type === 'object') {
  } else if (type === Text) {
    if (!n1) {
      const el = (n2.el = createText(n2.children))
      insert(container, el)
    } else {
      const el = (n2.el = n1.el)
      if (n2.children !== n1.children) {
        setText(el, n2.children)
      }
    }
  } else if (type === Fragment) {
    // Fragment本身不渲染任何东西,只要对它的子节点做处理就好了
    if (!n1) {
      // 旧vnode不存在,直接挂载Fragment的children
      n2.children.forEach((child) => patch(null, child, container))
    } else {
      // 旧vnode存在,更新子节点
      patchChildren(n1, n2, container)
    }
  }
}

const renderer = createRenderer({
  unmount(vnode) {
    // 卸载的函数要对 Fragment 做处理,只要卸载它的子节点就好了
    if (vnode.type === Fragment) {
      vnode.children.forEach((child) => unmount(child))
      return
    }
    const parent = vnode.el.parentNode
    if (parent) {
      parent.removeChild(vnode.el)
    }
  },
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59