关于javascript:从零到一手写迷你版Vue

52次阅读

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

Vue 响应式设计思路

Vue 响应式次要蕴含:

  • 数据响应式
  • 监听数据变动,并在视图中更新
  • Vue2 应用 Object.defineProperty 实现数据劫持
  • Vu3 应用 Proxy 实现数据劫持
  • 模板引擎
  • 提供形容视图的模板语法
  • 插值表达式{{}}
  • 指令 v-bind, v-on, v-model, v-for,v-if
  • 渲染
  • 将模板转换为 html
  • 解析模板,生成 vdom,把vdom 渲染为一般 dom

数据响应式原理

数据变动时能自动更新视图,就是数据响应式
Vue2 应用Object.defineProperty 实现数据变动的检测

原理解析

  • new Vue()⾸先执⾏初始化,对 data 执⾏响应化解决,这个过程发⽣在 Observer
  • 同时对模板执⾏编译,找到其中动静绑定的数据,从 data 中获取并初始化视图,这个过程发⽣在 Compile
  • 同时定义⼀个更新函数和Watcher 实例,未来对应数据变动时,Watcher 会调⽤更新函数
  • 因为 data 的某个 key 在⼀个视图中可能呈现屡次,所以每个 key 都须要⼀个管家 Dep 来治理多个 Watcher
  • 未来 data 中数据⼀旦发⽣变动,会⾸先找到对应的 Dep,告诉所有Watcher 执⾏更新函数

一些要害类阐明

CVue:自定义 Vue 类 Observer:执⾏数据响应化(分辨数据是对象还是数组)Compile:编译模板,初始化视图,收集依赖(更新函数、watcher 创立)Watcher:执⾏更新函数(更新 dom)Dep:治理多个 Watcher 实例,批量更新

波及要害办法阐明

observe: 遍历 vm.data 的所有属性,对其所有属性做响应式,会做繁难判断,创立 Observer 实例 进行真正响应式解决

html 页面

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>cvue</title>
  <script src="./cvue.js"></script>
</head>
<body>
  <div id="app">
    <p>{{count}}</p>
  </div>

  <script>
    const app = new CVue({el: '#app',      data: {        count: 0}    })    setInterval(() => {      app.count +=1}, 1000);  </script>
</body>
</html>

CVue

  • 创立根本 CVue 构造函数:
  • 执⾏初始化,对 data 执⾏响应化解决
// 自定义 Vue 类
class CVue {constructor(options) {
    this.$options = options
    this.$data = options.data

    // 响应化解决
    observe(this.$data)
  }
}

// 数据响应式, 批改对象的 getter,setter
function defineReactive(obj, key, val) {
  // 递归解决,解决 val 是嵌套对象状况
  observe(val)
  Object.defineProperty(obj, key, {get() {return val},
    set(newVal) {if(val !== newVal) {console.log(`set ${key}:${newVal}, old is ${val}`)

        val = newVal
        // 持续进行响应式解决,解决 newVal 是对象状况
        observe(val)
      }
    }
  })
}

// 遍历 obj,对其所有属性做响应式
function observe(obj) {
  // 只解决对象类型的
  if(typeof obj !== 'object' || obj == null) {return}
  // 实例化 Observe 实例
  new Observe(obj)
}

// 依据传入 value 的类型做相应的响应式解决
class Observe {constructor(obj) {if(Array.isArray(obj)) {// TODO} else {
      // 对象
      this.walk(obj)
    }
  }
  walk(obj) {
    // 遍历 obj 所有属性,调用 defineReactive 进行响应化
    Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
  }
}

为 vm.$data 做代理

不便实例上设置和获取数据

例如

本来应该是

vm.$data.count
vm.$data.count = 233

代理之后后,能够应用如下形式

vm.count
vm.count = 233

给 vm.$data 做代理

class CVue {constructor(options) {
    // 省略
    // 响应化解决
    observe(this.$data)

    // 代理 data 上属性到实例上
    proxy(this)
  }
}

// 把 CVue 实例上 data 对象的属性到代理到实例上
function proxy(vm) {Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {get() {
        // 实现 vm.count 取值
        return vm.$data[key]
      },
      set(newVal) {
        // 实现 vm.count = 123 赋值
        vm.$data[key] = newVal
      }
    })
  })
}

参考 前端手写面试题具体解答

编译

初始化视图

依据节点类型进行编译
class CVue {constructor(options) {
    // 省略。。// 2 代理 data 上属性到实例上
    proxy(this)

    // 3 编译
    new Compile(this, this.$options.el)
  }
}

// 编译模板中 vue 语法,初始化视图,更新视图
class Compile {constructor(vm, el) {
    this.$vm = vm
    this.$el = document.querySelector(el)

