手写简版Vue

在这里插入图片描述
学习了Vue之后,根据自己的理解,手写了简版Vue,希望对正在学习Vue源码的初学者有所帮助,一起加油!

语雀访问地址:手写Vue

HTML

<!-- 声明meta,不然会乱码 -->
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">

<!-- HTML -->
<div id="app">
  <p>{{counter}}</p>
  <p>{{counter}}</p>
  <p>{{name}}</p>
  <p j-text='name'></p>
  <p j-html='hobby'></p>
  <button @click='clickFn'>点我</button>
  <!-- <button j-show='show' @click='clickFn'>点我</button> -->
</div>

<!-- 引入JVue -->
<script src="./J-Vue.js"></script>

<!-- 逻辑 -->
<script>
  const app = new JVue({
    el: '#app',
    data: {
      show: false,
      name: '张三',
      hobby: "我喜欢<span style='color: red'>听歌</span>",
      counter: 1
    },
    methods: {
      clickFn () {
        console.log(this.counter)
        this.show = !this.show
        this.counter++
      }
    }
  })
  // setInterval(() => {
  //   app.counter++
  // }, 1000)
</script>

简版JVue

key、Dep、watcher之间的关系

  1. data中一个key对应一个Dep实例(管家)
  2. 界面中出现一次key绑定,就会产生一个watcher
  3. Dep(管家)负责管理同一个key产生的一个或多个watcher

实现步骤

  1. defineReactive时为data中每⼀个key创建⼀个Dep实例(管家)
  2. 初始化视图时,读取某个key,例如name1,创建⼀个watcher1 => Compile里创建
  3. 由于触发name1的getter⽅法,便将watcher1添加到name1对应的Dep(管家)中,即创建Dep和watcher之间订阅(管理)关系
  4. 当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新

自我理解

  1. 初始化的时候,页面中首次渲染Compile,调用更新方法update(),同时为每一个读取的key创建(new)一个watcher,并将当前data、key、更新方法update(newVal)作为参数传递给这个watcher,将来key的值发生改变后,不会再走Compile里的update()了,而是直接触发watcher中的update()
  2. 此时我们创建一个Watcher类,作用:
    • 获得data、key、更新函数
    • 刻意读取一次当前key,要立flag做判断,即:Dep.target = this,保证是在刻意读取key的情况下,才收集wathcer依赖
    • 详解:new Watcher的同时,刻意读取一次该watch对应的key,触发defineProperty中的get方法,在get方法中触发管家(Dep)的addDep方法,将此watcher添加到管家的deps中,为了判断是watcher刻意读取的,我们需要定义一个全局变量,即Dep.target = this,同时这个this也是需要添加到deps中的watcher,如果Dep.target存在,我们才调用addDep方法,key读取完成后,我们要把Dep.target设置为null,防止之后再获取key时,也会触发addDep的方法
  3. 同时,在初始化的时候,在defineReactive中为每一个key创建(new)一个“一对一的专属管家“——Dep
  4. 此时我们创建一个Dep类,作用:
    • 收集依赖(watcher)——deps数组添加watcher
    • 通知watcher更新——遍历触发watcher中的update
  5. 这样,每一个管家Dep就收集到了所有使用同一个key的watcher
  6. 将来,data中的key发生变化时,触发defineProperty的set方法,在set中让管家(Dep)触发通知notify方法,遍历一个个watcher,调用watcher中的update(),至此就完成了视图更新。
// 可按序号阅读
class JVue {
  constructor(options) {
    // 1、保存配置项
    this.$options = options
    // 2、保存data
    this.$data = options.data
    // 3、保存方法
    this.$methods = options.methods
    // 14、获取DOM
    this.$el = document.querySelector(options.el)

    // 4、给data中的数据添加响应式
    observe(this.$data)
    // 12、添加代理,可以直接通过this + key获取
    proxy(this)

    // 15、调用编译
    if (this.$el) {
      new Complie(this, this.$el)
    }
  }
}

