前言

在上一篇文章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,用于浏