在我们之前的《手写vue - 实现数据响应式、数据双向绑定和事件监听》中,有些许不足:
直接模板 => dom,跳过了虚拟dom的生成和相关操作。由于没有VNode,每当data中的数据发生变化时,都会进行实时的更新,增加了程序的负担。每个key对应一个Watcher,当其中一个值发生变化时,都会遍历执行更新方法,在Vue2是每个组件实例对应一个Watcher,利用VNode和diff算法减少更新的次数,且是批量异步更新。我们在拜读Vue2源码之后,参考源码的编程设计思路,对我们之前编写的案例进行改进,主要是以下几个方面的改进:
一个组件只有一个Watcher,从而减少更新方法的触发次数,降低性能消耗;增加Vnode的概念,利用我们的简单diff算法,不直接对模板中的真实dom进行操作。在Vue2中,组件实例的创建挂载到渲染挂载的执行顺序是:
$mount()挂载,其中会创建一个Watcher,也就是一个组件实例对应一个Watcher;定义updateComponent()组件更新方法,将其保存到Watcher中;当视图需要更新时,updateComponent()调用reader()获取vndoe,执行_update()将vnode转化为真实dom;_update()中调用__patch__(),也就采用diff算法。关于Vue2源码相关内容,感兴趣的同学可以看下我之前写的《vue2源码解析(一) - new Vue()的初始化过程》,可能会对我们这次“造轮子”有所帮助。
这部分代码与我们之前基本保持一致。
// 定义响应式数据的方法 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]) }) } }我们改造的重点就是这一部分。主要是下面几点:
定义$mount()挂载方法,创建Watcher保存组件渲染更新方法;编写我们简版的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) }) } } }仿源码的编程思路,将传入的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() } }这一部分也没有太大的变化,只是将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()) } }手写Vue2后,对于Vue2的源码总结
$mount() => updateComponent() => reader() => _update() => __patch__()