手写vue2 - 实现$mount()和简单diff算法

前言

在我们之前的《手写vue - 实现数据响应式、数据双向绑定和事件监听》中,有些许不足:

  1. 直接模板 => dom,跳过了虚拟dom的生成和相关操作。
  2. 由于没有VNode,每当data中的数据发生变化时,都会进行实时的更新,增加了程序的负担。
  3. 每个key对应一个Watcher,当其中一个值发生变化时,都会遍历执行更新方法,在Vue2是每个组件实例对应一个Watcher,利用VNode和diff算法减少更新的次数,且是批量异步更新。

我们在拜读Vue2源码之后,参考源码的编程设计思路,对我们之前编写的案例进行改进,主要是以下几个方面的改进:

  1. 一个组件只有一个Watcher,从而减少更新方法的触发次数,降低性能消耗;
  2. 增加Vnode的概念,利用我们的简单diff算法,不直接对模板中的真实dom进行操作。

一、明确思路

1. Vue2的设计思路

在Vue2中,组件实例的创建挂载到渲染挂载的执行顺序是:

  1. $mount()挂载,其中会创建一个Watcher,也就是一个组件实例对应一个Watcher;
  2. 定义updateComponent()组件更新方法,将其保存到Watcher中;
  3. 当视图需要更新时,updateComponent()调用reader()获取vndoe,执行_update()将vnode转化为真实dom;
  4. _update()中调用__patch__(),也就采用diff算法。

关于Vue2源码相关内容,感兴趣的同学可以看下我之前写的《vue2源码解析(一) - new Vue()的初始化过程》,可能会对我们这次“造轮子”有所帮助。

2. 改造思路

  1. 增加$mount(),updateComponent(),_update()等方法;
  2. 定义diff算法的相关方法__patch__()等。
  3. 改造Watcher类和Dep类,即两者关系为1 : n;
  4. 我们案例中去除了之前的Compile编译器,所以没有指令和事件监听相关内容,感兴趣的可以对应加上。

二、手写Vue2

1. 定义数据响应式和通知依赖更新

这部分代码与我们之前基本保持一致。

// 定义响应式数据的方法
function defineReactive(obj, key, val) {
    // 递归,将对象进行深层次响应式处理
    // 如obj = { foo: 'foo', bar: { a: 1 } }
    observe(val)

    // 为每个key创建Dep实例
    const dep = new Dep()

    Object.defineProperty(obj, key, {
        get() {
            // 依赖收集,Dep.target为Watcher对象
            Dep.target && dep.addDep(Dep.target)
            return val
        },
        set(newVal) {
            if (newVal != val) {
                // 考虑到用户可能对对象进行 obj.bar = { b: 2 } 的操作
                // 重新对新值newVal做响应式处理
                observe(newVal)
                val = newVal

                // 更新依赖
                dep.notify()
            }
        }
    })
}

// 将普通对象转化为响应式对象的方法
function observe(obj) {
    // 判断对象类型,若对象类型不是object或对象值为null,则不跳出
    // 这里我暂不考虑对象类型为Array时的情况
    if (typeof obj !== 'object' || obj === null) {
        return
    }
    
    new Observer(obj)
}

// 用户对原对象追加新属性,对新属性做响应式处理的方法
// 仿Vue.$set()方法
function set(obj, key, val) {
    defineReactive(obj, key, val)
}

// 代理,作用:使用户能直接通过vm实例访问到data里的数据,即this.xxx。否则需this.$data.xxx
function proxy(vm) {
    Object.keys(vm.$data).forEach(key => {
        Object.defineProperty(vm, key, {
            get() {
                return vm.$data[key]
            },
            set(newVal) {
                vm.$data[key] = newVal
            }
        })
    })   
}

// Observer类,对传入的value值做响应式处理
class Observer {
    constructor(value) {
        this.value = value

        if (Array.isArray(value)) {
            // todo
        } else {
            this.walk(value)
        }
    }

    walk(obj) {
        // 遍历对象的属性,做响应式处理
        // 如obj = { foo: 'foo', bar: { a: 1 } }
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key])
        })
    }
}

2. JVue类和更新渲染

我们改造的重点就是这一部分。主要是下面几点:

  1. 定义$mount()挂载方法,创建Watcher保存组件渲染更新方法;
  2. 编写我们简版的diff算法和相关dom操作方法。
