• 四、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))
      }
    }
  • 相关阅读:
    fatal error C1083: 无法打开包括文件:“iostream.h”: No such file or directory
    Dan Saks
    '=' : left operand must be lvalue 左值和右值
    sizeof使用
    stream.js :一个新的JavaScript数据结构
    Kibo:键盘事件捕捉高手
    c中不能用引用的办法
    分布式版本控制工具:git与Mercurial
    非常好的BASH脚本编写教程
    Handler让主线程和子线程进行通信
  • 原文地址:https://www.cnblogs.com/haishen/p/11783374.html
Copyright © 2020-2023  润新知