阅读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,这样渲染器就知道哪个属性会发生改变。 
