最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • vue2.0源码分析 简单实现new Vue(1)

    正文概述 掘金(JSteve_杰)   2021-01-12   518

    作为前端小白,日常搬砖,写的一般都是业务代码,对底层的实现原理一知半解。 so 觉得这样浑浑噩噩木有提升,开始从vue2.0源码入手,简单分析。最终实现一个简化版的Vue即可。
    本篇文章不从源码入手,因为源码功能繁多,通过简单的几个案例和分析,实现深入了解,从而了解vue的实现原理

    Vue 实现原理

    核心:实现数据 响应式

    理解Vue的设计思想:MVVM

    Vue是基于MVVM的一个前端框架,so 我就从MVVM入手

    MVVM

    M:模型层,负责业务数据相关
    V:视图层,负责视图相关,可以理解就是html+css层
    VM:用以连接V与M,负责监听M或者V的修改,扫描数据、拦截感知、执行更新函数,下图 Data Bindings 让视图更新,反之通过事件触发 model 的数据,从而实现双向绑定数据劫持。

    vue2.0源码分析 简单实现new Vue(1)

    将视图View的状态和⾏为抽象化,让我们将视图 UI 和业务逻辑分开。

    可以总结三点:数据响应式、模板引擎、渲染

    1. 数据响应式

    • 说明:监听 数据变化并在视图中更新
    • 实现方法:Object.defineProperty()
    • 前置:创建一个对象obj, 设置一个属性foo为0,在视图层有个id为app的容器。
    • 场景:if 当 obj.off 发生改变时,让视图内容跟着变化。简单实现数据变化并在视图中更新
    <div id="app"></div>
    <script>
      // 响应式通过  Object.defineProperty()
      function defineReactive (obj, key, val) {
        Object.defineProperty(obj, key, {
          get () { // 有变化就返回
            return val
          },
          set(v) {
            if (val !== v) { // 如果当前值和传入值不相同
              val = v
              update() // 触发页面渲染
            }
          }
        })
      }
      
      function update() {
        app.innerText = obj.foo
      }
    
      const obj = {}
      defineReactive(obj, 'foo', 'foo')
    
      setInterval(() => {
        obj.foo = new Date().toLocaleTimeString()
      }, 1000)
    </script>
    

    由上例可知,obj对象 的参数映射到 dom,参数改变影响页面

    对方法升级改造

    1. 首先,我们触发更新的方法是defineReactive(obj, 'foo', 'foo')。这样并不理想,所以要定义obj
    const obj = {
      foo: 'foo',
      bar: 'bar',
      baz: {
        a: 1
      }
    }
    
    1. 我去拦截obj的每一个key属性,并赋值。那么就需要递归,去遍及obj的值,写一个observe方法
    // 递归遍历obj,动态拦截obj的所有key
    function observe(obj) {
      if (typeof obj !== "object" || obj == null) { // 指定obj类型必须是对象
        return obj;
      }
    
      Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
      })
    }
    

    这样当触发 obj.foo = 'fooooo',会被拦截并赋值

    但是又遇到问题

    1. obj.baz是个对象,无法拦截。所以我们在 defineReactive函数中去执行一次 observe 方法 对每个val再进行递归
    2. 直接操作某层级的对象。e.g.:obj.baz = {a: 10} 监听不到 obj.baz.a的变化,此时应在set时递归
    3. if 动态追加属性,无法拦截 不触发get,set,此时应用this.$set()/Vue.set()
    function defineReactive (obj, key, val) {
      // 循环obj的每一项,都触发响应式
      observe(val)
      Object.defineProperty(obj, key, {
        get () {
          console.log('get', key)
          return val
        },
        set(v) {
          console.log('set', key)
          if (val !== v) {
            // 这里递归 if是对象 监听对象内部值的变化
            observe(v)
            val = v
          }
        }
      })
    }
    
    // 追加属性触发 set e.g. 
    // obj.newValue = 1  × 监听不到
    // set(obj,newValue, 1) √
    function set (obj, key, val) {
      defineReactive(obj, key, val)
    }
    

    此时数据响应式分析完成,下面是代码

    // Object.defineProperty()
    // 将传入的obj,动态设置一个key,它的值val
    function defineReactive(obj, key, val) {
      // 递归
      observe(val)
      
      Object.defineProperty(obj, key, {
        get() {
          console.log('get', key);
          return val
        },
        set(v) {
          if (val !== v) {
            console.log('set', key);
            // 传入新值v可能还是对象
            observe(v)
            val = v
          }
        },
      })
    }
    
    // 递归遍历obj,动态拦截obj的所有key
    function observe(obj) {
      if (typeof obj !== 'object' || obj == null) {
        return obj
      }
      Object.keys(obj).forEach(key => {
        defineReactive(obj, key, obj[key])
      })
    }
    
    // this.$set()
    // Vue.set()
    function set(obj, key, val) {
      defineReactive(obj, key, val)
    }
    
    const obj = {
      foo: 'foo',
      bar: 'bar',
      baz: {
        a: 1
      }
    }
    // defineReactive(obj, 'foo', 'foo')
    observe(obj)
    
    // obj.foo
    // obj.foo = 'fooooooo'
    // obj.baz.a
    // obj.baz = { a: 10 }
    // obj.baz.a
    // obj.dong = 'dong'
    // obj.dong
    // set(obj, 'dong', 'dong')
    // obj.dong
    

    而Vue中无外乎多了一些编译步骤,例如

    <div id="app">{{foo}}</div>
    

    因为无法识别出foo,需要通过编译器去解析 {{}}
    模板视图转换成更新函数 即解析 foo,触发update函数 就应用到 模板引擎, 渲染


    2. 模板引擎

    • 模版引擎:提供描述视图的模版语法
    • 插值:{{}}
    • 指令:v-bind,v-on,v-model,v-for,v-if 等
      1. 渲染:如何将模板转换为html
    • 模板 => vdom => dom

    接下来就要实现一个简单的Vue,实现原理分析

    1. new Vue() ⾸先执⾏初始化,对data执⾏响应化处理,这个过程发⽣在 Observer
    2. 同时对模板执⾏编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发⽣在 Compile
    3. 同时定义⼀个 更新函数 和 Watcher ,将来对应数据变化时Watcher会调⽤更新函数
    4. 由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher
      • (对应关系:1key:1Dep:nWatcher)
    5. 将来data中数据⼀旦发⽣变化,会⾸先找到对应的Dep,通知所有Watcher执⾏更新函数

    涉及类型介绍

    • Jvue:框架构造函数
    • Observer:执⾏数据响应化(分辨数据是对象还是数组)
    • Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
    • Watcher:执⾏更新函数(更新dom)
    • Dep:管理多个Watcher,批量更新

    开始实现,首先创建一个index.html

    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
    <div id="app">
      <!-- 把标签、{{}}都转换成函数 -->
      <p>{{counter}}</p>
      <p>{{counter}}</p>
      <p>{{counter}}</p>
      <!-- 不同指令对应函数 -->
      <p j-text="counter" @click="onclick"></p>
      <p j-html="desc"></p>
      <input type="text" j-model="counter">
    </div>
    <!-- 引入自定义的Vue脚本 -->
    <script src="./jvue.js"></script>
    <script>
      /* 生成vue实例 */
      const app = new JVue({
        el: '#app',
        data: {
          counter: 1,
          desc: '<span style="color: red">村长真棒</span>'
        },
        methods: {
          onclick() {
            this.counter++
          }
        },
      })
      setInterval(() => {
        app.counter++
      }, 1000);
    </script>
    

    js部分

    JVue

    首先创建一个Jvue类,接收外部传递的数据,执⾏初始化,对data执⾏响应化处理

    function defineReactive(obj, key, val) {
      // 递归
      observe(val);
      
      Object.defineProperty(obj, key, {
        get() {
          console.log("get", key);
          return val;
        },
        set(v) {
          if (val !== v) {
            console.log("set", key);
            // 传入新值v可能还是对象
            observe(v);
            val = v;
          }
        },
      });
    }
    
    // 递归遍历obj,动态拦截obj的所有key
    function observe(obj) {
      if (typeof obj !== "object" || obj == null) {
        return obj;
      }
    
      // 每出现一个对象,创建一个Ob实例  所以vue审查时,如果有_ob 即是相应式数据
      new Observer(obj);
    }
    
    class JVue { // 框架构造函数
      constructor(options) {
        // 保存选项,方便在应用位置拿到值
        this.$options = options;
        this.$data = options.data;
        
        // 2.响应式处理
        observe(this.$data)
      }
    }
    
    // Observer: 判断传入obj类型,做对应的响应式处理
    class Observer {
      constructor(obj) {
        this.value = obj;
    
        // 判断对象类型
        if (Array.isArray(obj)) {
          // todo
        } else {
          this.walk(obj);
        }
      }
    
      // 对象响应式
      walk(obj) {
        Object.keys(obj).forEach((key) => {
          defineReactive(obj, key, obj[key]);
        });
      }
    }
    
    

    代理data

    此时,页面数据理论上绑定了,实际并没有,因为传入的我们其实想要相应式的是data,而不是实例,所以进行一次代理,代理data到JVue实例上

    // 代理时要注意,很容易出现实例和代理有相同的key,这里为了方便不做处理
    function proxy(vm) {
      Object.keys(vm.$data).forEach((key) => { // 遍历data 给vm挂上
        Object.defineProperty(vm, key, {
          get() {
            return vm.$data[key];
          },
          set(v) {
            vm.$data[key] = v;
          },
        });
      });
    }
    
    class JVue {
      constructor(options) {
        // ...
    
        // 3.代理data到JVue实例上
        proxy(this); 
    
      }
    }
    

    此时,数据已绑定上,初始化数据已更新


    编译 - Compile

    编译模板中vue模板特殊语法,初始化视图、更新视图 (通过递归遍历节点)

    class JVue {
      constructor(options) {
        // ...
    
        // 4.编译
        new Compile(options.el, this);
    
      }
    }
    
    class Compile {
      // el-宿主,vm-JVue实例
      constructor(el, vm) {
        this.$vm = vm;
        this.$el = document.querySelector(el);
    
        this.compile(this.$el);
      }
    
      compile(el) {
        // 遍历el dom树
        el.childNodes.forEach((node) => {
          if (this.isElement(node)) {
            // element
            // 需要处理属性和子节点
            // console.log("编译元素", node.nodeName);
            this.compileElement(node);
    
            // 递归子节点
            if (node.childNodes && node.childNodes.length > 0) {
              this.compile(node);
            }
          } else if (this.isInter(node)) {
            // console.log("编译插值表达式", node.textContent);
            // 获取表达式的值并赋值给node
            this.compileText(node);
          }
        });
      }
    
      isElement(node) { // 节点是元素节点
        return node.nodeType === 1;
      }
    
      // {{xxx}}
      isInter(node) { // 节点是文本节点 并符合 {{}}
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
      }
    
      isDir(attr) {
        return attr.startsWith("j-");
      }
    
      // 更新函数,
      update(node, exp, dir) {
        // init
        const fn = this[dir + 'Updater']
        fn && fn(node, this.$vm[exp])
      }
      
      // 编译文本,将{{ooxx}}
      compileText(node) {
        this.update(node, RegExp.$1, 'text')
      }
    
      textUpdater(node, val) {
        node.textContent = val
      }
      
      // 处理元素所有动态属性
      compileElement(node) {
        Array.from(node.attributes).forEach((attr) => {
          const attrName = attr.name;
          const exp = attr.value;
    
          // 判断是否是一个指令
          if (this.isDir(attrName)) {
            // 执行指令处理函数
            // j-text, 截取text 触发指令方法
            const dir = attrName.substring(2);
            this[dir] && this[dir](node, exp)
          }
        });
      }
    
      // j-text处理函数
      text(node, exp) {
        this.update(node, exp, 'text')
      }
    
      // j-html
      html(node, exp) {
        this.update(node, exp, 'html')    
      }
    
      htmlUpdater(node, val) {
        node.innerHTML = val
      }
    }
    
    

    此时已编译成功,但是触发频率一直触发get,set,我们想通过某个方法监听,真正发生改变才会触发变更函数,就用到了Watcher ,Dep


    依赖收集

    视图中会⽤到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来⽤⼀个 Watcher来维护它们,此过程称为依赖收集。
    多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知。

    实现思路

    1. defineReactive时为每⼀个key创建⼀个Dep实例
    2. 初始化视图时读取某个key,例如name1,创建⼀个watcher1
    3. 由于触发name1的getter⽅法,便将watcher1添加到name1对应的Dep中
    4. 当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新
    // 依赖:和响应式对象的每个key一一对应
    class Dep {
      constructor() {
        this.deps = []
      }
    
      addDep(dep) {
        this.deps.push(dep)
      }
    
      notify() {
        this.deps.forEach(dep => dep.update())
      }
    }
    
    // 做dom更新
    class Watcher {
      constructor(vm, key, updateFn) {
        this.vm = vm
        this.key = key
        this.updateFn = updateFn
     
        // 创建watcher时触发getter 读取一下key的值,触发其get,从而收集依赖
        Dep.target = this
        this.vm[this.key]
        Dep.target = null
      }
    
      update() {
        this.updateFn.call(this.vm, this.vm[this.key])
      }
    }
    
    class Compile {
      // ...
      // 更新函数,
      update(node, exp, dir) {
        // init
        const fn = this[dir + 'Updater']
        fn && fn(node, this.$vm[exp])
    
        // update: 创建Watcher
        new Watcher(this.$vm, exp, function(val) {
          fn && fn(node, val)
        })
      }
    }
    
    function defineReactive(obj, key, val) {
      // 递归
      observe(val);
    
      // 依赖收集,创建Dep实例
      const dep = new Dep()
      
      Object.defineProperty(obj, key, {
        get() {
          console.log("get", key);
          // 依赖收集
          Dep.target && dep.addDep(Dep.target)
          return val;
        },
        set(v) {
          if (val !== v) {
            console.log("set", key);
            // 传入新值v可能还是对象
            observe(v);
            val = v;
    
            dep.notify()
          }
        },
      });
    }
    
    

    搞定


    起源地下载网 » vue2.0源码分析 简单实现new Vue(1)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元