    if(this.$el) {this.complie(this.$el)
    }
  }
  // 编译
  complie(el) {
    // 取出所有子节点
    const childNodes = el.childNodes
    // 遍历节点,进行初始化视图
    Array.from(childNodes).forEach(node => {if(this.isElement(node)) {
        // TODO
        console.log(` 编译元素 ${node.nodeName}`)
      } else if(this.isInterpolation(node)) {console.log(` 编译插值文本 ${node.nodeName}`)
      }
      // 递归编译,解决嵌套状况
      if(node.childNodes) {this.complie(node)
      }
    })
  }
  // 是元素节点
  isElement(node) {return node.nodeType === 1}
  // 是插值表达式
  isInterpolation(node) {
    return node.nodeType === 3
      && /\{\{(.*)\}\}/.test(node.textContent)
  }
}
编译插值表达式
// 编译模板中 vue 语法,初始化视图,更新视图
class Compile {complie(el) {Array.from(childNodes).forEach(node => {if(this.isElement(node)) {console.log(` 编译元素 ${node.nodeName}`)
      } else if(this.isInterpolation(node)) {// console.log(` 编译插值文本 ${node.textContent}`)
        this.complieText(node)
      }
      // 省略
    })
  }
  // 是插值表达式
  isInterpolation(node) {
    return node.nodeType === 3
      && /\{\{(.*)\}\}/.test(node.textContent)
  }
  // 编译插值
  complieText(node) {// RegExp.$1 是 isInterpolation()中 /\{\{(.*)\}\}/ 匹配进去的组内容
    // 相等于 {{count}} 中的 count
    const exp = String(RegExp.$1).trim()
    node.textContent = this.$vm[exp]
  }
}
编译元素节点和指令

须要取出指令和指令绑定值
应用数据更新视图

// 编译模板中 vue 语法,初始化视图,更新视图
class Compile {complie(el) {Array.from(childNodes).forEach(node => {if(this.isElement(node)) {console.log(` 编译元素 ${node.nodeName}`)
        this.complieElement(node)
      }
      // 省略
    })
  }
  // 是元素节点
  isElement(node) {return node.nodeType === 1}
  // 编译元素
  complieElement(node) {
    // 取出元素上属性
    const attrs = node.attributes
    Array.from(attrs).forEach(attr => {
      // c-text="count" 中 c -text 是 attr.name,count 是 attr.value
      const {name: attrName, value: exp} = attr
      if(this.isDirective(attrName)) {
        // 取出指令
        const dir = attrName.substring(2)
        this[dir] && this[dir](node, exp)
      }
    })
  }
  // 是指令
  isDirective(attrName) {return attrName.startsWith('c-')
  }
  // 解决 c -text 文本指令 
  text(node, exp) {node.textContent = this.$vm[exp]
  }
  // 解决 c -html 指令
  html(node, exp) {node.innerHTML = this.$vm[exp]
  }
}

以上实现首次渲染,然而数据变动后,不会触发页面更新

依赖收集

视图中会⽤到 data 中某 key,这称为 依赖
同⼀个 key 可能呈现屡次,每次呈现都须要收集 (⽤⼀个 Watcher 来保护保护他们的关系),此过程称为依赖收集。
多个 Watcher 须要⼀个 Dep 来治理,须要更新时由 Dep 统⼀告诉。

  • data 中的 key 和 dep 是一对一关系
  • 视图中 key 呈现和 Watcher 关系,key 呈现一次就对应一个 Watcher
  • dep 和 Watcher 是一对多关系

实现思路

  • defineReactive 中为每个 key 定义一个Dep 实例
  • 编译阶段,初始化视图时读取 key, 会创立Watcher 实例
  • 因为读取过程中会触发 key 的 getter 办法,便能够把 Watcher 实例 存储到 key 对应的 Dep 实例
  • 当 key 更新时,触发 setter 办法,取出对应的 Dep 实例Dep 实例 调用 notiy 办法告诉所有 Watcher 更新
定义 Watcher 类

监听器,数据变动更新对应节点视图

// 创立 Watcher 监听器,负责更新视图
class Watcher {// vm vue 实例,依赖 key,updateFn 更新函数(编译阶段传递进来)
  constructor(vm, key, updateFn) {
    this.$vm = vm
    this.$key = key
    this.$updateFn = updateFn
  }
  update() {
    // 调用更新函数,获取最新值传递进去
    this.$updateFn.call(this.$vm, this.$vm[this.$key])
  }
}
批改 Compile 类中的更新函数,创立 Watcher 实例
class Complie {
  // 省略。。。// 编译插值
  complieText(node) {// RegExp.$1 是 isInterpolation()中 /\{\{(.*)\}\}/ 匹配进去的组内容
    // 相等于 {{count}} 中的 count
    const exp = String(RegExp.$1).trim()
    // node.textContent = this.$vm[exp]
    this.update(node, exp, 'text')
  }
  // 解决 c -text 文本指令 
  text(node, exp) {// node.textContent = this.$vm[exp]
    this.update(node, exp, 'text')
  }
  // 解决 c -html 指令
  html(node, exp) {// node.innerHTML = this.$vm[exp]
    this.update(node, exp, 'html')
  }
  // 更新函数
  update(node, exp, dir) {const fn = this[`${dir}Updater`]
    fn && fn(node, this.$vm[exp])

    // 创立监听器
    new Watcher(this.$vm, exp, function(newVal) {fn && fn(node, newVal)
    })
  }
  // 文本更新器
  textUpdater(node, value) {node.textContent = value}
  // html 更新器
  htmlUpdater(node, value) {node.innerHTML = value}
}
定义 Dep 类
  • data 的一个属性对应一个 Dep 实例
  • 治理多个 Watcher 实例,告诉所有 Watcher 实例更新
