vue3学习-手动实现简单的vue方法

233 阅读2分钟

虚拟dom

虚拟dom 表现真实dom的javascript对象树,便于批量更新dom

vNode的大致表现

{
    tag: "div",
    children: [
        {
            text: "hi vNode"
        }
    ]
}

如何变成真实的dom

vue利用render 函数把模版转变成vNode,一旦节点发生改变,render函数从新运行,产生新的vNode,vue比较新旧vNode,进行修改。

核心模块

  1. 响应:创建javascript对象,并且监听
  2. 编译: 获取html模版,产生render function
  3. 渲染
  • 渲染:触发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>