关于前端:Vue2x-原理剖析二之手写一个简版Vue

1次阅读

共计 10533 个字符,预计需要花费 27 分钟才能阅读完成。

前言

在上一篇文章 Vue2.x 响应式原理分析(一)咱们曾经搞清楚了数据响应式的原理,那么明天无妨就让咱们利用上次的实现来造一个简版的 Vue 吧!

创立一个 MVVM 类

// TVue.js
/**
* @desc: TVue 是一个 MVVM 类,也是咱们本人手写的简版 Vue
*    @params {} options 实例创立时传入的选项
*/
class TVue {constructor (options) {
    this.$options = options
    this.$data = options.data
  }
}

TVue 在创立的时候须要做两件事:

  • 对传入的数据做响应式解决;
  • 编译模版将后果渲染
class TVue {constructor (options) {
    this.$options = options
    this.$data = options.data
         // 1. 响应式实现
    observe(this.$data)
         // 2. 编译
    if (options.el) {this.$mount(options.el)
    }
  }
}

⚠️咱们平时在应用 data 属性值的时候为什么能够间接通过 this.xxx 拜访,而无需通过 this.data.xxx 来拜访呢?

这是因为 Vue 源码里做了代理,将 vm 实例的 data 属性值间接代理到了 vm 实例上

这里咱们也能够学习源码来实现一个代理办法

function proxy (vm) {Object.keys(vm.$data).forEach(key=>{
    Object.defineProperty(vm, key, {get() {return vm.$data[key]
      },
      set(v) {vm.$data[key] = v
      }
    })
  })
}
// 此时 咱们的 TVue 应该如下:class TVue {constructor (options) {
    this.$options = options
    this.$data = options.data
         // 1. 响应式实现
    observe(this.$data)
    // 1.5 为 $data 做代理
    proxy(this)
         // 2. 编译
    if (options.el) {this.$mount(options.el)
    }
  }
}

observe 办法实现

通过上篇文章的分享,Vue2.x 中利用了 JS 语言个性 Object.defineProperty(),通过定义对象属性 getter/setter 拦挡对属性的拜访,上面让咱们来回顾下 observe 办法的创立和性能吧

function observe(obj) {if (typeof obj !== 'object' || typeof === null) {
         // 如果传入的数据不是对象或者为 null 不做操作,间接返回   
    return 
  }
  // 只有是对象,就创立一个伴生的 Observer 实例
  new Observer(obj)
}
// Observer 对象依据数据类型执行对应的响应化操作 
// defineReactive 定义对象属性的 getter/setter
// getter 负责增加依赖,setter 负责告诉更新
class Observer {constructor(options) {if (Array.isArray(obj)) {// todo: 数组有非凡解决,可在源码 Array.js 查阅 这次咱们先不做数组响应式的解决} else {this.walk(options)
    }
  }
  walk(obj) {
         // 遍历对象的所有属性并对其做响应式解决   
    Object.keys(obj).forEach(key => {defineReactive(obj, key, obj[key]
    })
  }
}            
function defineReactive (obj, key, val) {
    // 向下递归遍历
  observe(val)
    Object.defineProperty(obj, key, {get () {
      // todo: 依赖收集
      return val
    },
    set(newVal) {if (newVal !== val) {
        val = newVal
        // 解决赋的值是对象的状况(譬如 test.foo={f1: 666})
        observe(val)
         // todo: 派发订阅
      }
    }
  })
}

Watcher 和 Dep 的创立

1. Watcher

  • 依赖收集后保留在 deps 里
  • 变动的时候 deps 作为发布者告诉 watcher
  • watcher 进行回调渲染
class Watch {
  // expOrFn: 创立 Watcher 实例时传入的渲染函数
  constructor (vm, expOrFn){
    this.vm = vm
    this.getter = expOrFn
    // 触发依赖收集
    this.get()}
  get() {
    Dep.target = this
    this.getter.call(this.vm)
    Dep.target = null
  }
  update() {
    // Dep 将来回告诉更新
    this.get()}
}

2. Dep

