详解 Vue 2.X 核心源码,手撸一个简易版Vue框架(下篇)

624 阅读4分钟

《 详解 Vue 2.X 核心源码,手撸一个简易版Vue框架(上篇)》记录了Vue响应式系统的实现、虚拟DOM的生成,DOM的定向更新等。本篇文章对源码实现流程的总结

Vue源码实现流程

Vue实例初始化

这个阶段主要是完成数据的挂载($options_data)、Vue选项的初始化 譬如data初始化、computed初始化、watch初始化等。

数据的代理

遍历所有data,将data数据通过Object.defineProperty代理到Vue实例上

数据的劫持

为每个data数据对象注册一个侦听器Observer,并为其声明事件中心Dep实例,遍历data并传入到defineReactive(data[key])。在defineReactive中进行数据劫持,通过getter收集watcher依赖,通过setter通过Dep更新。Dep会通知所有watcher更新。

渲染函数与虚拟DOM

Vue初始化会调用mount,mount,在mount中声明render watcher并对其求值。render watcher求值函数会先调用渲染函数生成 虚拟DOM,将虚拟DOM传入_update方法,进行虚拟DOM的Diff对比 定向更新DOM节点。 当Dep通知render watcher更新时,它会重复这个逻辑。

1.png

全部代码及注释讲解

Vue响应式系统及VDOM部分