// jvue类
class JVue {
    constructor(options) {
        this.$options = options
        this.$data = options.data
        // $data做响应式处理
        observe(this.$data)

        // 代理。作用:使用户能直接通过vm实例访问到data里的数据,即this.xxx。否则需this.$data.xxx
        proxy(this)
        
        if (options.el) {
            this.$mount(options.el)
        }
    }

    // $mount
    $mount(el) {
        // 获取根节点元素
        this.$el = document.querySelector(el)
        // 组件的渲染函数
        const updateComponent = () => {
            // 从选项options中获取reader函数
            const { reader } = this.$options
            // reader()方法的作用就是获取虚拟dom,$createElement方法就是reader()中参数h
            const vnode = reader.call(this, this.$createElement)
            // 把vnode转化成真实dom
            this._update(vnode)
        }

        // 为每个组件实例创建一个Watcher
        new Watcher(this, updateComponent)        
    }

    // 这个就是我们reader()中的参数h。
    // 参数:tag标签;props标签属性,可为空;children子节点,也可能是文本标签。
    // 注意:元素的childrenNodes与text互斥,即文本标签不可能存在子节点
    $createElement(tag, props, children) {
        return { tag, props, children }
    }

    // 根据判断更新节点信息
    _update(vnode) {
        // 获取上一次的vnode树
        const prevVnode = this._vnode
        
        // 若老节点树不存在,则初始化,否则更新
        if(!prevVnode) {
            // 初始化
            this.__patch__(this.$el, vnode)
        } else {
            // 更新
            this.__patch__(prevVnode, vnode)
        }
    }

    __patch__(oldVnode, vnode) {
        // 判断oldVnode是否为真实dom
        // 是则将vnode转化为真实dom,添加到根节点
        // 否则遍历判断新老两棵vnode树,做增删改操作
        if(oldVnode.nodeType) { // 真实dom,初始化操作
            // 获取根元素的父节点,即body
            const parent = oldVnode.parentNode
            // 获取根元素的下个节点
            const refElm = oldVnode.nextSibling
            // 递归创建子节点
            const el = this.createElm(vnode)
            // 在body下,根节点旁插入el
            parent.insertBefore(el, refElm)
            // 删除之前的根节点
            parent.removeChild(oldVnode)

            // 保存vdone,用于下次更新判断
            this._vnode = vnode
        } else { // 更新操作
            // 获取vnode对应的真实dom,用于做真实dom操作
            const el = vnode.el = oldVnode.el
            // 判断是否为同一个元素
            if(oldVnode.tag === vnode.tag) {
                // props属性更新
                this.propsOps(el, oldVnode, vnode)

                // children更新
                // 获取新老节点的children
                const oldCh = oldVnode.children
                const newCh = vnode.children
                // 若新节点为文本
                if(typeof newCh === 'string') {
                    // 若老节点也为文本
                    if(typeof oldCh === 'string') {
                        // 若新老节点文本内容不一致,则文本内容替换为新文本内容
                        if(newCh !== oldCh) {
                            el.textContent = newCh
                        }
                    } else { // 若老节点有子节点,则情况后设置文本内容
                        el.textContent = newCh
                    }
                } else { // 若新节点有子节点
                    // 若老节点无子节点,为文本,则清空文本后创建并新增子节点
                    if(typeof oldCh === 'string') {
                        el.textContent = ''
                        newCh.forEach(children => this.createElm(children))
                    } else { // 若老节点也有子节点,则检查更新
                        this.updateChildren(el, oldCh, newCh)
                    }
                }
            }
        }
    }

    // 创建节点元素
    createElm(vnode) {
        // 创建一个真实dom
        const el = document.createElement(vnode.tag)

        // 若存在props属性,则处理
        if(vnode.props) {
            // 遍历设置元素attribute属性
            for (const key in vnode.props) {
                el.setAttribute(key, vnode.props[key])
            }
        }

        // 若存在chilren,则处理
        if(vnode.children) {
            // 判断children类型
            if(typeof vnode.children === 'string') {
                // 该节点为文本
                el.textContent = vnode.children
            } else {
                // 该节点有子节点
                // 递归遍历创建子节点,追加到元素下
                vnode.children.forEach(v => {
                    const child = this.createElm(v)
                    el.appendChild(child)
                })
            }
        }

        // 保存真实dom,用于diff算法做真实dom操作
        vnode.el = el

        return el
    }