  • 发布者,能够订阅多个观察者
  • 收集依赖后会有一个或者多个 watcher
  • 一旦有变动便告诉所有 watcher
class Dep {
  // 依赖:和响应式对象的 key 一一对应
  constructor (){
    // 避免反复创立
    this.deps = new Set()}
  addDep (watcher) {this.deps.add(watcher)
  }
  notify() {this.deps.forEach(watcher => watcher.update())
  }
}

3. 关系

  • Dep 负责管理一组 Watcher,包含 watcher 实例的增删及告诉更新
  • Watcher 解析一个表达式并收集依赖,当数值变动时触发回调函数,罕用于 $watch API 和指令中。每个组件也会有对应的 Watcher,数值变动会触发其 update 函数导致从新渲染

4. 革新 defineReactive 办法 — 创立 Dep 实例,收集依赖 & 派发订阅

function defineReactive (obj, key, val) {
    // 如果 val 是对象,须要递归解决之
  observe(val)
  // 创立 Dep 实例
  const dep = new Dep()
    Object.defineProperty(obj, key, {get () {
      // 取值触发依赖收集
      Dep.target && dep.addDep(Dep.target)
      return val
    },
    set(newVal) {if (newVal !== val) {
        val = newVal
        // 如果 newVal 是对象,也要做响应式解决
        observe(val)
         // 告诉更新
        dep.notify()}
    }
  })
}

编译模版,$mount 实现

1. $mount 创立

function $mount (el){this.$el = document.createElement(el)
      // 定义更新函数 理论调用是在 lifeCycleMixin 中定义的_update 和 renderMixin 中定义的_render
    const updateComponent = ()=> {const { render} = this.$options
      // 执行渲染函数,获取 vnode
      const vnode = render.call(this, this.$createElement)
      this._update(vnode)
    }
    // 创立一个 watcher 实例
    new Watcher(this, updateComponent)
  }

2. $createElement 和 _update 实现

  • $createElement 只做一件事:返回虚构 dom($createElement 理论就是传递给 render 函数的 h)
function $createElement(tag, props, children) {return { tag, props, children}
}
  • _update 函数 负责更新 dom,转换 vnode 为 dom
function _update(vnode) {
    const prevVnode = this._vnode
    if(!prevVnode) {
      // 初始化
      this.__patch__(this.$el, vnode)
    } else {
      // 更新
      this.__patch__(prevVnode, vnode)
    }
  }

3. patch

patch 是 createPatchFunction 的返回值,传递 nodeOps 和 modules 是 web 平台特地实现

patch 实现

首先进行树级别比拟,可能有三种状况: 增删改。

  • new VNode 不存在就删;
  • old VNode 不存在就增;
  • 都存在就执行 diff 执行更新

    比拟两个 VNode,包含三种类型操作: 属性更新、文本更新、子节点更新 具体规定如下:

    1. 新老节点 均有 children 子节点,则对子节点进行 diff 操作,调用 **updateChildren
    2. 如果 新节点有子节点而老节点没有子节点 **,先清空老节点的文本内容,而后为其新增子节点。
    3. 新节点没有子节点而老节点有子节点 的时候,则移除该节点的所有子节点。
    4. 新老节点都无子节点 的时候,只是文本的替换。
function __patch__(oldVnode, Vnode) {
    // oldVnode 是 dom
    if (oldVnode.nodeType) {
      const parent = oldVnode.parentElement
      const refElm = oldVnode.nextSibling
      // 将虚构 dom 转换成实在 dom,并插入文档中
      const el = this.createElm(vnode)
      parent.insertBefore(el, refElm)
      // 删除老的节点
      parent.removeChild(oldVnode)
    } else {
      // 获取 dom
      const el = vnode.el = oldVnode.el
      // 新老节点是标签雷同 则比拟子节点
      if (oldVnode.tag === vnode.tag) {
        const oldCh = oldVnode.children
        const newCh = vnode.children
        /**
         * 新旧节点 diff 情景
         * 1. 新老节点都是 string(文本更新)* 2. 新老节点都是数组(首尾 diff)* 3. 新节点为数组,老节点为 string(递归创立 dom 树)* 4. 新节点是 string, 老节点是数组(间接将新节点赋值给老节点)*/
        if (typeof newCh === 'string') {
          // 新的为 string
          if (typeof oldCh === 'string') {
            // 新老都是 string
            if (newCh !== oldCh) {el.textContent = newCh}
          } else {
            // 新的是 string 老的不是 间接对 dom 做文本更新操作
            el.textContent = newCh
          }
        } else {
          // 新的为数组
          // 1. 新的是数组,老的为文本(阐明新增了子元素,须要递归创立新的 dom 树)
          if (typeof oldCh === 'string') {
            oldCh.innerHTML = ''
            newCh.forEach(vnode => this.createElm(vnode))
          } else {// 2. 新老节点都是数组(源码是做首位 diff 优化算法)
            this.updateChildren(el, oldCh, newCh)
          }
        }
      } else {// 不是同一标签 临时不思考}
    }
    // 保留以后 vnode
    this._vnode = vnode
  }

4. updateChildren 比对新旧两个 VNode 的 children 得出最小操作

  • 执行一个双循环是传统形式,vue 中针对 web 场景特点做了特地的算法优化
  • 在新老两组 VNode 节点的左右头尾两侧都有一个变量标记,在 遍历过程中这几个变量都会向两头聚拢 。当oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx 时完结循环
  • 上面是遍历规定:

    1. 当 oldStartVnode 和 newStartVnode 或者 oldEndVnode 和 newEndVnode 满足 sameVnode,间接将该 VNode 节点进行 patchVnode 即可,不需再遍历就实现了一次循环
    2. 如果 oldStartVnode 与 newEndVnode 满足 sameVnode。阐明 oldStartVnode 曾经跑到了 oldEndVnode 前面去了,进行 patchVnode 的同时还须要将实在 DOM 节点挪动到 oldEndVnode 的前面
    3. 如果 oldEndVnode 与 newStartVnode 满足 sameVnode,阐明 oldEndVnode 跑到了 oldStartVnode 的前 面,进行 patchVnode 的同时要将 oldEndVnode 对应 DOM 挪动到 oldStartVnode 对应 DOM 的后面。
    4. 如果以上状况均不合乎,则在 old VNode 中找与 newStartVnode 雷同的节点,若存在执行 patchVnode,同时将 elmToMove 挪动到 oldStartIdx 对应的 DOM 的后面。
    5. 当然也有可能 newStartVnode 在 old VNode 节点中找不到统一的 sameVnode,这个时候会调用 createElm 创立一个新的 DOM 节点。
    6. 至此循环完结,然而咱们还须要解决剩下的节点。

      • 当完结时 oldStartIdx > oldEndIdx,这个时候旧的 VNode 节点曾经遍历完了,然而新的节点还没有。说 明了新的 VNode 节点实际上比老的 VNode 节点多,须要将剩下的 VNode 对应的 DOM 插入到实在 DOM 中,此时调用 addVnodes(批量调用 createElm 接口)。
      • 然而,当完结时 newStartIdx > newEndIdx 时,阐明新的 VNode 节点曾经遍历完了,然而老的节点还有 残余,须要从文档中删 的节点删除

⚠️原算法比较复杂,能够间接去源码查阅,以下咱们能够实现一个没有通过优化的硬更新操作

// 更新孩子
  updateChildren (parentElm, odlCh, newCh) {const len = Math.min(oldCh.length, newCh.length)
    // 遍历较短的子数组
    for(let i =0; i<len; i++) {this.__patch__(oldCh[i], newCh[i])
    }
    // newCh 若是更长的那个,新增
    if(newCh.length > oldCh.length) {newCh.slice(len).forEach(vnode=>{const el = this.createElm(vnode)
        parentElm.appendChild(el)
      })
    } else if(newCh.length < old.length){parentElm.removeChild(vnode.el)
    }
  }

5. createElm 递归创立 dom 树

createElm(vnode) {const el = document.createElement(vnode.tag)
    // 解决 props
    if(vnode.props) {for(const key in vnode.props) {el.setAttribute(key, vnode.props[key])
      }
    }
    // 解决 children
    if (vnode.children) {
      // 解决文本
      if(typeof vnode.children === 'string') {el.textContent = vnode.children} else {
        // 子元素解决
        vnode.children.forEach(vnode=>{const child = this.createElm(vnode)
          el.appendChild(child)
        })
      }
      vnode.el = el
      return el
    }
  }

6. 残缺版本的 TVue 源码

function defineReactive(obj, key, val) {
  // ! 向下递归遍历
  observe(val)
  // 创立 Dep 实例
  const dep = new Dep()
  Object.defineProperty(obj, key, {get() {console.log(`get ${key}: ${val}`)
      Dep.target && dep.addDep(Dep.target)
      return val
    },
    set(newVal) {if (newVal !== val) {console.log(`set ${key}: ${newVal}`)
        val = newVal
        //! 解决赋的值是对象的状况(譬如 test.foo={f1: 666})
        observe(val)
        dep.notify()}
    }
  })
}
function observe(obj) {if (typeof obj !== 'object' || obj === null) {return}
  // * 只有 obj 是对象,就创立一个伴生的 Observer 实例
  new Observer(obj)

}
function proxy(vm) {Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {get() {return vm.$data[key]
      },
      set(v) {vm.$data[key] = v
      }
    })
  })
}
class Observer {constructor(options) {if (Array.isArray(options)) {// todo 数组有非凡解决} else {this.walk(options)
    }
  }
  walk(obj) {Object.keys(obj).forEach(key => {defineReactive(obj, key, obj[key])
    })
  }
}
class TVue {constructor(options) {
    this.$options = options
    this.$data = options.data
    // ! 1. 数据响应式
    observe(this.$data)
    // ! 1.5 代理 将 data 中的所有属性代理到 JVue 实例上不便用户应用
    proxy(this)
    // ! 2. 编译
    // new Compile(options.el, this)
    if (options.el) {this.$mount(options.el)
    }
  }
  $mount (el) {
    // 获取宿主元素
    this.$el = document.querySelector(el)
    const updateComponent = () => {
       // 执行渲染函数
      const {render} = this.$options;

      // 实在 dom 操作版实现
      // const el = render.call(this);
      // const parent = this.$el.parentElement;
      // parent.insertBefore(el, this.$el.nextSibling);
      // parent.removeChild(this.$el);
      // this.$el = el;

      // vnode 版本实现
      const vnode = render.call(this, this.$createElement)
      this._update(vnode)
    }
    // 创立一个 Watcher 实例
    new Watcher(this, updateComponent)
  }
  $createElement (tag, props, children) {
    return {
      tag,
      props,
      children
    }
  }
  _update (vnode) {
    const prevVnode = this._vnode
    if (!prevVnode) {this.__patch__(this.$el, vnode)
    } else {this.__patch__(prevVnode, vnode)
    }
  }
  __patch__ (oldVnode, vnode) {
    // oldVnode 是 dom
    if (oldVnode.nodeType) {
      const parent = oldVnode.parentElement
      const refElm = oldVnode.nextSibling
      // props
      // children
      const el = this.createElm(vnode)
      parent.insertBefore(el, refElm)
      parent.removeChild(oldVnode)
    } else {
      // update
      // 获取 dom
      const el = vnode.el = oldVnode.el
      if (oldVnode.tag === vnode.tag) {
        const oldCh = oldVnode.children
        const newCh = vnode.children

        /**
         * 新旧节点 diff
         * 1. 新老节点都是 string(文本更新)* 2. 新老节点都是数组(首尾 diff)* 3. 新节点为数组,老节点为 string(递归创立 dom 树)* 4. 新节点是 string, 老节点是数组(间接将新节点赋值给老节点)*/
        if (typeof newCh === 'string') {if(typeof oldCh === 'string') {
            // 新旧节点都是 string 且值不同 间接更新
            if(newCh !== oldCh) {el.textContent = newCh}
          } else {el.textContent = newCh}

        } else {// 1. 新的是数组,老的为文本(阐明新增了子元素,须要递归创立新的 dom 树)
          if (typeof oldCh === 'string') {
            // 清空文本
            oldCh.innerHTML = ''
            newCh.forEach(vnode => this.createElm(vnode))
          } else {
            // 2. 新老节点都是数组
            this.updateChildren(el, oldCh, newCh)
          }
        }
      }
    }
    this._vnode = vnode
  }
  // 递归创立 dom 树
  createElm (vnode) {const el = document.createElement(vnode.tag)
    // 解决 props
    if (vnode.props) {for (const key in vnode.porps) {el.setAttribute(key, vnode.props[key])
      }
    }
    // 解决 children
    if (vnode.children) {
      // 解决文本
      if (typeof vnode.children === 'string') {el.textContent = vnode.children} else {
        // 子元素
        vnode.children.forEach(vnode => {const child = this.createElm(vnode)
          el.appendChild(child)
        })
      }
    }
    // vnode 中保留 dom
    vnode.el = el
    return el
  }
  // 更新孩子
  updateChildren(parentElm, oldCh, newCh) {const len = Math.min(oldCh.length, newCh.length)
    // 遍历较短的那个子数组
    for (let i = 0; i < len; i++) {this.__patch__(oldCh[i], newCh[i])
    }

    // newCh 若是更长的那个,新增
    if (newCh.length > oldCh.length) {newCh.slice(len).forEach(vnode => {const el = this.createElm(vnode)
        parentElm.appendChild(el)
      })
    } else if(newCh.length < oldCh.length){oldCh.slice(len).forEach(vnode => {parentElm.removeChild(vnode.el)
      })
    }
  }
}


