3.1 声明式地描述 UI
前端页面
使用模板和 JavaScript 对象(更加灵活,本质虚拟dom)描述 UI
一个组件要渲染的内容是通过渲染函数来描述的
h 函数的返回值就是一个对象,其作用是让我们编写虚拟DOM 变得更加轻松。
如果还有子节点,那么需要编写的内容就更多了,所以 h 函数就是一个辅助创建虚拟 DOM 的工具函数。
import { h } from 'vue' export default { render() { return h('h1', { onClick: handler }) // 虚拟 DOM } }
3.2 初识渲染器
什么是虚拟 DOM,它其实就是用 JavaScript 对象来描述真实的 DOM 结构。
对于渲染器来说,它需要精确地找到 vnode 对象的变更点并且只更新变更的内容。 而不需要再走一遍完整的创建元素的流程。渲染器的工作原理归根结底,都是使用一些我们熟悉的 DOM 操作 API 来完成渲染工作。
function renderer(vnode, container) { const el = document.createElement(vnode.tag); for (const key in vnode.props) { if (/^on/.test(key)) { el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key]); } } if (typeof vnode.children === "string") { const text = document.createTextNode(vnode.children); el.appendChild(text); } else if (Array.isArray(vnode.children)) { vnode.children.forEach((element) => { renderer(element, el); }); } container.appendChild(el); } const vnode = { tag: "div", props: { onClick: () => alert("hello"), }, children: "click me", }; renderer(vnode, document.body);
3.3 组件的本质
组件又是什么呢?组件和虚拟 DOM 有什么关系?渲染器如何渲染组件?
虚拟 DOM 除了能够描述真实 DOM 之外,还能够描述组件。
组件就是一组 DOM 元素的封装
组件的返回值也是虚拟 DOM,它代表组件要渲染的内容。
让虚拟 DOM 对象中的 tag 属性来存储组件函数
Vue.js 中的有状态组件就是使用对象结构来表达的
用函数表达组件
function renderer(vnode, container) { if (typeof vnode.tag === 'string') { // 说明 vnode 描述的是标签元素 mountElement(vnode, container) } else if (typeof vnode.tag === 'function') { // 说明 vnode 描述的是组件 mountComponent(vnode, container) } } 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) } function mountComponent(vnode, container) { // 调用组件函数,获取组件要渲染的内容(虚拟 DOM) const subtree = vnode.tag() // 递归调用 renderer 渲染 subtree renderer(subtree, container) } const MyComponent = function () { return { tag: 'div', props: { onClick: () => alert('hello') }, children: 'click me' } } const vnode = { tag: MyComponent } renderer(vnode, document.body)
用对象来表达组件
<body></body> <script> function renderer(vnode, container) { if (typeof vnode.tag === 'string') { // 说明 vnode 描述的是标签元素 mountElement(vnode, container) } else if (typeof vnode.tag === 'function') { // 说明 vnode 描述的是组件 mountComponent(vnode, container) } } 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) } function mountComponent(vnode, container) { // 调用组件函数,获取组件要渲染的内容(虚拟 DOM) const subtree = vnode.tag() // 递归调用 renderer 渲染 subtree renderer(subtree, container) } const MyComponent = function () { return { tag: 'div', props: { onClick: () => alert('hello') }, children: 'click me' } } const vnode = { tag: MyComponent } renderer(vnode, document.body) </script>
3.4 模板的工作原理
无论是手写虚拟 DOM(渲染函数)还是使用模板,都属于声明式地描述 UI,并且 Vue.js 同时支持这两种描述 UI 的方式。
编译器的作用其实就是将模板编译为渲染函数。
<div @click="handler"> click me </div>
对于编译器来说,模板就是一个普通的字符串,它会分析该字符串并生成一个功能与之相同的渲染函数
render() { return h('div', { onClick: handler }, 'click me') }
无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟 DOM 渲染为真实 DOM,这就是模板的工作原理,也是 Vue.js 渲染页面的流程。
<template> <div @click="handler"> click me </div> </template> <script> export default { data() {/* ... */}, methods: { handler: () => {/* ... */} } } </script>
其中