    // 节点的props属性操作方法
    propsOps(el, oldVnode, newVnode) {
        // 获取新老节点的属性列表
        const oldProps = oldVnode.props || {}
        const newProps = newVnode.props || {}
        // 遍历新属性列表
        for (const key in newProps) {
            // 若老节点中不存在新节点的属性,则删除该属性
            if (!(key in oldProps)) {
                el.removeAttribute(key)
            } else {
                // 否则更新属性内容
                const oldValue = oldProps[key]
                const newValue = newProps[key]
                if(oldValue !== newValue) {
                    el.setAttribute(key, newValue)
                }
            }
        }
    }

    // 更新子节点
    updateChildren(parentElm, oldCh, newCh) {
        // 获取新老子节点树的最小长度
        const len = Math.min(oldCh.length, newCh.length)

        // 根据最小长度len遍历做节点更新
        for (let i = 0; i < len; i++) {
            this.__patch__(oldCh[i], newCh[i])
        }

        // 判断新老节点树的长度,做新增或删除操作
        // 若老节点树长度大于新新节点树长度,则删除多余节点,反之则新增节点
        if(oldCh.length > newCh.length) {
            oldCh.slice(len).forEach(child => {
                const el = this.createElm(child)
                parentElm.removeChild(this.createElm(child))
            })
        } else if(newCh.length > oldCh.length) {
            newCh.slice(len).forEach(child => {
                const el = this.createElm(child)
                parentElm.appendChild(el)
            })
        }
    }
}

3. Watcher类收集依赖

仿源码的编程思路,将传入的fn(即updateComponent())保存到getter中,由get()方法触发依赖收集和执行更新渲染函数。

/ watcher类
// 监听器类。负责依赖更新
class Watcher {
    // vm: vue实例
    // fn: vm组件实例对应的渲染更新方法
    constructor(vm, fn) {
        this.vm = vm
        this.getter = fn
        
        // 触发依赖收集和执行渲染函数
        this.get()
    }

    // 触发依赖收集和执行渲染函数
    get() {
        // 触发依赖收集
        Dep.target = this
        // 执行渲染函数
        this.getter.call(this.vm)
        Dep.target = null
    }

    update() {
        this.get()
    }
}

4. Dep类依赖管理和通知更新

这一部分也没有太大的变化,只是将Deps的类型由Array改为了Set。使Dep和Watcher的关系变为N : 1。

// dep类
// 依赖收集,统一通知执行依赖中的各个更新函数
class Dep {
    // 为Vue.$data中的每个key创建一个依赖数组
    constructor() {
        this.deps = new Set()
    }
    
    // 为Vue.$data中的每个key依赖数组deps追加对应的watcher
    // 追加时机:响应式对象的get()方法中,即在对Vue.$date做响应式处理时的defineReactive方法的get()中收集依赖
    addDep(watcher) {
        this.deps.add(watcher)
    }

    // 当响应式数据更新时,进行统一通知更新依赖
    notify() {
        this.deps.forEach(watcher => watcher.update())
    }
}

5. 测试案例

<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<div id="app">
  
</div>
<script src="jvue.js"></script>
<script>
  const app = new JVue({
    el: "#app",
    data: {
      counter: 1,
      desc: '<span style="color:red">哈哈哈哈</span>',
      text: ''
    },
    reader(h) {
      return h('div', {id: '#app'}, [
        h('p', null, 'first blood ' + this.counter)
      ])
    },
    methods: {
      onClick() {
        alert('冬瓜冬瓜我是西瓜')
      }
    }
  });
  setInterval(() => {
    app.counter++;
  }, 1000);
</script>

三、总结

手写Vue2后,对于Vue2的源码总结

1. Vue的挂载渲染流程

$mount() => updateComponent() => reader() => _update() => __patch__()

2. Watcher

  1. Watcher的创建发生在$mount()
  2. 每个组件对应一个reader Watcher

3. reader()和__patch__()

  1. diff算法发生在__patch__()中
  2. reader()的作用是生成vnod
  3. __patch__()的作用是将vnode转化为真实dom
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值