introduce
核心模块(core modules)
Reactivity Module
有响应式对象发生改变的时候会重新调用render函数
Compiler Module 获取HTML模板并编译成渲染函数(render function)
Renderer Module
-Render Phase(渲染阶段)
调用render函数生成Vnode
-Mount Phase
调用VNode挂载到网页
-Patch Phase(补丁阶段也就是更新阶段)
将所有新旧VNode进行比较并更新到网页
- Renderer Module
渲染函数返回虚拟dom
template或者HTML节点->render()->Vnode->Element->mount->RealElement
import {h} from 'vue'
function h(tag,props,children)
{
return {
tag,
props,
children
}
}
function render()
{
return h(elementTypeName,{
elementProps
},elementChild)
//比如
return h('div',{
id:"#app",
onClick:()=>{this.count++}
},h('span','hello world'))// <div id="app"><span>hello world</span></div>
}
//如何处理类似于onClick的事件?
//在执行mount函数遍历props属性的时候执行
for(const key in vnode.props)
{
const value=vnode.props[key];
if(key.startWith('on'))
{
//addEventListener
el.addEventListener(key.slice(2).toLowerCase(),value);
}
else
{
el.setAttribute(key,value);
}
}
Rendering Mechanism(渲染原理)
core code(核心代码)
import { h } from 'vue'
function render()
{
return h(elementTypeName,{
elementProps
},elemntChild)
}
- 举个例子
import { h } from 'vue'
function render()
{
return h('div',{
id:"#app",
onClick:clicked
},'hello world')
}
How to Use Render Function
如何使用类似于V-if或者v-for这些指令
import {h} from 'vue'
const App={
render(){
//v-if='ok' 使用三元表达式
return this.ok?
h('div',"i am the ok content"):
h('span','i am the ont ok content');
}
//如何使用v-for呢?
render()
{
//v-for='item in list'
return this.list.map(item=>{
return h('span',{
key:item.id
},item.text)
})
}
}
如何处理slot
const App={
render(){
//拿到默认插槽的虚拟DOM
const slot:Array<VNode>=this.$slot.default();
return slot.map(child=>{
return h('div',child)
})
}
}
//比如我做一个嵌套的左侧导航栏
<Wrap>
<div class='margin-4'>
<div>h1</div>
<div>h1</div>
<div>h1</div>
</div>
<Wrap>
<div class='margin-4'>
<div>h2</div>
<div>h2</div>
<div>h2</div>
</div>
</Wrap>
<Wrap>
Compiler And Renderer API
使用了缓存的render可以不用再patch的时候做重新render
//可以缓存一个html元素的所有东西,props或者监听事件等等
//这个缓存类似于react的useMemo useCallback
//不同的是Vue已经帮你做了这个工作
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createVNode("div", {
id: "app",
onclick: "test"
}, "Hello World!", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1
]))
}
compiler做的工作
//比如下面的一个DOM
<div>
<section></section>
<div></div>
<span>{{msg}}</span>
</div>
//当然,人一眼就看出来只有span才是有可能变化的东西,但是机器不知道
//如果compiler不告诉机器只有span可能会变化,机器会遍历整个DOM树
return (_openBlock(), _createBlock("div", null, [
_hoisted_1,
_createVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
//_openBlock这个函数执行之后,会生成一个动态子节点添加到根节点上去
//现在的根节点就只有动态子节点了
Create A Mount Function
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
</body>
<script>
function h(tag,props,children)
{
return {
tag,
props,
children
}
}
function mount(vnode,container)
{
const {tag,props,children}=vnode;
//将真实的DOM元素存储到vnode上去,方便等会patch的时候使用
const element=vnode.el=document.createElement(tag);
if(props)
{
for(const key in props)
{
const value=props[key];
if(key.startsWith('on'))
{
//addEventListener
element.addEventListener(key.slice(2).toLowerCase(),value);
}
else
{
element.setAttribute(key,value);
}
}
}
//children->可以字符串和 数组类型的children
if(typeof children ==="string")
{
const textNode=document.createTextNode(children);
element.append(textNode);
}
//children是一个数组
else if(Array.isArray(children))
{
children.forEach(vnode=>{
//在当前的element容器中递归调用‘
//并且挂载到当前的容器上去
mount(vnode,element);
})
}
container.append(element);
}
const vdom=h('div',{
class:"red"
},[
h('span',null,'hello')
])
mount(vdom,document.querySelector('#app'))
</script>
</html>
How To Patch(更新虚拟DOM)
//v1->old vnode n2->new vnode
const vdom2=h('div',{
class:"green"
},[
h('span',null,'changed')
])
function patch(n1,n2)
{
//同样的给新节点也赋一个这样的属性,以便新节点在变成旧节点的时候使用
const el=n2.el=n1.el//之前存储的就节点派上用场
//相同tag
if(n1.tag===n2.tag)
{
//props
const oldProps=n1.props || {};
const newProps=n2.props || {};
for(const key in oldProps)
{
const oldValue=oldProps[key];
const newValue=newProps[key];
if(oldValue!=newValue)
{
el.setAttribute(key,newValue)
}
}
//旧节点有,新节点没有
for(const key in oldProps)
{
if(!(key in newProps))
{
el.removeAttribute(key);
}
}
//children
//1.newChildren-> string (old:string old:Array)
//2.newChildren-> array (old:string old:array)
const oldChildren=n1.children || [];
const newChildren=n22.children || [];
if(typeof newChildren=='string')
{
if(typeof oldChildren=='string')
{
if(newChildren!=oldChildren)
{
el.textContent=newChildren
}
}
else if(Array.isArray(oldChildren))
{
el.textContent=newChildren
}
}
else if(Array.isArray(newChildren))
{
if(typeof oldChildren=='string')
{
el.innerHTML='';
newChildren.forEach(child=>{
mount(child.el);
})
}
else
{
//both children are array
const length=Math.min(oldChildren.length,newChildren.length);
for(let i=0;i<length;i++)
{
patch(oldChildren[i],newChildren[i]);
}
if(newChildren.length>length)
{
//add new element
newChildren.slice(length).forEach(addChild=>{
mount(addChild,el);
})
}
if(oldChildren.length>length)
{
//remove old element
for(let index=length;index<oldChildren.length;index++)
{
const oldVnode=oldChildren[index];
oldVnode.el.parent.removeChild(oldVnode.el);
}
}
}
}
}
else
{
//replace
n1.el.replaceWith(document.createElement(n2.tag));
}
}
patch(vdom,vdom2)//这样就直接使用啦
Reactivity(响应式的实现)
let update,state
const onStateChanged=()=>{
view=render(state)
}
const setState=newState=>{
state=newState;
update();
}
发布订阅的实现
let activeEffect;
//为了在调用depend()的时候把相关函数作为订阅者
//添加到subscribers中去,以后notify的时候就可以用了
class Dep{
constructor(value)
{
this.subscribers=new Set();///所有的订阅者
this._value=value;
}
get value()
{
this.depend();
return this._value
}
set value(newVal)
{
this._value=newVal;
this.notify();
}
depend()
{
if(activeEffect)
{
this.subscribers.add(activeEffect)
}
}
notify(){
//通知订阅者
this.subscribers.forEach(effect=>{
effect();
})
}
}
function watchEffect(effect)
{
activeEffect=effect;
effect();
activeEffect=null;
}
const dep = new Dep('primaryValue');
//如何将一个dep与一个值相关联
//在Dep的构造函数中加一个value属性,通知的时候使用value就好了
watchEffect(()=>{
console.log(dep.value)
//自动调用dep.depned()
})
dep.value='changed'//自动调用dep.notify()
ref的实现
function ref(target)
{
let value=target;
let dep=new Dep();
const obj={
get value()
{
dep.depend();
return value;
},
set value(newVal)
{
value=newVal;
dep.notify();
}
}
return obj
}
reactive的实现
let activeEffect;
class Dep{
constructor(value)
{
this.subscribers=new Set();///所有的订阅者
//在实现reactive的时候,Dep不需要维护自己的value
//value来自reactive中的参数
}
depend()
{
if(activeEffect)
{
this.subscribers.add(activeEffect)
}
}
notify(){
//通知订阅者
this.subscribers.forEach(effect=>{
effect();
})
}
}
function watchEffect(effect)
{
activeEffect=effect;
effect();
activeEffect=null;
}
const targetMap=new WeakMap();
//weakmap只能用对象作为键
function getDep(target,key)
{
let depsMap=targetMap.get(target);
//depsMap包含了所有与对象相关的dep
if(!depsMap)
{
depsMap=new Map();
targetMap.set(target,depsMap);
}
let dep=depsMap.get(key)
if(!dep)
{
dep=new Dep();
depsMap.set(key,dep);
}
return dep;
}
const reactiveHandler={
get(target,key)
{
//难点在于怎么找到dep
let dep=getDep(target,key);
dep.depend();
return Reflect.get(target,key);
},
set(target,key,value,receiver)
{
const res=Reflect.set(target,key,value);
let dep=getDep(target,key);
dep.notify();
//必须返回一个布尔值来指示操作是否成功
return res
}
}
function reactive(obj)
{
//使用object.defineProperty的缺点
//只能对现有的属性进行响应式监听
//添加的新属性无法做响应式
//可以监听到数组的方法
return new Proxy(obj,reactiveHandler)
}
const state=reactive({
count:0
})
watchEffect(()=>{
console.log(state.count)
})
state.count++;
测试代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.red{
color: red;
font-size: 66px;
cursor: pointer;
}
.green{
color: green;
}
</style>
</head>
<body>
<div id="app"></div>
</body>
<script>
function h(tag,props,children)
{
return {
tag,
props,
children
}
}
function mount(vnode,container)
{
const {tag,props,children}=vnode;
const element=vnode.el=document.createElement(tag);
if(props)
{
for(const key in props)
{
const value=props[key];
if(key.startsWith('on'))
{
//addEventListener
element.addEventListener(key.slice(2).toLowerCase(),value);
}
else
{
element.setAttribute(key,value);
}
}
}
//children->可以字符串和 数组类型的children
if(typeof children ==="string")
{
const textNode=document.createTextNode(children);
element.append(textNode);
}
//children是一个数组
else if(Array.isArray(children))
{
children.forEach(vnode=>{
//在当前的element容器中递归调用‘
//并且挂载到当前的容器上去
mount(vnode,element);
})
}
container.append(element);
}
function patch(n1,n2)
{
//同样的给新节点也赋一个这样的属性,以便新节点在变成旧节点的时候使用
const el=n2.el=n1.el//之前存储的就节点派上用场
//相同tag
if(n1.tag===n2.tag)
{
//props
const oldProps=n1.props || {};
const newProps=n2.props || {};
for(const key in oldProps)
{
const oldValue=oldProps[key];
const newValue=newProps[key];
if(oldValue!=newValue)
{
el.setAttribute(key,newValue)
}
}
//旧节点有,新节点没有
for(const key in oldProps)
{
if(!(key in newProps))
{
el.removeAttribute(key);
}
}
//children
//1.newChildren-> string (old:string old:Array)
//2.newChildren-> array (old:string old:array)
const oldChildren=n1.children || [];
const newChildren=n2.children || [];
if(typeof newChildren=='string')
{
if(typeof oldChildren=='string')
{
if(newChildren!=oldChildren)
{
el.textContent=newChildren
}
}
else if(Array.isArray(oldChildren))
{
el.textContent=newChildren
}
}
else if(Array.isArray(newChildren))
{
if(typeof oldChildren=='string')
{
el.innerHTML='';
newChildren.forEach(child=>{
mount(child,el);
})
}
else
{
//both children are array
const length=Math.min(oldChildren.length,newChildren.length);
for(let i=0;i<length;i++)
{
patch(oldChildren[i],newChildren[i]);
}
if(newChildren.length>length)
{
//add new element
newChildren.slice(length).forEach(addChild=>{
mount(addChild,el);
})
}
if(oldChildren.length>length)
{
//remove old element
for(let index=length;index<oldChildren.length;index++)
{
const oldVnode=oldChildren[index];
oldVnode.el.parent.removeChild(oldVnode.el);
}
}
}
}
}
else
{
//replace
n1.el.replaceWith(document.createElement(n2.tag));
}
}
let activeEffect;
class Dep{
constructor(value)
{
this.subscribers=new Set();///所有的订阅者
//在实现reactive的时候,Dep不需要维护自己的value
//value来自reactive中的参数
}
depend()
{
if(activeEffect)
{
this.subscribers.add(activeEffect)
}
}
notify(){
//通知订阅者
this.subscribers.forEach(effect=>{
effect();
})
}
}
function watchEffect(effect)
{
activeEffect=effect;
effect();
activeEffect=null;
}
const targetMap=new WeakMap();
//weakmap只能用对象作为键
function getDep(target,key)
{
let depsMap=targetMap.get(target);
//depsMap包含了所有与对象相关的dep
if(!depsMap)
{
depsMap=new Map();
targetMap.set(target,depsMap);
}
let dep=depsMap.get(key)
if(!dep)
{
dep=new Dep();
depsMap.set(key,dep);
}
return dep;
}
const reactiveHandler={
get(target,key)
{
//难点在于怎么找到dep
let dep=getDep(target,key);
dep.depend();
return Reflect.get(target,key);
},
set(target,key,value,receiver)
{
const res=Reflect.set(target,key,value);
let dep=getDep(target,key);
dep.notify();
//必须返回一个布尔值来指示操作是否成功
return res
}
}
function reactive(obj)
{
//使用object.defineProperty的缺点
//只能对现有的属性进行响应式监听
//添加的新属性无法做响应式
//可以监听到数组的方法
return new Proxy(obj,reactiveHandler)
}
function ref(target)
{
let value=target;
// let keyMap=new Map();
// keyMap.set(value,new Dep())
// let dep=keyMap.get(value)
let dep=new Dep();
const obj={
get value()
{
// let dep=getDep(obj,'value');
dep.depend();
return value;
},
set value(newVal)
{
// let dep=getDep(obj,'value');
value=newVal;
dep.notify();
}
}
return obj
}
const App={
data:{count:ref(0),
test:ref(1)},
render(){
return h('div',{
class:'red',
onClick:()=>{
this.data.count.value++;
this.data.test.value++
}
},String(`${this.data.count.value}--${this.data.test.value}`))
}
}
function mountApp(component,container)
{
let isMounted=false;
let preVdom;
watchEffect(()=>{
if(!isMounted)
{
preVdom=component.render();
mount(preVdom,container);
isMounted=true;
}
else
{
let newVdom=component.render();
patch(preVdom,newVdom);
preVdom=newVdom;
}
})
}
mountApp(App,document.querySelector("#app"))
</script>
</html>