阅读Vuejs设计与实现(第三章)
第三章、Vue.js的设计思路
3.1 声明式地描述UI
-
-
模板声明
<div id='app'></div>
<div :id='dynamicId'></div>
<div @click='handler'></div>
-
-
通过JavaScript对象来描述
const title = {
// 标签名字
tag: "h1",
props: {
onClick: handler
},
children: [
{tag: 'span'}
]
}
<h1 @click='handler'><span></span></div>
-
-
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元素就是组件要渲染的内容。因此我们可以定义一个函数来代表组件,而函数的返回代表组件要渲染的内容。
-
-
描述组件
const myComponent = function() {
return {
tag: "div",
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
}
-
-
用虚拟DOM来描述组件
// 就像tag:'div'来描述<div>标签一样,tag:MyComponent用来描述组件
const vnode = {
tag: MyComponent
}
-
-
此时需要修改渲染器 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,这样渲染器就知道哪个属性会发生改变。