// 负责视图更新,与依赖一一对应
class Watcher {constructor(vm, expOrFn) {
    this.vm = vm;
    this.getter = expOrFn;
     // 触发依赖收集
    this.get()}
  get () {
    Dep.target = this;
    this.getter.call(this.vm)
    Dep.target = null
  }
  // Dep 将来会告诉更新
  update() {this.get()
  }
}
// 依赖:和响应式对象的 key 一一对应
class Dep {constructor() {this.deps = new Set();
  }
  addDep(wather) {this.deps.add(wather)
  }
  notify() {this.deps.forEach(wather => wather.update())
  }
}

总结

以上咱们曾经发明了一个简略版本的 TVue,实现了数据响应和异步批量更新 dom 根底性能。

当然 Vue2.x 的弱小之处远远不止于此,剩下的就留给大家去源码中去找答案吧!

以下是 Vue 中几个重要概念的简略介绍,兴许能够帮忙大家更好的了解这篇文章的总体思路

  1. Observer 是用来给数据增加 Dep 依赖。
  2. Dep 是 data 每个对象包含子对象都领有一个该对象, 当所绑定的数据有变更时, 通过 dep.notify()告诉 Watcher。
  3. Compile 是 HTML 指令解析器,对每个元素节点的指令进行扫描和解析,依据指令模板替换数据,以及绑定相应的更新函数。
  4. Watcher 是连贯 Observer 和 Compile 的桥梁,Compile 解析指令时会创立一个对应的 Watcher 并绑定 update 办法 , 增加到 Dep 对象上。

扩大

Vue 源码的学习小技巧

  1. 获取 Vue 源码

    我的项目地址: https://github.com/vuejs/vue

  2. 调试环境搭建

    • 装置依赖: npm i 装置 phantom.js 时即可终止
    • 装置 rollup: npm i -g rollup 批改 dev 脚本,增加 sourcemap,package.json
    • 运行开发命令: npm run dev 引入后面创立的 vue.js
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web- full-dev"
  1. 术语解释:

    • runtime: 仅蕴含运行时,不蕴含编译器
    • common:cjs 标准,用于 webpack1
    • esm:ES 模块,用于 webpack2+
    • umd: universal module definition,兼容 cjs 和 amd,用于浏
正文完
 0