美文网首页
Vue源码解析,模拟Vue的执行流程,实现一个简易的Vue

Vue源码解析,模拟Vue的执行流程,实现一个简易的Vue

作者: 闪电西兰花 | 来源:发表于2021-03-19 14:36 被阅读0次
    关于源码的部分总结
    • 编译的重要性:首先vue模板中的很多语法html是不能识别的,例如插值表达式、指令等,其次我们通过编译的过程可以进行依赖收集,依赖收集后 data 中的数据模型就跟数据产生了绑定关系,当数据模型发生变化就可以通知依赖做更新,最终实现模型驱动视图变化
    • 双向绑定的原理:双向绑定是指在 input 元素上使用 v-model 指令,在编译时解析 v-model 然后给当前元素加上事件监听,将 v-model 的回调函数作为 input 的回调函数,如果input 发生变化就可以更新对应值,值又是绑定在 vue 实例上的,实例同时又做了数据响应化也就是数据劫持,会触发他的setter 函数,然后通知对应依赖进行更新
    相关方法的理解
    • Object.defineProperty() 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
    • Document.createDocumentFragment 创建一个新的空白的文档片段
    1. vue 整体执行流程
    1. new Vue()
        1.1 通常new Vue()之后,Vue会调用进行初始化,初始化生命周期、事件、props、methods、data、computed、watch等
        1.2 在beforeCreate之后、create之前,data数据的劫持、属性代理
            - 主要是通过Object.defineProperty给每个属性添加Dep实例,负责收集与该属性有依赖关系的Watcher(这个Dep实例可以存储当前属性的依赖、添加依赖、通知属性更新)
            - 将属性定义到当前创建的vue实例上,同时创建属性的setter、getter函数,实时获取与修改实例中的data数据
    
    2. 调用$mount挂载组件
    
    3. compile编译(对template模板进行扫描) 
        3.1 转换根节点内容为DocumentFragment对象
            - 通过document.createDocumentFragment创建空白文档片段,将根节点中的所有子元素添加至文档片段中,然后编译DocumentFragment对象
        3.2 正则解析vue的指令,形成语法树(AST)
            - 遍历所有子节点,分别匹配元素节点(v-、@、:开头的)、文本节点(插值表达式)
        3.3 标记静态节点,用于性能优化
            - 匹配对应节点后,调用对应的更新函数,更新node节点的值,同时创建依赖(也就是watcher),一个节点属性对应一个watcher
            - 创建依赖时,会将依赖存入每个属性的Dep实例中(同一个变量在模板中使用几次,对应属性的Dep实例数组中就保存几个watcher)
            - 同时创建属性的setter、getter函数,在访问和修改对象属性时,更新对应的依赖
        3.4 将编译后的结果追加至根节点
        3.5 generate 将AST转为渲染函数render function
    
    4. render function
        4.1 生成虚拟DOM树,后期改变数据改变的其实都是虚拟DOM上的数据,在更新前会做diff算法的比较,通过计算后执行最小更新,这个流程是用js计算时间换来了更少的dom操作,核心目的就是减少浏览器的页面渲染次数和数量
        4.2 依赖收集,通过数据劫持、观察者模式,发现数据发生变化,然后找到对应的DOM节点进行更新
    
    2. 实现数据响应(数据劫持)部分,利用defineProperty对属性进行读取、改变操作时,会执行属性对应的 gettersetter 函数
    // 写个小例子感受下defineProperty,在obj对象上定义一个新属性
    // 设置属性的getter、setter函数
    
    // defineProperty.html
    <body>
      <div id="app">
        <p id="name"></p>
      </div>
    
      <script>
        let obj = {}
        Object.defineProperty(obj,'name', {
          get () {
            return document.querySelector('#name').innerHTML
          },
          set (val) {
            document.querySelector('#name').innerHTML = val
          }
        })
        obj.name = 'Asher'
      </script>
    </body>
    
    下面实现数据响应部分(数据劫持)
    // 先约定好使用方法,和vue一样
    new Vue({
      el: '',
      data: {},
      methods: {}
    })
    
    // svue.js
    class SVue {
      constructor (options) {
        this.$options = options
    
        // 数据响应
        this.$data = options.data
        this.observe(this.$data)
      }
      
      observe (val) {
        if(!val || typeof val !== 'object') return
        // 遍历data的属性
        Object.keys(val).forEach(key => {
            this.defineReactive(val, key, val[key])
        })
      }
    
      // 数据响应化函数(数据劫持)
      defineReactive (obj, key, val) {
        Object.defineProperty (obj, key, {
          get () {
            return val
          },
          set (newVal) {
            if(val === newVal) return
            val = newVal
            console.log(`${key}属性更新了:${val}`)
          }
        })
      }
    }
    
    // index.html
    // 执行当前文件后,发现只打印了一个'属性更新了'
    // 因为data属性中,有一个foo属性的值是一个对象,因此需要考虑深度遍历的问题
    <script src="svue.js"></script>
    <script>
        const app = new SVue({
          data: {
            test: 'I am test',
            foo: {
              bar: 'bar'
            },
            name: 'Asher'
          }
        })
        app.$data.test = 'hello, SVue'   // test属性更新了:hello, SVue
        app.$data.foo.bar = 'oh bar'
    </script>
    
    // svue.js
    // 修改数据响应化函数,添加递归
    defineReactive (obj, key, val) {
      // 添加递归 解决数据嵌套
        this.observe(val)
    
        Object.defineProperty (obj, key, {
          get () {
            return val
          },
          set (newVal) {
            if(val === newVal) return
            val = newVal
            console.log(`${key}属性更新了:${val}`)
          }
        })
      }
    }
    
    4. 依赖收集:遍历模板( template )进行依赖收集,在更新数据时,只有能匹配到有对应依赖时才会做数据更新
    // 通过一个简单例子理解下依赖收集
    // 在下面这个例子中,data数据中有name1、name2和name3,但实际在元素中使用并展示的数据只有name1、name2,而且name1使用2次,但是在created函数中又更新了name1和name3
    // 这时的内部执行机制是先进行视图依赖收集,保存视图中有哪些地方对数据有依赖,这样当数据变化时,直通知对应依赖即可,没有依赖的则不做更新
    new Vue({
      template:
        `
        <div>
          <span>{{name1}}</span>
          <span>{{name2}}</span>
          <span>{{name1}}</span>
        </div>
        `,
        data: {
          name1: 'name1',
          name2: 'name2',
          name3: 'name3'
        },
        created () {
          this.name1 = 'Asher',
          this.name3 = 'Andy'
        }
    })
    
    实现依赖收集、观察者部分,针对 data 中的每个属性创建每个对应的依赖,当属性在页面中出现几次,那么就有几个 watcher,这个过程大概就是每个依赖有自己各自的 watcher,依赖对自己的 watcher 进行管理
    // svue.js
    // 用来管理watcher
    class Dep {
      constructor () {
        // 存放若干依赖(watcher,一个watcher对应一个属性)
        this.deps = []
      }
    
      // 添加依赖
      addDep (dep) {
        this.deps.push(dep)
      }
    
      // 通知watcher,更新依赖
      notify () {
        this.deps.forEach(dep => dep.update())
      }
    }
    
    // watcher
    class Watcher {
      constructor () {
        // 将当前watcher实例指向Dep静态属性target
        Dep.target = this
      }
    
      update () {
        console.log('属性更新了')
      }
    }
    
    模拟依赖创建(注意这里只是模拟!模拟!new Watcher()并不在这里调用)
    // 在构造函数中模拟watcher创建,修改数据劫持部分
      constructor (options) {
        this.$options = options
    
        // 数据响应
        this.$data = options.data
        this.observe(this.$data)
    
        // 模拟watcher创建,每改变一个属性就要创建一个watcher
        new Watcher()
        this.$data.test;
        new Watcher()
        this.$data.foo.bar;
      }
    
      // data属性响应式
      defineReactive (obj, key, val) {
        // 递归 解决数据嵌套
        this.observe(val)
    
        // 初始化 Dep
        const dep = new Dep()
    
        Object.defineProperty(obj, key, {
          get () {
            Dep.target && dep.addDep(Dep.target)
            return val
          },
          set (newVal) {
            if(val === newVal) return
            val = newVal
            dep.notify()
          }
        })
      }
    
    5. 编译:在vue中使用的插值表达式、v-指令@事件 等等,这些标识,html 都不认识
    使用 document.createDocumentFragment() 创建空白文档片段,主要用于将元素附加到该文档上,然后将文档片段附加到DOM树,这样不会引起页面回流,避免频繁操作DOM更新页面
    // compile.js
    class Compile {
      // 当前遍历元素 当前vue实例
      constructor (el, vm) {
        // 要遍历的节点
        this.$el = document.querySelector(el)
        this.$vm = vm
    
        if(this.$el) {
          // 转换内部内容为Fragment
          this.fragment = this.node2Fragment(this.$el)
          // 执行编译
          this.compile(this.fragment)
          // 将编译后的结果追加至$el
          this.$el.appendChild(this.fragment)
        }
      }
    
      // 编译
      compile (el) {
        const childsNodes = el.childNodes
        Array.from(childsNodes).forEach(node => {
          if(this.isElement(node)) {
            // 元素节点
            console.log('编译元素' + node.nodeName)
          }else if(this.isInterpolation(node)) {
            // 文本节点 插值格式
            console.log('编译文本' + node.textContent)
          }
    
          // 递归子节点
          if(node.childNodes && node.childNodes.length > 0) {
            this.compile(node)
          }
        })
      }
    
      node2Fragment (el) {
        const frag = document.createDocumentFragment()
        // 将el中所有子元素移至frag中
        let child;
        while(child = el.firstChild) {
          frag.appendChild(child)
        }
        return frag
      }
    
      // 元素子节点
      isElement (node) {
        return node.nodeType === 1
      }
    
      // 文本子节点 插值表达式{{}}
      isInterpolation (node) {
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
      }
    }
    
    修改之前的文件,测试一下编译部分的逻辑
    // index.html
    
    <div id="app">
       <p>{{name}}</p>
       <p s-text="name"></p>
       <p @click="handleClick">点击更换文案</p>
       <input type="text" s-model="name">
    </div>
    
    <script src="svue.js"></script>
    <script src="compile.js"></script>
    
    // svue.js
      constructor (options) {
        this.$options = options
    
        // 数据响应
        this.$data = options.data
        this.observe(this.$data)
    
        // 编译
        new Compile(options.el, this)
      }
    
    根据 index.html 结构可以得到打印结果,以及页面展示
    编译-区分节点.jpg
    完成元素节点的匹配,下面就是针对各种类型的节点做不同处理,例如文本节点插值表达式,我们要根据使用的属性在 data 中获取到对应的值,然后才能正确的展示在页面上
    // compile.js
    // 编译文本节点
    compileText (node) {
       // RegExp.$1 获取正则表达式中第一个()分组匹配到的值
       // 将变量值复制给DOM并展示
       node.textContent = this.$vm.$data[RegExp.$1]
    }
    
    编译-匹配插值表达式.jpg
    完成部分编译之后,每编译一部分内容就要绑定对应的依赖,要不然我们是没办法做到依赖监听和及时更新的,数据也只会初始化一次,我们来测试下现在没有绑定依赖的情况(参照如下文件的修改)
    // index.html
    <div id="app">
       <p>{{name}}</p>
       <p s-text="name"></p>
       <p @click="handleClick">点击更换文案</p>
       <input type="text" s-model="name">
    </div>
    
      <script>
        const app = new SVue({
          el: 'app',
          data: {
            test: 'I am test',
            foo: {
              bar: 'bar'
            },
            name: 'Asher'
          },
          // 设置生命周期以及延时执行修改name
          created () {
            console.log('created执行了')
            setTimeout(() => {
              console.log('created中的setTimeout执行了')
              this.name = 'created中修改name属性'
            }, 1500)
          },
        })
      </script>
    
    // svue.js
      constructor (options) {
        this.$options = options
    
        // 数据响应
        this.$data = options.data
        this.observe(this.$data)
    
        // 编译
        new Compile(options.el, this)
    
        // 匹配created 修改this绑定
        if(options.created) {
          options.created.call(this)
        }
      }
    
    从下面打印的结果可以看出,created 和延时函数都执行了,但页面展示的结果并没有改变,说明数据并未更新
    编译-未绑定依赖.jpg
    下一步,处理属性代理,添加更新逻辑,属性代理是将属性绑定到当前vue实例上,为了方便在访问属性时可以直接通过 this.data 访问
    // svue.js
    class SVue {
      // 观察者
      observe (val) {
        if(!val || typeof val !== 'object') return
        // 遍历data的属性
        Object.keys(val).forEach(key => {
            this.defineReactive(val, key, val[key])
            // 代理data中的属性到vue实例上
            this.proxyData(key)
        })
      }
    
      proxyData (key) {
        // this 指当前vue实例
        Object.defineProperty(this, key, {
          get () {
            return this.$data[key]
          },
          set (newVal) {
            this.$data[key] = newVal
          }
        })
      }
    }
    
    // compile.js
    // 通用更新函数 参数:节点、实例、表达式、指令(区分文本、事件等,方法名前缀)
    update (node, vm, exp, dir) {
        const updaterFn = this[dir + 'Updater']
        // 初始化
        updaterFn && updaterFn(node, vm[exp])
        // 依赖收集
        new Watcher(vm, exp, function(val){
          updaterFn && updaterFn(node, val)
        })
    }
    
    textUpdater (node, val) {
       node.textContent = val
    }
    
    // 修改 compileText 方法
    compileText (node) {
       this.update(node, this.$vm, RegExp.$1, 'text')
    }
    
    // svue.js
    // 修改 Watcher 类
    // watcher
    class Watcher {
      constructor (vm, key, callback) {
        this.vm = vm
        this.key = key
        this.callback = callback
        // 将当前watcher实例指向Dep静态属性target
        Dep.target = this
        // 触发getter 添加依赖
        this.vm[this.key]
        // 置空 避免重复添加
        Dep.target = null
      }
    
      update () {
        // console.log('属性更新了')
        this.callback.call(this.vm, this.vm[this.key])
      }
    }
    
    编译-绑定依赖后 延时更新成功.jpg
    接着补充编译部分对指令、事件等的识别和处理
    // compile.js
    // 编译
    compile (el) {
        const childsNodes = el.childNodes
        Array.from(childsNodes).forEach(node => {
          if(this.isElement(node)) {
            // 元素节点
            console.log('编译元素' + node.nodeName)
            // 查找s-、@、:开头的
            const nodeAttrs = node.attributes
            Array.from(nodeAttrs).forEach(attr => {
              // 属性名
              const attrName = attr.name
              // 属性值
              const exp = attr.value
              if(this.isDirective(attrName)) {
                const dir = attrName.substring(2)
                this[dir] && this[dir](node, this.$vm, exp)
              }
              if(this.isEvent(attrName)) {
                const dir = attrName.substring(1)
                this.eventHandler(node, this.$vm, exp ,dir)
              }
            })
          }else if(this.isInterpolation(node)) {
            // 文本节点 插值格式
            console.log('编译文本' + node.textContent)
            this.compileText(node)
          }
    
          // 递归子节点
          if(node.childNodes && node.childNodes.length > 0) {
            this.compile(node)
          }
        })
    }
    
    // 指令
    isDirective (attr) {
       return attr.indexOf('s-') == 0
    }
    
    // 事件
     isEvent (attr) {
       return attr.indexOf('@') == 0
    }
    
    // 事件处理 从methods找出对应方法
    eventHandler (node, vm, exp, dir) {
        let fn = vm.$options.methods && vm.$options.methods[exp]
        if(dir && fn) {
          node.addEventListener(dir, fn.bind(vm))
        }
    }
    
    text (node, vm, exp) {
       this.update(node, vm, exp, 'text')
     }
    
    下面可以添加一个事件测试一下
    // index.html
    <div id="app">
       <p>{{name}}</p>
       <p s-text="name"></p>
       <p @click="handleClick">点击更换文案</p>
       <input type="text" s-model="name">
    </div>
    
    methods: {
      handleClick () {
        this.name = 'Click'
      }
    }
    
    事件绑定.jpg
    最后,补充双向绑定的部分,通过指令 s-modle 使用
    // compile.js
      module (node, vm, exp) {
        // 指定input的value属性
        this.update(node, vm, exp, 'model')
        node.addEventListener('input', e => {
          vm[exp] = e.target.value
        })
      }
    
      moduleUpdater (node, value) {
        node.value = value
      }
    
      // 编译
      compile (el) {
        const childsNodes = el.childNodes
        Array.from(childsNodes).forEach(node => {
          if(this.isElement(node)) {
            // 元素节点
            console.log('编译元素' + node.nodeName)
            // 查找s-、@、:开头的
            const nodeAttrs = node.attributes
            Array.from(nodeAttrs).forEach(attr => {
              // 属性名
              const attrName = attr.name
              // 属性值
              const exp = attr.value
              if(this.isDirective(attrName)) {
                const dir = attrName.substring(2)
                this[dir] && this[dir](node, this.$vm, exp)
              }
              if(this.isEvent(attrName)) {
                const dir = attrName.substring(1)
                this.eventHandler(node, this.$vm, exp ,dir)
              }
            })
          }else if(this.isInterpolation(node)) {
            // 文本节点 插值格式
            console.log('编译文本' + node.textContent)
            this.compileText(node)
          }
    
          // 递归子节点
          if(node.childNodes && node.childNodes.length > 0) {
            this.compile(node)
          }
        })
      }
    

    相关文章

      网友评论

          本文标题:Vue源码解析,模拟Vue的执行流程,实现一个简易的Vue

          本文链接:https://www.haomeiwen.com/subject/qxqxqltx.html