class Vue {
  constructor(options) {
    this.$options = options
    this._data = options.data
    this.initData()
    // 8.4 无论是计算属性的初始化还是data的初始化都必须放到watch初始化之前,因为计算属性和data的初始化完成 watch才能侦测到它们。
    this.initComputed()
    this.initWatch()
    // 10.0 使用解析器和代码生成器 生成渲染函数  
    if (this.$options.el) {
      // 10.1 获取模板字符串
      let html = document.querySelector("div").outerHTML
      // 10.2 生成抽象语法树
      let ast = parser(html)
      // 10.3 生成渲染函数函数体
      let funCode = codegen(ast).render
      // 10.4 生成渲染函数并挂载到Vue实例上
      this.$options.render = new Function(funCode)
      // 16.0 调用$mount 更新视图
      this.$mount(this.$options.el)
    }
  }
  $mount(el) {
    // 16.1 将容器根节点挂载到Vue实例上
    this.$el = document.querySelector(el)
    // 16.2 新建render watcher
    this._watcher = new Watcher(this, () => {
      // 16.3 生成虚拟DOM
      const vnode = this.$options.render.call(this)
      // 16.4 调用_update,更新视图
      this._update(vnode)
    }, () => { })
  }
  _update(vnode) {
    //17.0 有上次vnode时
    if (this._vnode) {
      // 17.1 调用patch 并传入上次vnode和此次vnode
      patch(this._vnode, vnode)
    } else {
      // 17.2 第一次挂载Vue实例时 传入真实DOM节点
      patch(this.$el, vnode)
    }
    // 17.3 保存此次vnode
    this._vnode = vnode
  }
  // 11.0 生成元素节点
  _c(tag, attrs, children, text) {
    return new VNode(tag, attrs, children, text)
  }
  // 12.0 生成纯文本节点
  _v(text) {
    return new VNode(null, null, null, text)
  }
  // 13.0 获取变量内容
  _s(val) {
    // 13.1 如果值为空就返回空字符串
    if (val === null || val === undefined) {
      return ''
      // 13.2 如果为对象
    } else if (typeof val === 'object') {
      return JSON.stringify(val)
      // 13.3 如果为数字或字符串
    } else {
      return val
    }
  }
  initData() {
    let data = this._data
    let keys = Object.keys(data)
    // 数据代理
    for (let i = 0; i < keys.length; i++) {
      Object.defineProperty(this, keys[i], {
        enumerable: true,
        configurable: true,
        set: function proxySetter(newVal) {
          data[keys[i]] = newVal
        },
        get: function proxyGetter() {
          return data[keys[i]]
        },
      })
    }
    // 数据劫持
    observe(data)
  }
  initWatch() {
    const watches = this.$options.watch
    // 存在watch选项
    if (watches) {
      const keys = Object.keys(watches)
      for (let index = 0; index < keys.length; index++) {
        new Watcher(this, keys[index], watches[keys[index]])
      }
    }
  }
  // 8.3 对计算属性单独初始化 
  initComputed() {
    const computeds = this.$options.computed
    if (computeds) {
      const keys = Object.keys(computeds)
      for (let index = 0; index < keys.length; index++) {
        // 8.5 第二个参数传入计算属性函数
        // 8.15 计算属性初始化的watcher  需要将其标记为惰性的
        const watcher = new Watcher(this, computeds[keys[index]], function () { }, { lazy: true })
        // 8.6 将该watcher挂载到Vue实例上  
        Object.defineProperty(this, keys[index], {
          enumerable: true,
          configurable: true,
          // 8.7 不允许用户修改计算属性
          set: function computedSetter() {
            console.warn("请不要修改计算属性")
          },
          // 8.8 通过watcher的get方法求值,并将求值结果返回出去
          get: function computedGetter() {
            // 8.9 只有watcher为脏数据时,再重新求值
            if (watcher.dirty) {
              watcher.get()
              // 8.10 求出新值 更新dirty状态  
              watcher.dirty = false
            }
            // 9.12 在计算属性的getter中判断 是否还有watcher需要收集
            if (Dep.target) {
              for (let i = 0; i < watcher.deps.length; i++) {
                // 9.13 将watcher的dep 拿出来继续收集剩余的watcher
                watcher.deps[i].depend()
              }
            }
            return watcher.value
          }
        })
      }
    }
  }
  $watch(key, cb) {
    new Watcher(this, key, cb)
  }
  // 6.6 __ob__的挂载,依赖的收集工作已做完  
  $set(targt, key, value) {
    const oldValue = { ...targt }
    // 6.7 将传入的新属性也变为响应式  
    defineReactive(targt, key, value)
    // 6.8 手动派发依赖更新  
    targt.__ob__.dep.notify(oldValue, targt)
  }
}
// 1、 observe函数:判断数据类型,声明并返回Observer实例
function observe(data) {
  const type = Object.prototype.toString.call(data)
  // 1.1 如果被观测的data为基本数据类型 就返回
  if (type !== '[object Object]' && (type !== '[object Array]')) return
  // 1.2 观测数据涉及一些复杂的逻辑 将这个过程封装为一个Observer类
  // 1.2 new Observer(data)
  // 6.3 将Observer实例 return出去,并在defineReactive中接收。
  if (data.__ob__) return data.__ob__
  return new Observer(data)
}

// 2、Observer类:观察者/侦听器,用来观测数据、生成负责处理依赖的Dep实例等复杂逻辑
class Observer {
  constructor(data) {
    // 6.1 为observer实例挂一个Dep实例(事件中心)
    this.dep = new Dep()
    // 7.5 数组不能调用walk,因为walk会通过defineProperty劫持下标会出现依赖回调错乱等问题
    if (Array.isArray(data)) {
      // 7.6 用我们改造好的数组原型覆盖 自身的原型对象
      data.__proto__ = ArrayMethods
      // 7.7 将数组所有子元素变为响应式 
      this.observeArray(data)
    } else {
      // 2.1 将data所有属性 变为响应式  
      this.walk(data)
    }
    // 6.2 将observer实例挂在到不可枚举的属性__ob__上,供外部$set使用 
    Object.defineProperty(data, "__ob__", {
      value: this,
      enumerable: false,
      configurable: true,
      writable: true
    })
  }
  walk(data) {
    let keys = Object.keys(data)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(data, keys[i], data[keys[i]])
    }
  }
  // 7.8 将传入的数组的所有子元素 变为响应式
  observeArray(arr) {
    for (let i = 0; i < arr.length; i++) {
      observe(arr[i])
    }
  }
}

