vlambda博客
学习文章列表

阅读Vuejs设计与实现(第三章)

第三章、Vue.js的设计思路

3.1 声明式地描述UI


    1. 模板声明
<div id='app'></div>
<div :id='dynamicId'></
div>
<div @click='handler'></div>

    1. 通过JavaScript对象来描述
const title = {
  // 标签名字
  tag"h1",
  props: {
    onClick: handler
  },
  children: [
    {tag'span'}
  ]
}

<h1 @click='handler'><span></span></div>

    1. h函数:有一个组件要渲染的内容是通过渲染函数来描述的,也就是上面代码中的 render函数。
import { h } from 'vue'

export default {
  render(){
    return h('h1', { onClick: handler })
  }
}
  • 优点:使用模板和JavaScript对象描述UI有什么区别吗?使用JavaScript对象描述的UI更加的 灵活。
// h 标签的级别
let level = 3
const title = {
  tag`h${level}`// h3标签
}

3.2 初识渲染器

  • 虚拟DOM  => 真实DOM
  • render函数
// vnode
const vnode = {
  tag'div',
  props: {
    onClick() => alert('hello')
  },
  children'click me'
}
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)
}
  • 上述代码三步走

    • 创建元素
    • 为元素添加属性和事件
    • 处理children:注意使用递归调用
  • 要点:上述 render 函数完成了基本的元素创建,不过我要需要重点考虑其需要精确找到vnode对象变更点并且只更新变更点。

3.3 组件的本质

一句话总结:组件就是一组DOM元素的封装,这组DOM元素就是组件要渲染的内容。因此我们可以定义一个函数来代表组件,而函数的返回代表组件要渲染的内容。


    1. 描述组件
const myComponent = function({
  return {
    tag"div",
    props: {
      onClick() => alert('hello')
    },
    children'click me'
  }
}

    1. 用虚拟DOM来描述组件
// 就像tag:'div'来描述<div>标签一样,tag:MyComponent用来描述组件

const vnode = {
  tag: MyComponent
}

    1. 此时需要修改渲染器 renderer函数
// 1. vnode.tag是字符串,说明描述普通元素, 调用mountElemenet函数
// 2. vnode.tag是函数,说明描述组件,调用mountComponent函数
function renderer(vnode, container){
  if(typeof vnode.tag === 'string'){
    // 说明vnode 描述的是标签元素
    mountElemenet(vnode, container)
  }else if(typeof vnode.tag === 'function'){
    // 说明 vnode 描述的是组件
    mountComponent(vnode, container)
  }
}
  • mountElemenet & mountComponent
    • mountElemenet 则就是与上方 renderer 函数一样。
    • mountComponent则是我要需要 重点实现的
// mountComponent
// 1. 首先调用 vnode.tag 函数,返回组件本身的虚拟DOM,保存到 subtree 中
// 2. 将subtree 传进去,递归调用 renderer 来渲染

function mountComponent(vnode,container){
  // 调用组件函数,获取组件要渲染的内容(虚拟DOM)
  const subtree = vnode.tag()
  // 递归调用 renderer 渲染 subtree
  renderer(subtree,container)
}
  • 进一步修改,因为组件并不是只限于函数和简单标签,还有可能是 JavaScript 对象。
    • 修改 renderer 函数判断
    • 修改 mountComponent函数
// 对renderer添加对object判断

function renderer(vnode,container){
  if(typeof vnode.tag === 'string'){
    mountElement(vnode,container)
  } else if(typeof vnode.tag === 'object') {
    // 如果是对象,说明vnode描述的是组件
    mountComponent(vnode,container)
  }
}
// 对mountComponent直接调用 vnode.tag.render() 方法

function mountComponent(vnode,container){
  // vnode.tag 是组件对象,调用它的 render 函数得到组件要渲染的内容(虚拟DOM)
  const subtree = vnode.tag.render()
  // 递归地调用 renderer 渲染 subtree
  renderer(subtree,container)
}

3.4 模板工作原理

  • 编译器的作用其实就是将模板编译为渲染函数。
    • 对于一个组件来说,它要渲染的内容最终都是通过渲9染函数产生的,然后再把 渲染器再把渲染函数返回的虚拟 DOM渲染为 真实DOM
// 编译前模板
<div @click='handler'>
click me
</div>

// 编译后渲染函数
render(){
return h('div',{ onClick: handler }, 'click me');
}

3.5 Vuejs是各个模块组成的有机整体

  • 使用编译器和渲染器 结合。
    • 编译器将其编译成渲染函数
    • 渲染器将其渲染到页面上
  • 注意:Vuejs在编译成渲染函数的时候,对变量添加上 patchFlags属性,值为1,这样渲染器就知道哪个属性会发生改变。