框架设计里到处体现了权衡的艺术。
声明式和命令式框架的区别:
声明式:关注结果,将实现的过程进行了封装,比如将大象放入冰箱那么他只会发一个命令将大象放入冰箱
命令式:关注过程,将实现的方式展现出来,比如将大象放入冰箱,他的实现可能是第一步将冰箱门打开,第二步放入大象,第三步关上冰箱门。
vue.js是一个编译运行的框架。
js对象
const title = {
tag : 'h1',
props: {
onClick: handler
},
children: [
{tag: 'span'}
]
}
js对象对应在vue.js中就是如下所示
<h1 @click="handler"><span>sapn>h1>
如下所示这是个虚拟DOM对象。
const vnode = {
tag: "div",
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
设计一个函数将上面的虚拟DOM变成真实的DOM
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') {
el.appendChild(document.createTextNode(vnode.children))
}else if(Array.isArray(vnode.children)){
vnode.children.forEach(child => renderer(child,el))
}
container.appendChild(el)
}
这个函数将虚拟DOM解析为了真实的DOM元素
完整代码
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>title>
<style>
div::before{
width: 100px;
height: 100px;
content: "";
background-color: red;
}
style>
head>
<body>
<div>111div>
<script>
const vnode = {
tag: "div",
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
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') {
el.appendChild(document.createTextNode(vnode.children))
}else if(Array.isArray(vnode.children)){
vnode.children.forEach(child => renderer(child,el))
}
container.appendChild(el)
}
renderer(vnode, document.body)
script>
body>
html>
组件的本质就是一组DOM元素的封装
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>title>
head>
<body>
<div>111div>
<script>
const MyComponent = function() {
return {
tag: "div",
props: {
onClick: () => alert('hello')
},
children: 'click me'
}
}
// vnode 的tag元素可以是一个函数,字符串乃至对象
const vnode = {
tag: MyComponent
}
function renderer(vnode, container){
if(typeof vnode.tag === 'string'){
mountElement(vnode, container)
}else if(typeof vnode.tag === 'function'){
mountComponent(vnode, container)
}
}
// 和原来的renderer函数一致
function mountElement(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') {
el.appendChild(document.createTextNode(vnode.children))
}else if(Array.isArray(vnode.children)){
vnode.children.forEach(child => renderer(child,el))
}
container.appendChild(el)
}
// 解析到tag再次调用renderer函数
function mountComponent(vnode, container){
const subtree = vnode.tag()
renderer(subtree, container)
}
renderer(vnode, document.body)
script>
body>
html>
将vue的模板编译到script的内容里
<template>
<div @click="handler">
click me
div>
template>
<script>
export default {
data() {},
methods:{
handler: () => {
}
}
}
script>
如上所示是一个模板的基本构造,编译器会把html也就是template标签的内容编译到export里面,编译后的代码如下
<script>
export default {
data() {},
methods:{
handler: () => {
}
},
render() {
return h('div', {onclick: handler}, 'click me')
}
}
</script>
组件的实现依赖于渲染器,模板的编译依赖于编译器。vue.js就是将各个模组组织到一起。
模板
<div id="foo" :class="cls">div>
编译器编译成渲染函数
render(){
return {
tag: 'div',
props: {
id: 'foo',
class: cls
},
patchFlags: 1
}
}
总上所示,class是一个变量,为了后续的方便更新,减少不必要的消耗,编译器会加上特殊的标记。方便后续更新视图。patchFlags便是提示那些可以更新的值。
const obj = { text : 'hello world'}
function effect() {
document.body.innerText = obj.text
}
effect();
obj.text = 'hello vue3'
如上所示,obj 就是一个响应数据,执行了副作用函数之后,页面的内容变为hello world,但是之后修改obj的值,页面上的内容并没有变过来。
我们将给对象设置的时候,让其重新执行副作用函数,
监听对象的属性读取操作,vue2使用的是Object.defineProperty,vue3使用的是对象Proxy方法
const bucket = new Set()
const data = { text : 'hello world'}
const obj = new Proxy(data,{
get(target, key){
bucket.add(effect)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => fn())
return true
}
})
function effect() {
document.body.innerText = obj.text
}
effect();
setTimeout(() =>{
obj.text = 'hello vue3'
},1000)
在读取对象属性是,将副作用添加到桶中,设置的时候将桶中的方法再执行一次。
1.读取操作的时候将副作用函数放入桶中
2.当设置操作的时候在将副作用函数取出执行
// 全局变量存储副作用函数
let activeEffect;
const bucket = new Set()
// 接受一个函数作为参数
function effect(fn) {
activeEffect = fn;
fn();
}
const data = { text : 'hello world'}
const obj = new Proxy(data,{
get(target, key){
if(activeEffect){
bucket.add(activeEffect)
}
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => fn())
return true
}
})
// 匿名函数注册
effect(fn => {
console.log("执行了这个函数")
document.body.innerText = obj.text
});
setTimeout(() =>{
obj.notExist = 'hello vue3'
},1000)
如上代码所示,给obj添加notExist属性的时候,副作用函数也执行了,因为副作用函数是直接是挂在Object对象上的,那这个响应系统不是很完善,实际应该在对应的属性上比较好。
接下来,我们实现这个方法。
首先用WeakMap代替Set,修改拦截方法。
// 全局变量存储副作用函数
let activeEffect;
const bucket = new WeakMap()
// 接受一个函数作为参数
function effect(fn) {
activeEffect = fn;
fn();
}
const data = { text : 'hello world'}
const obj = new Proxy(data,{
get(target, key){
if(!activeEffect){
return target[key]
}
let depsMap = bucket.get(target)
if(!depsMap){
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key)
if(!deps){
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect);
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
let depsMap = bucket.get(target)
if(!depsMap){
return
}
let effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
})
// 匿名函数注册
effect(fn => {
console.log("执行了这个函数")
document.body.innerText = obj.text
});
setTimeout(() =>{
obj.notExist = 'hello vue3'
},1000)
如上所示,对应的键就和副作用函数绑定在一起了。
将方法封装一下。如下:
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>title>
head>
<body>
<script>
// 全局变量存储副作用函数
let activeEffect;
const bucket = new WeakMap()
// 接受一个函数作为参数
function effect(fn) {
activeEffect = fn;
fn();
}
const data = { text : 'hello world'}
const obj = new Proxy(data,{
get(target, key){
track(target, key);
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
function track(target, key){
if(!activeEffect){
return
}
let depsMap = bucket.get(target)
if(!depsMap){
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key)
if(!deps){
depsMap.set(key, (deps = new Set()))
}
deps.add(activeEffect);
}
function trigger(target, key){
let depsMap = bucket.get(target)
if(!depsMap){
return
}
let effects = depsMap.get(key)
effects && effects.forEach(fn => fn())
}
// 匿名函数注册
effect(fn => {
console.log("执行了这个函数")
document.body.innerText = obj.text
});
setTimeout(() =>{
obj.notExist = 'hello vue3'
},1000)
script>
body>
html>
如下所示,ok为true的时候,页面上的值来自于text,否则就是一个固定值。此时ok和text两个键都绑定了副作用函数,如果我们将ok的值改为false,那么text对应的副作用函数应该是需要去除的。这就是分支。
const data = { ok: true, text : 'hello world'}
// 匿名函数注册
effect(fn => {
console.log("执行了这个函数")
document.body.innerText = obj.ok ? obj.text : 'not';
});
首先想到的就是执行副作用函数的时候重新绑定以下副作用函数。
function effect(fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
fn();
}
effectFn.deps = []
effectFn();
}
function cleanup(effectFn){
for(let i = 0; i < effectFn.deps.length;i++){
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
执行副作用函数的时候,将effectFn.deps的内容清除掉,然后执行副作用函数。这段我看的也不是很透彻。
修正对应的trigger方法
function trigger(target, key){
let depsMap = bucket.get(target)
if(!depsMap){
return
}
let effects = depsMap.get(key)
const effectsToRun= new Set(effects)
effectsToRun.forEach(fn => fn())
}
effect(() => {
obj.foo = obj.foo + 1
})
在一个方法中既有读取也有设置操作,就会导致无限循环
function trigger(target, key){
let depsMap = bucket.get(target)
if(!depsMap){
return
}
let effects = depsMap.get(key)
const effectsToRun= new Set()
effects && effects.forEach(effectFn =>{
// 设置同一个方法只添加一次,防止无限循环
if(effectFn != activeEffect){
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(fn => fn())
}
effect下的lazy属性