// 3、defineReactive工具函数:用来递归劫持data,将data数据变为响应式数据
function defineReactive(obj, key, value) {
  // 3.1 递归调用defineReactive来递归劫持深层次data数据   defineReactive--observe--Observer--defineReactive
  // 3.1 observe(obj[key])
  // 6.4 接收Observer实例,为属性Dep收集依赖 Watcher
  let childOb = observe(obj[key])
  // 4.0、为每个data数据新建一个Dep实例,并通过闭包维护
  let dep = new Dep()
  // 3.2 对当前data对象的 key 进行数据劫持
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    set: function reactiveSetter(newVal) {
      if (newVal === value) return
      // 4.4、Dep派发依赖更新  
      dep.notify(newVal, value)
      value = newVal
    },
    get: function reactiveGetter() {
      // 4.5、闭包Dep收集依赖 Watcher
      dep.depend()
      // 6.5 observe函数 如果传入数据为简单数据类型 就不会返回Observer实例 所以需要判断一下是否有Observer实例,如果有就为Observer实例的Dep也收集一份 依赖
      if (childOb) childOb.dep.depend()
      return value
    }
  })
}
// 9.1 新增保存depTarget的栈  
let targetStack = []
// 4、Dep类:事件中心,负责收集依赖、通知依赖更新等  
class Dep {
  constructor(option) {
    // 4.1、subs用来保存所有订阅者
    this.subs = []
  }
  // 9.7 watcher收集完dep后,调用dep.addSub来收集watcher
  addSub(watcher) {
    this.subs.push(watcher)
  }
  // 4.2、depend方法用来收集订阅者依赖
  depend() {
    // 5.5、如果为Watcher实例初始化
    if (Dep.target) {
      // 5.6 每个data数据Watcher实例化,都会先设置Dep.target并触发data数据得getter,完成依赖得收集
      // this.subs.push(Dep.target)
      // 9.6 watcher收集dep
      Dep.target.addDep(this)
    }
  }
  // 4.3、notify方法用来派发订阅者更新
  notify(newVal, value) {
    // 5.7、 执行每个订阅者Watcher的run方法完成 更新
    // 8.12 依赖更新派发更新时 先走update判断是否要更新
    this.subs.forEach(watcher => watcher.update(newVal, value))
  }
}
let watcherId = 0
// watcher任务队列
let watcherQueue = []
// 5、Watcher类:订阅者,触发依赖收集、处理回调
class Watcher {
  constructor(vm, exp, cb, option = {}) {
    // 8.13 watcher增加新参数 option ,对watcher进行默认配置
    this.lazy = this.dirty = !!option.lazy
    // 5.1、将Vue实例、data属性名和处理回调 挂载到watcher实例上
    this.vm = vm
    this.exp = exp
    this.cb = cb
    this.id = ++watcherId
    // 9.8 watcher用来保存收集到的dep
    this.deps = []
    // 8.14 惰性watcher 初始化时不需要收集依赖
    if (!option.lazy) {
      // 5.2、触发data数据的getter 完成依赖收集
      this.get()
    }
  }
  addDep(dep) {
    // 9.9 由于每次9.0求值 watcher可能会收集多次dep 如果已经收集过就终止  
    if (this.deps.indexOf(dep) !== -1) return
    // 9.10 收集dep
    this.deps.push(dep)
    // 9.11 让dep收集watcher
    dep.addSub(this)
  }
  get() {
    // 9.2 在dep收集依赖watcehr时,先添加进栈中
    targetStack.push(this)
    // 5.3、将Watcher实例设为 Dep依赖收集的目标对象
    Dep.target = this
    // 8.1  收集依赖之前先判断是否为函数 计算属性求值时会传入函数  
    if (typeof this.exp === 'function') {
      // 8.2 执行函数 并求出值
      this.value = this.exp.call(this.vm)
    } else {
      // 5.4、触发data数据getter拦截器 对其进行求值
      this.value = this.vm[this.exp]
    }
    // 9.3 求值 收集依赖结束后 让watcher出栈
    targetStack.pop()
    // 9.4 判断栈中 是否有未被收集的watcher
    if (targetStack.length) {
      // 9.5 获取到栈顶的watcher
      Dep.target = targetStack[targetStack.length - 1]
    } else {
      // 清空依赖目标对象
      Dep.target = null
    }
  }
  // 8.11 在调用run之前先调用update,判断是否要直接run
  update(newVal, value) {
    // 8.12 依赖更新当前watcher为惰性时,不要直接run。而是将watcher标记为脏数据,等到用户主动获取结果再去run
    if (this.lazy) {
      this.dirty = true
    } else {
      this.run(newVal, value)
    }
  }
  run(newVal, value) {
    // 5.8 如果该任务已存在与任务队列中 则终止
    if (watcherQueue.indexOf(this.id) !== -1) return
    // 5.9 将当前watcher添加到 队列中
    watcherQueue.push(this.id)
    const index = watcherQueue.length - 1
    Promise.resolve().then(() => {
      // 9.0 依赖更新,对watcher进行求值 解决计算属性watcher 不被触发的问题
      this.get()
      this.cb.call(this.vm, newVal, value)
      // 5.10 任务执行结束 将其从任务队列中删除  
      watcherQueue.splice(index, 1)
    })
  }
}
// 7.0 获取数组原型对象
const ArrayMethods = {}
ArrayMethods.__proto__ = Array.prototype
// 7.1 声明需要被改造的数组方法 这里举两个例子
const methods = ['push', 'pop']
// 7.2 对数组方法进行改造
methods.forEach(method => {
  ArrayMethods[method] = function (...args) {
    const oldValue = [...this]
    // 7.9 将新插入的数据也变为响应式  
    if (method === 'push') {
      this.__ob__.observeArray(args)
    }
    // 7.3 传入参数执行原本方法
    const result = Array.prototype[method].apply(this, args)
    // 7.4 派发依赖更新 
    this.__ob__.dep.notify(oldValue, this)
    return result
  }
})
// 14.0 VNode抽象类实现虚拟DOM节点
class VNode {
  constructor(tag, attrs, children, text) {
    this.tag = tag
    this.attrs = attrs
    this.children = children
    this.text = text
  }
}
// 15.0  生成真实DOM 
function createEle(vnode) {
  // 15.1 为文字节点时
  if (!vnode.tag) {
    const el = document.createTextNode(vnode.text)
    // 15.2 将节点保存起来
    vnode.ele = el
    return el
  }
  // 15.3 为元素节点时 
  const el = document.createElement(vnode.tag)
  vnode.ele = el
  // 15.4 将子节点也转换成真实DOM 并插入到父节点中 
  vnode.children.map(createEle).forEach(e => {
    el.appendChild(e)
  })
  return el
}
// 18.6 判断新旧节点是否发生变化
function changed(oldNode, newNode) {
  return oldNode.tag !== newNode.tag || oldNode.text !== newNode.text
}
function patch(oldNode, newNode) {
  const isRealyElement = oldNode.nodeType
  // 18.0 当oldNode=this.$el  为元素节点  页面第一次挂载时
  if (isRealyElement) {
    let parent = oldNode.parentNode
    // 18.1 将vue容器节点替换为 vdom生成的新节点
    parent.replaceChild(createEle(newNode), oldNode)
    return
  }
  // 18.2 获取当前vdom的真实dom  上次patch 会在newNode上挂载ele 
  let el = oldNode.ele
  // 18.3 新vdom节点存在 将DOM挂载到vdom.ele上,下次patch 会使用ele
  if (newNode) {
    newNode.ele = el
  }
  let parent = el.parentNode
  // 18.4 新vdom节点不存在,就删除掉DOM中对应的节点
  if (!newNode) {
    parent.removeChild(el)
    // 18.5 新旧节点标签类型或文本不一致时
  } else if (changed(oldNode, newNode)) {
    // 18.7 调用createEle生成新DOM节点替换旧DOM节点  
    parent.replaceChild(createEle(newNode), el)
    // 18.8 对比子节点
  } else if (newNode.children) {
    let newLength = newNode.children.length
    let oldLength = oldNode.children.length
    // 18.9 遍历新旧vdom节点的所有子节点
    for (let index = 0; index < newLength || index < oldLength; index++) {
      // 18.10 子节点旧vdom不存在,调用createEle生成DOM插入到父节点el中
      if (index > oldLength) {
        el.appendChild(createEle(newNode.children[index]))
      } else {
        // 18.11 其余情况的子节点对比 通过调用 patch实现  
        patch(oldNode.children[index], newNode.children[index])
      }
    }
  }
}

