四、vue源码(简化版) ---kkb

知识要点

  • vue工作机制
  • vue响应式原理
  • 依赖收集与追踪
  • 编译compile

Vue工作机制

在new Vue() 之后。Vue会调用进行初始化,会初始化生命周期、事件、props、methods、data、computed 与 watch 等。其中最重要的是通过 Object.defineProperty 设置 setter 与 getter,用来实现 响应式 以及 依赖收集

初始化之后调用 $mount 挂载组件

简化版:

编译

编译模块分为三个阶段

1、parse:使用正则解析template中的vue指令(v-xxx)变量等等,形成抽象语法树AST

2、optimize:标记一些静态节点,用作后面的性能优化,在diff的时候直接略过

3、generate:把第一步生成的AST转化为渲染函数 render function

响应式

这一块是vue最核心的内容,初始化的时候通过defineProperty定义对象getter、setter,设置通知机制

当编译生成的渲染函数被实际渲染的时候,会触发getter进行依赖收集,在数据变化的时候,触发setter进行更新

虚拟dom

Virtual DOM 是react首创,Vue2开始支持,就是用JavaScript对象来描述dom结构,数据修改的时候,我们先修改虚拟dom中的数据,然后数组做diff,最后再汇总所有的diff,力求做最少的dom操作,毕竟js里对比很快,而真实的dom操作太慢

更新视图

数据修改触发setter,然后监听器会通知进行修改,通过对比新旧vdom树,得到最小修改,就是patch,然后只需要把这些差异修改即可

Vue2响应式原理:Object.defineProperty

实现 KVue:

// 定义KVue的构造函数
class KVue {
  constructor(options) {
    // 保存选项
    this.$options = options

    // 传入data
    this.$data = options.data

    // 响应化处理
    this.observe(this.$data)

    // 测试代码
    // new Watcher(this, 'foo')
    // this.foo
    // new Watcher(this, 'foo.bar.mua')
    // this.bar.mua

    // 测试编译器
    new Compile(options.el, this)

    if(options.created) {
      options.created.call(this)
    }
  }

  observe(value) {
    // 如果value不存在,或者value不是对象,就直接return
    // 这里不对数组进行处理
    if (!value || Object.prototype.toString.call(value) !== '[object Object]') {
      return
    }
    // 遍历value
    Object.keys(value).forEach(key => {
      // 响应式处理
      this.defineReactive(value, key, value[key])
      // 代理data中的属性到vue根上
      this.proxyData(key)
    })
  }

  defineReactive(obj, key, val) { // 这里实质上就是一个闭包,内部的get和set一直使用的是这里的val
    this.observe(val) // 如果是对象的话,进一步递归

    // 定义了一个Dep
    const dep = new Dep() // 每个dep实例和data中每个key有一对一关系

    // 给obj的每一个key定义拦截
    Object.defineProperty(obj, key, {
      get() {
        // 依赖收集
        Dep.target && dep.addDep(Dep.target)
        return val
      },
      set(newVal) {
        if (newVal !== val) {
          val = newVal
          // console.log(key + '属性更新了')
          dep.notify() // 通知
        }
      }
    })
  }

  // 在vue根上定义属性代理data中的数据
  proxyData(key) {
    // this指的是KVue的实例
    Object.defineProperty(this, key, {
      get() {
        return this.$data[key]
      },
      set(newVal) {
        this.$data[key] = newVal
      }
    })
  }
}

// 创建Dep:管理所有的Watcher(观察订阅模式)
class Dep {
  constructor() {
    // 存储所有依赖
    this.watchers = []
  }
  addDep(watcher) {
    this.watchers.push(watcher)
  }
  notify() {
    this.watchers.forEach(watcher => watcher.update())
  }
}

// 创建Watcher:保存data中数值和页面中的挂钩关系
class Watcher {
  constructor(vm, key, cb) { // 某个组件中的某个key
    // 创建实例时立刻将该实例指向Dep.target,便于依赖收集
    Dep.target = this
    this.vm = vm
    this.key = key
    this.cb = cb

    Dep.target = this
    this.vm[this.key] // 读一下,触发依赖收集
    Dep.target = null
  }
  // 更新
  update() {
    // console.log(this.key + '更新了!!!')
    this.cb.call(this.vm, this.vm[this.key])
  }
}

编译compile

核心任务:

1、获取并遍历DOM树

2、文本节点:获取 {{}} 格式的内容并解析

3、元素节点:访问节点特性,截获 k- 和 @开头内容并解析

