虚拟dom
虚拟dom 表现真实dom的javascript对象树,便于批量更新dom
vNode的大致表现
{
tag: "div",
children: [
{
text: "hi vNode"
}
]
}
如何变成真实的dom
vue利用render 函数把模版转变成vNode,一旦节点发生改变,render函数从新运行,产生新的vNode,vue比较新旧vNode,进行修改。
核心模块
- 响应:创建javascript对象,并且监听
- 编译: 获取html模版,产生render function
- 渲染
- 渲染:触发render function返回vNode,
- 挂载:挂载到mount
- 补丁:把新旧节点发送path,比较更新
自定义简单的 mount
// h 接受tag,props,children
function h(tag, props, children) {
return {
tag,
props,
children
}
}
// mount接受h函数
function mount(vNode, container) {
const {tag, props, children} = vNode
const el = document.createElement(tag)
// props
if (props) {
for(let key in props) {
if (!key.startsWith('on')) {
el.setAttribute(key, props[key])
}
}
}
// children
if (Array.isArray(children)) {
children.forEach(child => {
mount(child, el)
})
} else {
el.textContent = children
}
container.appendChild(el)
}
// 渲染
const vDom = h('div', {class: 'red'}, [
h('span', null, 'hello vue h function')
])
mount(vDom, document.getElementById('app'))
自定义简单的 path
这里的path 只做最简单的处理,没有特别的优化
<!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;
}
.blue {
color: blue;
}
</style>
</head>
<body>
<div id="app"></div>
<script>
// h 接受tag,props,children
function h(tag, props, children) {
return {
tag,
props,
children
}
}
// mount接受h函数
function mount(vNode, container) {
const {tag, props, children} = vNode
// vNode.el 保存节点,用于path函数 比对替换
const el = vNode.el = document.createElement(tag)
// props
if (props) {
for(let key in props) {
if (!key.startsWith('on')) {
el.setAttribute(key, props[key])
}
}
}
// children
if (Array.isArray(children)) {
children.forEach(child => {
mount(child, el)
})
} else {
el.textContent = children
}
container.appendChild(el)
}
// 渲染
const vDom = h('div', {class: 'red'}, [
h('div', null, 'hello vue h function')
])
mount(vDom, document.getElementById('app'))
// 比较新旧节点,更新视图(n1 旧节点, n2 新节点)
function patch(n1, n2) {
if (n1.tag === n2.tag) {
const el = n2.el = n1.el;
// 比较props,
// new porps === null, old props !== null
// new porps !== null, old props === null
// 上面2中可以直接替换
// new porps !== null, new porps !== null
// props 里面的属性是否,存在,是否相等
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 比较属性
for (let key in newProps) {
const oldVal = oldProps[key]
const newVal = newProps[key]
// 不相等替换(替换的元素,在mount中保存在了vDom的el属性上)
if (newVal !== oldVal) {
el.setAttribute(key, newVal)
}
}
for (let key in oldProps) {
// 新节点上不存在,直接删除
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// 比较 children
const oldChildren = n1.children
const newChildren = n2.children
if (typeof newChildren === 'string') {
// 直接替换
if (typeof oldChildren === 'string') {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.textContent = newChildren
}
} else {
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(child => {
mount(child, el)
})
} else {
// 这里不做复杂考虑,不优化,写一个简单的
// 现比较,共同的长度的children,在对多余或者缺少的children 做处理
const commonLen = Math.min(newChildren.length, oldChildren.length)
// 共有的
for(let i = 0; i < commonLen; i++) {
patch(oldChildren[i], newChildren[i])
}
// newChildren 独有的
if (newChildren.length > oldChildren.length) {
newChildren.splice(oldChildren.length).forEach(child => {
mount(child, el)
})
}
if (newChildren.length < oldChildren.length) {
oldChildren.splice(newChildren).forEach(child => {
el.removeChild(child)
})
}
}
}
} else {
// 直接替换?
}
}
const vDom2 = h('div', {class: 'blue'}, [
h('div', null, [h('span', null, 'hello path')])
])
patch(vDom, vDom2)
</script>
</body>
</html>
简单的追踪更新
手动通知
<script>
// 依赖监听
let activeEffect
// 存放依赖关系
class Dep {
// 存放依赖
subscribers = new Set()
// 搜集依赖
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()
watchEffect(() => {
dep.depend() // track
console.log('=====effect')
})
// trigger
dep.notify() // 通知,会再次触发effect
</script>
小小的优化一下,变的稍稍智能一些,不需要手动通知,加入数据改变功能
// 依赖监听
let activeEffect
// 存放依赖关系
class Dep {
constructor(value) {
// 存放依赖
this.subscribers = new Set()
this._value = value
}
// get set 实现自动追踪和通知更新的操作
get value() {
// trigger
this.depend() // 通知,会再次触发effect
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 ok = new Dep(false)
const msg = new Dep('hello')
watchEffect(() => {
if (ok.value) {
console.log('=====effect', msg.value)
} else {
console.log('不追踪')
}
})
实现一个简单的reactive
// 依赖监听
let activeEffect
// 存放依赖关系
class Dep {
// 存放依赖
subscribers = new Set()
// 搜集依赖
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
// 触发
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}
// target 的映射关系,最终,每个target object 作为map里面的key,每一个target.get(key),又对应一个map(map里面存放的是target object 的属性,对应的dep 关系),这样实现数据更新操作
const targetMap = new WeakMap()
function getDep (target, key) {
let depsMap = targetMap.get(target)
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 reactiveHandlers = {
get(target, key, receiver){
console.log('===target', target)
console.log('==key',key)
const dep = getDep(target, key)
dep.depend()
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver){
const dep = getDep(target, key)
const result = Reflect.set(target, key, value, receiver)
dep.notify()
return result
}
}
function reactive(obj) {
return new Proxy(obj, reactiveHandlers)
}
const state = reactive({
count: 1,
age: 20
})
// 监听
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
watchEffect(() => {
console.log('=====effect', state.count)
})
state.count++
实现一个mini-vue
现在有了reactive,path函数,可以把2者结合起来实现一个mini-vue了
<!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>
<script>
function h(tag, props, children) {
return {
tag,
props,
children
}
}
function mount(vNode, container) {
const {tag, props, children} = vNode
// vNode.el 保存节点,用于path函数 比对替换
const el = vNode.el = document.createElement(tag)
// props
if (props) {
for(let key in props) {
if (!key.startsWith('on')) {
el.setAttribute(key, props[key])
} else {
el.addEventListener(key.slice(2).toLowerCase(), props[key])
}
}
}
// children
if (Array.isArray(children)) {
children.forEach(child => {
mount(child, el)
})
} else {
el.textContent = children
}
container.appendChild(el)
}
function patch(n1, n2) {
if (n1.tag === n2.tag) {
const el = n2.el = n1.el;
const oldProps = n1.props || {}
const newProps = n2.props || {}
// 比较属性
for (let key in newProps) {
const oldVal = oldProps[key]
const newVal = newProps[key]
if (newVal !== oldVal) {
el.setAttribute(key, newVal)
}
}
for (let key in oldProps) {
if (!(key in newProps)) {
el.removeAttribute(key)
}
}
// 比较 children
const oldChildren = n1.children
const newChildren = n2.children
if (typeof newChildren === 'string') {
if (typeof oldChildren === 'string') {
if (newChildren !== oldChildren) {
el.textContent = newChildren
}
} else {
el.textContent = newChildren
}
} else {
if (typeof oldChildren === 'string') {
el.innerHTML = ''
newChildren.forEach(child => {
mount(child, el)
})
} else {
const commonLen = Math.min(newChildren.length, oldChildren.length)
for(let i = 0; i < commonLen; i++) {
patch(oldChildren[i], newChildren[i])
}
if (newChildren.length > oldChildren.length) {
newChildren.splice(oldChildren.length).forEach(child => {
mount(child, el)
})
}
if (newChildren.length < oldChildren.length) {
oldChildren.splice(newChildren).forEach(child => {
el.removeChild(child)
})
}
}
}
} else {
// 直接替换?
}
}
let activeEffect
class Dep {
// 存放依赖
subscribers = new Set()
// 搜集依赖
depend() {
if (activeEffect) {
this.subscribers.add(activeEffect)
}
}
// 触发
notify() {
this.subscribers.forEach(effect => {
effect()
})
}
}
const targetMap = new WeakMap()
function getDep (target, key) {
let depsMap = targetMap.get(target)
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 reactiveHandlers = {
get(target, key, receiver){
const dep = getDep(target, key)
dep.depend()
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver){
const dep = getDep(target, key)
const result = Reflect.set(target, key, value, receiver)
dep.notify()
return result
}
}
function reactive(obj) {
return new Proxy(obj, reactiveHandlers)
}
function watchEffect(effect) {
activeEffect = effect
effect()
activeEffect = null
}
const App = {
data: reactive({count: 1}),
render() {
return h('div',
{onClick: () => {
this.data.count++
}},
String(this.data.count)
)
}
}
function mountApp(component, container) {
let isMounted = false;
let oldVdom
watchEffect(() => {
if (!isMounted) {
oldVdom = component.render()
mount(oldVdom, container)
isMounted = true
} else {
const newVdom = component.render()
patch(oldVdom, newVdom)
oldVdom = newVdom
}
})
}
mountApp(App, document.getElementById('app'))
</script>
</body>
</html>