// 5、observe方法
function observe(data) {
  // 🔥判断是数组或者对象
  if (typeof data !== 'object' || data === null) return
  new Observe(data)
}

// 6、Observe类
class Observe {
  constructor(data) {
    if (Array.isArray(data)) {
      // 数组
      this.walkArray(data)
    } else {
      // 对象
      this.walk(data)
    }
  }

  // 7、给对象添加响应式
  walk(obj) {
    // 对对象进行遍历,给对象中的每一项key添加响应式
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }

  // 46、给数组添加响应式
  walkArray(array) {
    array.__proto__ = defineArray()
    // 48、对数组内部元素添加响应式
    for (let i = 0; i < array.length; i++) {
      observe(array[i])
    }
  }
}

// 47、给数组添加响应式
// (1) 找到数组原型
// (2) 覆盖那些能够修改数组的更新方法,使其可以通知更新
// (3) 将得到的新原型设置到数组的实际原型上
function defineArray() {
  // 找到数组原型
  const originProto = Array.prototype
  // 拷贝一份原型
  const arrayProto = Object.create(originProto)
  // 数组的方法
  const methods = ['push', 'pop', 'shift', 'unshift', 'reserve']
  console.log(originProto)
  methods.forEach(method => {
    arrayProto[method] = function () {
      // 原始操作
      originProto[method].apply(this, arguments)

      // 覆盖操作:通知更新
      console.log('调用了数组的' + method + '方法')
    }
  })
  return arrayProto
}

// 8、创建defineReactive方法
// (1) 注意对象嵌套的响应式
// (2) 注意直接将val赋值为对象的响应式
// (3) 注意新添加key的响应式
function defineReactive(obj, key, val) {

  // 9、递归,解决对象嵌套的问题
  observe(obj[key])

  // 38、初始化的时候为每一个key添加管家Dep
  const dep = new Dep()

  Object.defineProperty(
    obj,
    key,
    {
      get() {
        console.log('get:', key)

        // 43、将watcher添加到deps,依赖收集
        // 🔥要判断Dep.target存在才添加watcher,否则每获取一次key的值,都会触发addDep,此时target为null
        Dep.target && dep.addDep(Dep.target)
        return val
      },
      set(newVal) {
        console.log('set:', key)
        if (newVal !== val) {
          val = newVal
          // 10、解决直接将val赋值为对象
          observe(val)

          // 为了测试用
          // watchers.forEach(w => w.update(newVal))

          // 44、key的值发生变化,通知更新
          dep.notify(newVal)
        }
      }
    }
  )
}

// 11、set方法,用于对新添加的key添加响应式
function set(obj, key, val) {
  defineReactive(obj, key, val)
}

// 13、添加代理,可以直接通过this + key获取
function proxy(vm) {
  // 对data遍历,对每一个key代理
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(
      vm,
      key,
      {
        get() {
          // 获取data中的key,其实是获取的$data中的key
          return vm.$data[key]
        },
        set(newVal) {
          // 设置data中的key,其实是设置的$data中的key
          vm.$data[key] = newVal
        }
      }
    )
  })
}

// 16、Complie类
class Complie {
  constructor(vm, el) {
    // 17、保存vm和el,后面会使用到
    this.$vm = vm
    this.$el = el

    // 18、编译方法
    this.complie(el)
  }