// 遍历dom结构,解析指令和插值表达式
class Compile {
  // el:待编译模板  vm:KVue实例
  constructor(el, vm) {
    this.$vm = vm
    this.$el = document.querySelector(el)

    // 把模板中的内容移到片段中操作
    this.$fragment = this.node2Fragment(this.$el)
    // 执行编译
    this.compile(this.$fragment)
    // 放回$el中
    this.$el.appendChild(this.$fragment)
  }

  node2Fragment(el) {
    // 创建片段:游离于dom文档之外,做修改的话不会让文档刷新
    const fragment = document.createDocumentFragment()
    let child
    while (child = el.firstChild) {
      fragment.appendChild(child)
    }
    return fragment
  }

  compile(el) {
    const childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      if (node.nodeType === 1) {
        // 元素
        // console.log('编译元素' + node.nodeName)
        this.compileElement(node)
      } else if (this.isInter(node)) { // 判断是否是文本节点
        // 只关心{{xxx}}的文本节点
        // console.log('编译插值文本' + node.textContent)
        this.compileText(node)
      }

      // 递归子节点
      if (node.children && node.childNodes.length > 0) {
        this.compile(node)
      }
    })
  }

  isInter(node) {
    return node.nodeType === 3 && /{{(.*)}}/.test(node.textContent)
  }

  // 文本替换
  compileText(node) {
    // console.log(RegExp.$1)

    // 表达式
    const exp = RegExp.$1
    this.update(node, exp, 'text') // v-text
    // node.textContent = this.$vm[RegExp.$1]
  }

  update(node, exp, dir) {
    const updator = this[dir + 'Updator']
    updator && updator(node, this.$vm[exp])
    // 创建Watcher实例,依赖收集完成了
    new Watcher(this.$vm, exp, function (value) {
      updator && updator(node, value)
    })
  }

  textUpdator(node, value) {
    node.textContent = value
  }

  compileElement(node) {
    // 关心属性
    const nodeAttrs = node.attributes
    Array.from(nodeAttrs).forEach(attr => {
      // 规定:k-xxx="yyy"
      const attrName = attr.name // k-xxx
      const exp = attr.value // yyy
      if (attrName.indexOf('k-') === 0) {
        // 指令
        const dir = attrName.substring(2) // xxx
        // 执行
        this[dir] && this[dir](node, exp)
      }
    })
  }

  text(node, exp){
    this.update(node, exp, 'text')
  }
}

测试代码:

<body>
  <div id="app">
    <p>{{name}}</p>
    <p k-text="name"></p>
    <p>{{age}}</p>
    <p> {{doubleAge}} </p>
    <input type="text" k-model="name">
    <button @click="changeName">呵呵</button>
    <div k-html="html"></div>
  </div>
  <script src='./compile.js'></script>
  <script src="./kvue.js"></script>
  <script>
    const app = new KVue({
      el: '#app',
      data: {
        name: "I am test.",
        age: 12,
        html: '<button>这是⼀一个按钮</button>'
      },
      created() {
        console.log('开始啦')
        setTimeout(() => {
          this.name = '我是测试'
          console.log('结束啦')
        }, 1500)
      },
      methods: {
        changeName() {
          this.name = '哈喽,开课吧'
          this.age = 1
        }
      }
    })
  </script>
</body>

v-html指令

html(node, vm, exp) {
  this.update(node, vm, exp, 'html')
}
htmlUpdator(node, value) {
  node.innerHTML = value
}

v-model指令

model(node, vm, exp) {
  this.update(node, vm, exp, 'model')

  node.addEventListener('input', e => {
    vm[exp] = e.target.value
  })
}

modelUpdator(node, value) {
  node.value = value
}

事件监听

compileElement(node) {
  // 关心属性
  const nodeAttrs = node.attributes
  Array.from(nodeAttrs).forEach(attr => {
    // 规定:k-xxx="yyy"
    const attrName = attr.name // k-xxx
    const exp = attr.value // yyy
    if (attrName.indexOf('k-') === 0) {
      // 指令
      const dir = attrName.substring(2) // xxx
      // 执行
      this[dir] && this[dir](node, exp)
    }

    // 事件处理
    if(attrName.indexOf('@') === 0) {
      const dir = attrName.substring(1) // 事件名称
      // 事件监听处理
      this.eventHandler(node, this.$vm, exp, dir)
    }
  })
}

// 事件处理:给node添加事件监听,dir-事件名称
// 通过vm.$options.methods[exp]可获得回调函数
eventHandler(node, vm, exp, dir) {
  let fn = vm.$options.methods && vm.$options.methods[exp]
  if (dir && fn) {
    node.addEventListener(dir, fn.bind(vm))
  }
}
原文地址:https://www.cnblogs.com/haishen/p/11783374.html