Skip to content

设计思路

声明式地描述 UI

vue 是一个声明式的 UI 框架

描述页面需要哪些

  1. DOM 元素 <a/> <div/>
  2. 属性 class id src
  3. 事件 click 按键
  4. 元素层级结构

vue3 的解决方案

  1. 一致 <a></a> <div></div>
  2. 一致 还可以使用v-bind 绑定 id="123" :id="demo"
  3. 使用@ 或者 v-on绑定 @click
  4. 一致 <div><span></span></div> 可以声明式的描述 UI(模板),当然 vue 也可以使用对象描述
js
 const title = {
   // 标签名称
   tag: 'h1',
   // 标签属性
   props: {
     onClick: handler
   },
   // 子节点
   children: [
     { tag: 'span' }
   ]
 }

对应到模板

vue
<h1 @click="handler"><span></span></h1>

模板 和 对象描述的区别,

  • 对象更加灵活
  • 模板更加直观

对于 循环 判断等 对象更加方便

vue 使用 h 函数,方便创建 js 对象,使创建虚拟 dom 更加方便

js
import { h } from 'vue'

 export default {
   render() {
     return h('h1', { onClick: handler }) // 虚拟 DOM
   }
 }
  • 渲染函数。一个组件要渲染的内容是通过渲染函数来描述的,也就是上面代码中的 render 函数,Vue.js 会根据组件的 render 函数的返回值拿到虚拟 DOM,然后就可以把组件的内容渲染出来了。

初识渲染器

作用: 就是把虚拟 DOM 渲染为真实 DOM

h("div","hello") => 渲染器 => 真是 dom (<div>hello</div>)

简单实现一个简单的渲染器 先写一个简单的虚拟 dom

js
const vnode = {
  tag: 'div',
  props: {
    onClick: () => alert('hello')
  },
  children: 'click me'
}
  • 首先简单解释一下上面这段代码
    • tag 用来描述标签名称,所以 tag: 'div' 描述的就是一个
      标签。
    • props 是一个对象,用来描述
      标签的属性、事件等内容。可以看到,我们希望给 div 绑定一个点击事件。
    • children 用来描述标签的子节点。在上面的代码中,children 是一个字符串值,意思是 div 标签有一个文本子节点:
      click me

实际上,你完全可以自己设计虚拟 DOM 的结构,例如可以使用 tagName 代替 tag,因为它本身就是一个 JavaScript 对象,并没有特殊含义。

下面是一个简单渲染器

js
 function renderer(vnode, container) {
   // 使用 vnode.tag 作为标签名称创建 DOM 元素
   const el = document.createElement(vnode.tag)
   // 遍历 vnode.props,将属性、事件添加到 DOM 元素
   for (const key in vnode.props) {
     if (/^on/.test(key)) {
       // 如果 key 以 on 开头,说明它是事件
       el.addEventListener(
         key.substr(2).toLowerCase(), // 事件名称 onClick ---> click
         vnode.props[key] // 事件处理函数
       )
     }
   }

   // 处理 children
   if (typeof vnode.children === 'string') {
     // 如果 children 是字符串,说明它是元素的文本子节点
     el.appendChild(document.createTextNode(vnode.children))
   } else if (Array.isArray(vnode.children)) {
     // 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
     vnode.children.forEach(child => renderer(child, el))
   }

   // 将元素添加到挂载点下
   container.appendChild(el)
 }
  • 这里的 renderer 函数接收如下两个参数。
    • vnode:虚拟 DOM 对象。
    • container:一个真实 DOM 元素,作为挂载点,渲染器会把虚拟 DOM 渲染到该挂载点下。

现在我们回过头来分析渲染器 renderer 的实现思路,总体来说分为三步。

  • 创建元素:把 vnode.tag 作为标签名称来创建 DOM 元素。
  • 为元素添加属性和事件:遍历 vnode.props 对象,如果 key 以 on 字符开头,说明它是一个事件,把字符 on 截取掉后再调用 toLowerCase 函数将事件名称小写化,最终得到合法的事件名称,例如 onClick 会变成 click,最后调用 addEventListener 绑定事件处理函数。
  • 处理 children:如果 children 是一个数组,就递归地调用 renderer 继续渲染,注意,此时我们要把刚刚创建的元素作为挂载点(父节点);如果 children 是字符串,则使用 createTextNode 函数创建一个文本节点,并将其添加到新创建的元素内。

renderer

组件的本质

组件就是一组 DOM 元素的封装

js
 const MyComponent = function () {
   return {
     tag: 'div',
     props: {
       onClick: () => alert('hello')
     },
     children: 'click me'
   }
 }

修改 renderer

js
 function renderer(vnode, container) {
   // 使用 vnode.tag 作为标签名称创建 DOM 元素
   const el = document.createElement(vnode.tag)
   // 遍历 vnode.props,将属性、事件添加到 DOM 元素
   for (const key in vnode.props) {
     if (/^on/.test(key)) {
       // 如果 key 以 on 开头,说明它是事件
       el.addEventListener(
         key.substr(2).toLowerCase(), // 事件名称 onClick ---> click
         vnode.props[key] // 事件处理函数
       )
     }
   }

  // 处理 children
  if (typeof vnode.tag === 'string') {
    // 说明 vnode 描述的是标签元素
    mountElement(vnode, container)
  } else if (typeof vnode.tag === 'function') {
    // 说明 vnode 描述的是组件
    mountComponent(vnode, container)
  }else if (Array.isArray(vnode.children)) {
     // 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
     vnode.children.forEach(child => renderer(child, el))
   }

   // 将元素添加到挂载点下
   container.appendChild(el)
 }
js
 function mountElement(vnode, container) {
   // 使用 vnode.tag 作为标签名称创建 DOM 元素
   const el = document.createElement(vnode.tag)
   // 遍历 vnode.props,将属性、事件添加到 DOM 元素
   for (const key in vnode.props) {
     if (/^on/.test(key)) {
       // 如果 key 以字符串 on 开头,说明它是事件
       el.addEventListener(
         key.substr(2).toLowerCase(), // 事件名称 onClick ---> click
         vnode.props[key] // 事件处理函数
       )
     }
   }

   // 处理 children
   if (typeof vnode.children === 'string') {
     // 如果 children 是字符串,说明它是元素的文本子节点
     el.appendChild(document.createTextNode(vnode.children))
   } else if (Array.isArray(vnode.children)) {
     // 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
     vnode.children.forEach(child => renderer(child, el))
   }

   // 将元素添加到挂载点下
   container.appendChild(el)
 }
js
function mountComponent(vnode, container) {
  // 调用组件函数,获取组件要渲染的内容(虚拟 DOM)
  const subtree = vnode.tag()
  // 递归地调用 renderer 渲染 subtree
  renderer(subtree, container)
}

Vue.js 中的有状态组件就是使用对象结构来表达的。

js
 // MyComponent 是一个对象
 const MyComponent = {
   render() {
     return { 
       tag: 'div',
       props: {
         onClick: () => alert('hello')
       },
       children: 'click me'
     }
   }
 }

编译器

编译器的作用其实就是将模板编译为渲染函数

html
 <div @click="handler">
   click me
 </div>

转换为

js
 render() {
   return h('div', { onClick: handler }, 'click me')
 }

对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟 DOM 渲染为真实 DOM,这就是模板的工作原理,也是 Vue.js 渲染页面的流程

编译器需要知道计算出哪些是变化的 哪些是固定的