  // 19、编译方法
  complie(el) {
    // 获取DOM节点
    const childNodes = el.childNodes
    // 循环节点,获得元素类型和文本类型
    childNodes.forEach(node => {
      // nodeType:1元素、2属性、3文本
      if (node.nodeType === 1) {
        // 元素
        // 26、对元素的属性进行遍历
        const attributes = node.attributes
        // attributes是一个类数组,要把它转化成数组
        Array.from(attributes).forEach(attr => {
          const attrName = attr.name
          const attrVal = attr.value
          // 27、判断是不是自定义指令
          if (this.isDirective(attrName)) {
            const dir = attrName.slice(2)
            this[dir] && this[dir](node, attrVal, dir)
          }

          // 29、判断是不是事件
          if (this.isEvent(attrName)) {
            // 事件类型
            const eventType = attrName.slice(1)
            this.eventHanlder(node, eventType, attrVal)
          }
        })
      } else if (this.isInter(node)) {
        // 21、文本:只判断是包含插值表达式的文本
        // 23、触发update(),RegExp.$1会保存最近一个正则匹配的分组
        this.text(node, RegExp.$1)
        console.log(node, RegExp.$1)
      }

      // 20、递归,遍历到DOM每一个节点
      if (node.childNodes && node.childNodes.length) {
        this.complie(node)
      }
    })
  }

  // 22、是否是包含插值表达式的文本
  isInter(node) {
    const interRule = /\{\{(.*)\}\}/
    return node.nodeType === 3 && interRule.test(node.textContent)
  }

  // 24、更新方法
  update(node, exp, dir) {
    // 拼接textUpdater、htmlUpdate方法
    const fn = this[dir + 'Updater']
    // (1) 只会初始化执行
    fn && fn(node, this.$vm[exp])

    // 32、为每个使用key创建watcher
    new Watcher(this.$vm, exp, (newVal) => {
      // (2) 除开初始化,之后data数据每一次更新执行
      fn && fn(node, newVal)
    })
  }

  // 27、判断是不是自定义指令
  isDirective(attrName) {
    return attrName.startsWith('j-')
  }

  // 25、text()、textUpdater(),更新文本节点
  text(node, exp) {
    this.update(node, exp, 'text')
  }
  textUpdater(node, value) {
    node.textContent = value
  }

  // 28、html()、htmlUpdater(),更新HTML
  html(node, exp) {
    this.update(node, exp, 'html')
  }
  htmlUpdater(node, value) {
    node.innerHTML = value
  }
  
  // 45、model()、modelUpdater(),更新value
  //j-model本质是语法糖:value设定和事件监听
  model(node, exp) {
    this.update(node, exp, 'model')
    // 监听input事件
    node.addEventListener('input', e => {
      this.$vm[exp] = e.target.value
    })
  }
  modelUpdater(node, value) {
    // 回显value
    node.value = value
  }

  // 30、判断是不是事件
  isEvent(attrName) {
    return attrName.startsWith('@')
  }

  // 31、绑定事件
  eventHanlder(node, eventType, fnName) {
    // 主要修改this指向
    node.addEventListener(eventType, this.$vm.$methods[fnName].bind(this.$vm))
  }
}

// 为了测试用
// const watchers = []

// 33、创建Watcher类
class Watcher {
  constructor(vm, key, updateFn) {
    // 34、JVue实例
    this.$vm = vm
    // 35、依赖key
    this.key = key
    // 36、更新函数
    this.updateFn = updateFn

    // 为了测试用
    // watchers.push(this)

    // 42、刻意读取一次key,创建watcher时触发getter
    // 立一个flag,保证是刻意读取的情况下收集的依赖watcher
    Dep.target = this // 此处的this就是watcher
    this.$vm[this.key] // 刻意读取
    Dep.target = null // 要清空,不然下次再读取key也会变成刻意读取
  }

  // 37、触发更新的方法
  update() {
    // 将key最新的值传过去
    // 🔥注意this指向和key最新的值传递方式
    this.updateFn.call(this.vm, this.$vm[this.key])
  }
}

// 38、创建一个Dep类
class Dep {
  constructor() {
    // 39、创建数组,装同一key的watcher
    this.deps = []
  }

  // 40、添加watcher
  addDep(dep) {
    this.deps.push(dep)
  }

  // 41、通知更新
  notify() {
    // 循环遍历deps,触发watcher的更新
    this.deps.forEach(dep => dep.update())
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值