解析器部分

// 对HTML模板字符串进行解析 最终得到元素树抽象语法树(ElementASTs)
/**
* {
*    children: [{…}],
*    parent: {},
*    tag: "div",
*    type: 1, //1-元素节点 2-带变量的文本节点 3-纯文本节点,
*    expression:'_s(name)', //type如果是2,则返回_s(变量)
*    text:'{{name}}' //文本节点编译前的字符串
*  }
*/
function parser(html) {
  // 层级栈:记录当前元素的层级
  let stack = []
  // 根元素节点
  let root = null
  // 当前元素的父元素节点  
  let currentParent = null
  // 1.0 不断对模板字符串解析
  while (html) {
    let index = html.indexOf("<")
    // 2.1 如果元素之前有文本节点   例: html = "{{name}}<div>1</div></root>"
    if (index > 0) {
      // 2.2 截取标签前文字部分
      let text = html.slice(0, index)
      // 5.4 调用parseText工具函数解析文本
      let element = parseText(text)
      // 5.5 文本节点增加 父节点属性
      element.parent = currentParent
      // 2.3 将文字节点推进父元素的children中
      currentParent.children.push(element)
      // 2.4 截掉已经处理完的部分
      html = html.slice(index)
      // 1.0 如果为开始标签  例: html = "<root>{{name}}<div>1</div></root>"
    } else if (html[index + 1] !== '/') {
      // 1.1 获取元素类型
      let gtIndex = html.indexOf(">")
      let eleType = html.slice(index + 1, gtIndex).trim()
      // 1.2 如果标签内存在属性  截掉标签属性部分  例: eleType = 'div id="app"' 处理后:eleType = 'div'
      let emptyIndex = eleType.indexOf(" ")
      let attrs = {}
      if (emptyIndex !== -1) {
        // 1.3 获取元素标签属性
        attrs = parseAttr(eleType.slice(emptyIndex + 1))
        eleType = eleType.slice(0, emptyIndex)
      }
      // 1.4 新建AST节点  
      const element = {
        children: [],
        attrs,
        parent: currentParent,
        tag: eleType,
        type: 1
      }
      // 1.5 没有根元素节点
      if (!root) {
        root = element
      } else {
        // 1.6 将当前元素节点推进父元素的children中 
        currentParent.children.push(element)
      }
      // 1.7 解析到元素开始标签 推元素进层级栈
      stack.push(element)
      // 1.8 更新当前父级元素
      currentParent = element
      // 1.9 截掉已经处理完的部分
      html = html.slice(gtIndex + 1)
      // 3.0 为结束标签  例: html = "</div></root>"
    } else {
      let gtIndex = html.indexOf(">")
      // 3.1 解析到元素的结束标签 层级栈退一个
      stack.pop()
      // 3.2 更新当前父级元素
      currentParent = stack[stack.length - 1]
      // 3.3 截掉已经处理完的部分
      html = html.slice(gtIndex + 1)
    }
  }
  return root
}
//解析文本节点
function parseText(text) {
  // 未解析的文本
  let originText = text
  // 有可能是纯文本或者带变量的文本  默认:纯文本
  let type = 3
  // 节点碎片 元素节点的文本节点可能是多段组成的 
  // 例:<p>我的 {{name}},我的 {{age}}</p>   token=['我的',{{name}},',我的',{{age}}]
  let token = []
  while (text) {
    let start = text.indexOf("{{")
    let end = text.indexOf("}}")
    //4.0 如果存在插值表达式
    if (start !== -1 && end !== -1) {
      // 4.1 将文本节点类型标记为 带变量的文本
      type = 2
      // 4.2 插值表达式前存在纯文本
      if (start > 0) {
        // 4.3 将插值表达式 前纯文本 推进token
        token.push(JSON.stringify(text.slice(0, start)))
      }
      // 4.4 获取插值表达式内的 表达式  
      let exp = text.slice(start + 2, end)
      // 4.5 解析表达式 并推进token  
      token.push(`_s(${exp})`)
      // 4.6 截掉已经处理完的部分
      text = text.slice(end + 2)
      // 5.0 不存在插值表达式
    } else {
      // 5.1 终止解析text 直接推进token
      token.push(JSON.stringify(text))
      text = ''
    }
  }
  let element = {
    text: originText,
    type
  }
  // 5.3 如果type为2带有变量  文本节点需要expression
  if (type === 2) {
    element.expression = token.join("+")
  }
  return element
}
// 解析标签属性  
function parseAttr(eleAttrs) {
  let attrs = {}
  attrString = eleAttrs.split(" ")
  attrString.forEach(e => {
    if (e && e.indexOf("=") !== -1) {
      const attrsArr = e.split("=")
      attrs[attrsArr[0]] = attrsArr[1]
    } else {
      attrs[e] = true
    }
  });
  return attrs
}

