写点什么

从零到一手写迷你版 Vue

  • 2022-12-12
    浙江
  • 本文字数:6232 字

    阅读完需:约 20 分钟

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,setterfunction 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.countvm.$data.count = 233
复制代码


代理之后后,可以使用如下方式


vm.countvm.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)) }}
复制代码


用户头像

还未添加个人签名 2022-07-31 加入

还未添加个人简介

评论

发布
暂无评论
从零到一手写迷你版Vue_JavaScript_helloworld1024fd_InfoQ写作社区