深入理解Vue(二)

84 阅读3分钟

三、Vue.js 3的设计思路

3.1 声明式地描述UI

编写前端页面要涉及的内容

1、DOM元素

2、元素的属性

3、元素的事件

4、元素的层级结构

Vue选择了和原生基本一致的描述方式来进行模版的书写,当然我们也可以用js对象的方式来描述

3.1.1模版描述ui
<templete>
  <div class="dom" :id="coustomId" @click="handler">
    <div />
  </div>
</templete>
3.1.2js对象描述ui
const vnode = {
  // 标签名称
  tag: 'div',
  // 标签属性
  props: {
    class: 'dom',
    id: coustomId
    onClick: handler
  },
  // 子节点
  children: [
    { tag: 'div' }
  ]
}
export default {
  render() {
    return vnode
  }
}
​

利用h函数

import { h } from 'vue'
export default {
  render () {
    return h('h1', { onClick:handler })
  }
}

3.2初始渲染器

Vue先通过渲染函数(.render ) 拿到虚拟DOM,之后再通过渲染器将虚拟DOM转化为真实DOM,如图3-1所示

image.png

const vnode = {
  // 标签名称
  tag: 'div',
  // 标签属性
  props: {
    class: 'dom',
    onClick: handler
  },
  // 子节点
  children: 'click me'
}
export default {
  render() {
    return title
  }
}       

先解释一下上面的代码:

  • tag用来描述标签名称,所以div就是一个
    标签
  • props是一个对象,用来描述
    标签的属性、事件等内容
  • children用来描述标签的子节点,上面代码中是一个字符串,所以代码
    有一个文本节点

接下来我们实现一个渲染器

function renderer (vnode, container) {
  const { tag, props, children} = vnode
  // 生成DOM节点
  const el = document.createElement(tag)
  // 处理props属性
  for ( const key in props) {
    // 如果是on开头说明是事件
    if(/^on/.test(key)) {
      el.addEventListener(key.slice(2).toLowerCase(), props[key])
    } else {
      el.setAttribute(key, props[key])
    }
  }
  // 处理children
  if (typeof children === 'string') el.appendChild(document.createTextNode(children))
  else if (Array.isArray(children)) children.forEach(child => renderer(child, el))
  container.appendChild(el)
}

当然这只是首次渲染,渲染器复杂的地方在于更新阶段,例如文本内容的变更从click me变更为 click again,渲染器应该只更新元素的文本内容,而不需要走一遍完整的创建过程 #### 3.3组件的本质

组件的本质就是DOM元素的封装,有点像js中的fragment切片,所以我们可以定义一个函数,函数的返回值就是这个页面要渲染的内容

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

重构渲染器renderer函数的tag处理部分

function renderer (vnode, container) {
  // 是dom节点
  if (typeof vnode.tag === 'string') mountElement(vnode, container)
  // 是组件节点
  else if (typeof vnode.tag === 'function') mountComponent(vnode, container)
}

mountElement是之前的renderer函数

function mountElement (vnode, container) {
  const { tag, props, children} = vnode
  // 生成DOM节点
  const el = document.createElement(tag)
  // 处理props属性
  for ( const key in props) {
    // 如果是on开头说明是事件
    if(/^on/.test(key)) {
      el.addEventListener(key.slice(2).toLowerCase(), props[key])
    } else {
      el.setAttribute(key, props[key])
    }
  }
  // 处理children
  if (typeof children === 'string') el.appendChild(document.createTextNode(children))
  else if (Array.isArray(children)) children.forEach(child => renderer(child, el))
  container.appendChild(el)
}

mountComponent如下,主要是为了生成虚拟DOM,然后再去调用renderer

function mountComponent (vnode, container) {
  // 获取虚拟DOM
  const sunTree = vnode.tag()
  renderer(sunTree, container)
}

当然我们知道的Vue的组件展示形式是一个对象,所以最终renderer、mountComponent应该是下方这个样子的

renderer如下

function renderer (vnode, container) {
  // 是dom节点
  if (typeof vnode.tag === 'string') mountElement(vnode, container)
  // 是组件节点
  else if (typeof vnode.tag === 'object') mountComponent(vnode, container)
}

mountComponent 如下

function mountComponent (vnode, container) {
  // 获取虚拟DOM
  const sunTree = vnode.tag.render()
  renderer(sunTree, container)
}   

3.4模版的工作原理

模版的工作原理其实主要是借助于编辑器,我们知道在写vue时会导出一个对象,编译器让模版生成作用的原理就是在导出的对象上生成一个render函数,返回vnode,并在进行分析时,将可能发生变化的数据在vnode中标识出来,方便渲染器进行更新时找到哪个地方可能发生变化

{
  render () {
    return {
      tag: 'div',
      props: {
        id: 'foo',
        class: cls
      },
      patchFlags: 1 //假设数字1代表class是动态的
    }
  }
}