代码生成器部分

// 将AST转换为渲染函数函数体
/**{
    children: [{ … }],
    parent: { },
    tag: "div",
    type: 1, //1-元素节点 2-带变量的文本节点 3-纯文本节点,
    expression: '_s(name)', //type如果是2,则返回_s(变量)
    text: '{{name}}' //文本节点编译前的字符串
} */
function codegen(ast) {
  // 1.0 ast第一层一定是个元素节点
  let code = genElement(ast)
  return {
    // 1.1 渲染函数执行时,传入this改变函数体内this指向。
    render: `with(this){return ${code}}`
  }
}
// 转换元素节点
function genElement(el) {
  // 2.1 获取子节点 
  let children = genChildren(el)
  // 2.0 返回_c(标签名,标签属性对象,标签子节点数组),将标签名变为JSON字符串 
  return `_c(${JSON.stringify(el.tag)}, ${JSON.stringify(el.attrs)}, ${children})`
}
// 转换文本节点
function genText(node) {
  // 5.0 带有变量的文本节点
  if (node.type === 2) {
    // node.expression 任何变量都会通过this.[node.expression] 进行求值 !!!!
    return `_v(${node.expression})`
  }
  // 5.1 纯文本节点 要变成JSON字符串 要不然会被当成变量处理
  return `_v(${JSON.stringify(node.text)})`
}
// 判断类型 转移对应节点 
function genNode(node) {
  // 4.0 判断节点类型
  if (node.type === 1) {
    return genElement(node)
  } else {
    return genText(node)
  }
}
// 转换子节点
function genChildren(node) {
  // 3.0 判断是否存在子节点
  if (node.children && node.children.length > 0) {
    // 3.1 转换所有子节点 [ 子节点1,子节点2,...],递归转换所有子节点  genNode--genElement--genChildren--genNode
    return `[${node.children.map(node => genNode(node))}]`
  }
}