// 创立订阅器,每个 Dep 实例对应 data 中的一个属性
class Dep {constructor() {this.deps = []
  }
  // 增加 Watcher 实例
  addDep(dep) {this.deps.push(dep)
  }
  notify() {
    // 告诉所有 Wather 更新视图
    this.deps.forEach(dep => dep.update())
  }
}
创立 Watcher 时触发 getter
class Watcher {// vm vue 实例,依赖 key,updateFn 更新函数(编译阶段传递进来)
  constructor(vm, key, updateFn) {
    // 省略
    // 把 Wather 实例长期挂载在 Dep.target 上
    Dep.target = this
    // 获取一次属性,触发 getter, 从 Dep.target 上获取 Wather 实例寄存到 Dep 实例中
    this.$vm[key]
    // 增加后,重置 Dep.target
    Dep.target = null
  }
}
defineReactive 中作依赖收集,创立 Dep 实例
function defineReactive(obj, key, val) {
  // 递归解决,解决 val 是嵌套对象状况
  observe(val)

  const dep = new Dep()
  Object.defineProperty(obj, key, {get() {Dep.target && dep.addDep(Dep.target)
      return val
    },
    set(newVal) {if(val !== newVal) {
        val = newVal
        // 持续进行响应式解决,解决 newVal 是对象状况
        observe(val)
        // 更新视图
        dep.notify()}
    }
  })
}

监听事件指令@xxx

  • 在创立 vue 实例时,须要缓存 methods 到 vue 实例上
  • 编译阶段取出 methods 挂载到 Compile 实例上
  • 编译元素时
  • 辨认出 v-on 指令时,进行事件的绑定
  • 辨认出 @ 属性时,进行事件绑定
  • 事件绑定:通过指令或者属性获取对应的函数,给元素新增事件监听,应用 bind 批改监听函数的 this 指向为组件实例
// 自定义 Vue 类
class CVue {constructor(options) {this.$methods = options.methods}
}

// 编译模板中 vue 语法,初始化视图,更新视图
class Compile {constructor(vm, el) {
    this.$vm = vm
    this.$el = document.querySelector(el)
    this.$methods = vm.$methods
  }

  // 编译元素
  complieElement(node) {
    // 取出元素上属性
    const attrs = node.attributes
    Array.from(attrs).forEach(attr => {
      // c-text="count" 中 c -text 是 attr.name,count 是 attr.value
      const {name: attrName, value: exp} = attr
      if(this.isDirective(attrName)) {
        // 省略。。。if(this.isEventListener(attrName)) {// v-on:click, subStr(5)即可截取到 click
          const eventType = attrName.substring(5)
          this.bindEvent(eventType, node, exp)
        }
      } else if(this.isEventListener(attrName)) {// @click, subStr(1)即可截取到 click
        const eventType = attrName.substring(1)
        this.bindEvent(eventType, node, exp)
      }
    })
  }
  // 是事件监听
  isEventListener(attrName) {return attrName.startsWith('@') || attrName.startsWith('c-on')
  }
  // 绑定事件
  bindEvent(eventType, node, exp) {
    // 取出表达式对应函数
    const method = this.$methods[exp]
    // 减少监听并批改 this 指向以后组件实例
    node.addEventListener(eventType, method.bind(this.$vm))
  }
}

v-model 双向绑定

实现 v-model 绑定 input 元素时的双向绑定性能

// 编译模板中 vue 语法,初始化视图,更新视图
class Compile {
  // 省略...
  // 解决 c -model 指令
  model(node, exp) {
    // 渲染视图
    this.update(node, exp, 'model')
    // 监听 input 变动
    node.addEventListener('input', (e) => {const { value} = e.target
      // 更新数据,相当于 this.username = 'mio'
      this.$vm[exp] = value
    })
  }
  // model 更新器
  modelUpdater(node, value) {node.value = value}
}

数组响应式

  • 获取数组原型
  • 数组原型创建对象作为数组拦截器
  • 重写数组的 7 个办法
// 数组响应式
// 获取数组原型, 前面批改 7 个办法
const originProto  = Array.prototype
// 创建对象做备份,批改响应式都是在备份的上进行,不影响原始数组办法
const arrayProto = Object.create(originProto)
// 拦挡数组办法,在变更时发出通知
;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
  // 在备份的原型上做批改
  arrayProto[method] = function() {
    // 调用原始操作
    originProto[method].apply(this, arguments)
    // 收回变更告诉
    console.log(`method:${method} value:${Array.from(arguments)}`)
  }
})

class Observe {constructor(obj) {if(Array.isArray(obj)) {
      // 批改数组原型为自定义的
      obj.__proto__ = arrayProto
      this.observeArray(obj)
    } else {
      // 对象
      this.walk(obj)
    }
  }
  observeArray(items) {
    // 如果数组外部元素时对象,持续做响应化解决
    items.forEach(item => observe(item))
  }
}

正文完
 0