<head><meta ...">
最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 关于Vue3响应式的理解

    正文概述 掘金(Yuin316)   2021-01-03   553

    简单的实现vue3的响应式

    简单的模拟vue3

    index.html

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
      </head>
      <body>
        <div id="app"></div>
      </body>
      <script type="module" src="./index.js"></script>
    </html>
    

    index.js

    import {
      ref,
      reactive,
      effect,
    } from "./node_modules/@vue/reactivity/dist/reactivity.esm-browser.js";
    
    //  vue3 最小模型
    
    const App = {
      render(context) {
        effect(() => {
          document.querySelector("#app").innerHTML = "";
          const div = document.createElement("div");
          div.innerHTML = context.count.value;
          document.querySelector("#app").append(div);
        });
      },
    
      setup() {
      // 在这里可以方便的在控制台通过修改count来观察变化
        window.count = ref(0);
    
        return { count };
      },
    };
    
    App.render(App.setup());
    

    结果

    关于Vue3响应式的理解

    effect的原理

    • 注意到上面的例子中,我们直接修改count.value就能直接引起dom的变化。这背后究竟是如何实现的?
    • 我们都知道ref, reactive这两个都是创建响应式对象的api,而effect在这里的作用是收集依赖和触发依赖。那么effect是如何做到的?我们再看一个例子。

    index.js

    import {
      ref,
      reactive,
      effect,
    } from "./node_modules/@vue/reactivity/dist/reactivity.esm-browser.js";
    
    const a = ref(5);
    let b;
    
    // 1.当程序第一次到这里时,会自动执行一次fn () => {...}
    effect(() => {
      b = a.value * 2;
      console.log("a", a.value, "b", b);
    });
    
    // a.value的setter触发时,触发第二次fn
    a.value = 10;
    

    输出
    a 5 b 10
    a 10 b 20

    • 在effect中,我们传入了一个函数fn,检测到这里有响应式数据a,那么fn被收集成为a的依赖。fn中的逻辑为a的setter触发时具体要做的事情。

    关于Vue3响应式的理解

    自己实现响应式

    如果我们自己实现响应式,那么要做的事情有什么?

    • 1、实现ref, reactivity
    • 2、实现effect
    • 3、收集依赖
    • 4、触发依赖

    为了实现对依赖(depend)的管理,我们可以定义一个Dep类来进行管理。

    ref与effect

    //  currentEffect是全局变量,方便Dep类访问并收集依赖
    let currentEffect = null;
    
    class Dep {
      #value;
    
      constructor(value) {
        this.#value = value;
    
        //  我们要保证依赖不会重复
        this.effects = new Set();
      }
    
      //  getter触发收集依赖
      get value() {
        this.depend(currentEffect);
        return this.#value;
      }
    
      //  setter触发所有依赖
      set value(newVal) {
        this.#value = newVal;
        this.notice();
      }
    
      //  收集依赖,即把effect中的fn收集起来
      depend() {
        //  判断currentEffect是否有值
        currentEffect && this.effects.add(currentEffect);
      }
    
      // 触发依赖
      notice() {
        this.effects.forEach((effect) => {
          effect();
        });
      }
    }
    
    function watchEffect(effect) {
      currentEffect = effect;
      effect();
      currentEffect = null;
    }
    
    function ref(val) {
      return new Dep(val);
    }
    
    const dep = ref(10);
    
    watchEffect(() => {
      //  触发getter, getter触发depend去收集依赖
      console.log(dep.value);
    });
    
    //  触发setter
    dep.value = 20;
    

    Result
    10
    20

    关于Vue3响应式的理解

    至此,我们已经实现了ref和effect的功能。

    reactive与effect

    • 注意到ref与effect中,一个value对应一个dep
    • 如果我们的响应式数据是一个对象呢,里面具有多个key,那么是不是就要需要多个dep来管理依赖?
    • 如果是对应多个dep,那么我们要选择怎样的数据结构去存储?
    • 如果我们要去取这个值,那么如何去取?

    接下来展示一下做法,利用Map和Proxy。

    //  currentEffect是全局变量,方便Dep类访问并收集依赖
    let currentEffect = null;
    
    class Dep {
    
      constructor() {
        //  我们要保证依赖不会重复
        this.effects = new Set();
      }
    
    
      //  收集依赖,即把effect中的fn收集起来
      depend() {
        //  判断currentEffect是否有值
        currentEffect && this.effects.add(currentEffect);
      }
    
      // 触发依赖
      notice() {
        this.effects.forEach((effect) => {
          effect();
        });
      }
    }
    
    function watchEffect(effect) {
      currentEffect = effect;
      effect();
      currentEffect = null;
    }
    

    在这里对Dep代码进行了部分的修改,因为在这里我们只需要Dep的收集依赖和触发依赖的功能。 关于Vue3响应式的理解

    //  targetsMap储存所有的对象(即用Map包装过的dep)
    //  数据结构
    //  targetsMap: Map { { name: 'xiaoming', age: 18 } => Map(0) {} }
    //  depsMap: Map  { 'name' => Dep { effects: Set(0) {} } }
    const targetsMap = new Map();
    
    // 辅助函数,用来获取对应的dep
    function getDep(target, key) {
      let depsMap = targetsMap.get(target);
      if (!depsMap) {
        // 相当于初始化的操作
        depsMap = new Map();
        targetsMap.set(target, depsMap);
      }
      
    //  targetsMap: Map { { name: 'xiaoming', age: 18 } => Map(0) {} }
    
    
      let dep = depsMap.get(key);
      if (!dep) {
        //  收集的依赖必须有值
        dep = new Dep();
        depsMap.set(key, dep);
      }
    //  depsMap: Map  { 'name' => Dep { effects: Set(0) {} } }
      return dep;
    }
    
    function reactive(raw) {
     //  如何知道取raw中的哪个key?
     //  通过Proxy
     //  Proxy是一个包含对象或函数并允许你对其拦截的对象 
    
      return new Proxy(raw, {
        get(target, key) {
          const dep = getDep(target, key);
          //  收集依赖
          dep.depend();
    
          return Reflect.get(target, key);
        },
        set(target, key, value) {
          const dep = getDep(target, key);
    
          // Reflect.set有返回值
          const result = Reflect.set(target, key, value);
          // notice()必须要在赋新值之后操作
          // 因为在赋新值前操作,你使用的还是旧值
          dep.notice();
          return result;
        },
      });
    }
    
    
    

    在reactive中,我们通过ProxyReflect进行操作,自然就不需要Dep中的getter和setter,Dep在这里的作用就是收集依赖和触发依赖。

    const user = {
      name: "xiaoming",
      age: 18,
    };
    
    const userState = reactive(user);
    
    watchEffect(() => {
      console.log(userState.name);
    });
    
    userState.name = "xiaohong";
    

    Result:
    xiaoming
    xiaohong

    使用自定义函数实现响应式

    关于Vue3响应式的理解

    首先我们把上述这些函数封装到文件夹/reactivity/index.js

    //  currentEffect是全局变量,方便Dep类访问并收集依赖
    let currentEffect = null;
    
    class Dep {
      #value;
    
      constructor(value) {
        this.#value = value;
    
        //  我们要保证依赖不会重复
        this.effects = new Set();
      }
    
      //  getter触发收集依赖
      get value() {
        this.depend(currentEffect);
        return this.#value;
      }
    
      //  setter触发所有依赖
      set value(newVal) {
        this.#value = newVal;
        this.notice();
      }
    
      //  收集依赖,即把effect中的fn收集起来
      depend() {
        //  判断currentEffect是否有值
        currentEffect && this.effects.add(currentEffect);
      }
    
      // 触发依赖
      notice() {
        this.effects.forEach((effect) => {
          effect();
        });
      }
    }
    
    export function watchEffect(effect) {
      currentEffect = effect;
      effect();
      currentEffect = null;
    }
    
    //  targetsMap储存所有的对象
    //  数据结构
    //  targetsMap: Map{key: depsMap<Map> }
    //  depsMap: Map {key: dep<Dep>}
    const targetsMap = new Map();
    
    function getDep(target, key) {
      let depsMap = targetsMap.get(target);
      if (!depsMap) {
        depsMap = new Map();
        targetsMap.set(target, depsMap);
      }
    
      let dep = depsMap.get(key);
      if (!dep) {
        dep = new Dep();
        depsMap.set(key, dep);
      }
    
      return dep;
    }
    
    export function reactive(raw) {
      return new Proxy(raw, {
        get(target, key) {
          const dep = getDep(target, key);
          dep.depend();
    
          return Reflect.get(target, key);
        },
        set(target, key, value) {
          const dep = getDep(target, key);
    
          const result = Reflect.set(target, key, value);
          dep.notice();
          return result;
        },
      });
    }
    
    export function ref(val) {
      return new Dep(val);
    }
    

    index.html

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
      </head>
      <body>
        <div id="app"></div>
      </body>
      <script type="module" src="./index.js"></script>
    </html>
    

    index.js

    import { ref, reactive, watchEffect } from "./reactivity/index.js";
    
    //  vue3 最小模型
    
    const App = {
      render(context) {
        watchEffect(() => {
          document.querySelector("#app").innerHTML = "";
          const div = document.createElement("div");
          div.innerHTML =
            context.count.value + " " + context.user.name + " " + context.user.age;
          document.querySelector("#app").append(div);
        });
      },
    
      setup() {
        window.count = ref(0);
        window.user = reactive({ name: "hello", age: 18 });
    
        return { count, user };
      },
    };
    
    App.render(App.setup());
    

    Result 关于Vue3响应式的理解

    • 到这里响应式基本完成,不过为了美观,我们需要抽离部分逻辑,实现解耦

    关于Vue3响应式的理解

    index.js

    import { createApp } from "./core/index.js";
    import App from "./App.js";
    
    //  createApp -> 创建根组件
    //  mount -> 挂载组件
    
    //  与vue3的用法保持一致
    createApp(App).mount(document.querySelector("#app"));
    

    App.js

    import { ref, reactive } from "./core/reactivity/index.js";
    export default {
    // 提供渲染模板
      render(context) {
        const div = document.createElement("div");
        div.innerHTML =
          context.count.value + " " + context.user.name + " " + context.user.age;
        return div;
      },
    
    // 提供响应式数据
      setup() {
        window.count = ref(0);
        window.user = reactive({ name: "hello", age: 18 });
    
        return { count, user };
      },
    };
    

    /core/index.js

    import { watchEffect } from "./reactivity/index.js";
    
    //  一开始我们的用法是App.render(App.setup())
    //  简单点说就是:
    //  setup()获取响应式数据 -> 丢给处理函数 -> watchEffect中渲染/更新视图
    
    //  我们封装之后就变成了
    //  createApp(App) -> mount -> setup获取App的响应式数据 -> 
    //  watchEffect -> 执行render(渲染/更新视图)
    export function createApp(rootComponent) {
      return {
        //  rootContainer 根容器
        mount(rootContainer) {
          //  setupResult获取响应式数据
          const setupResult = rootComponent.setup();
    
          watchEffect(() => {
            //  更新视图
            const element = rootComponent.render(setupResult);
            rootContainer.innerHTML = "";
            rootContainer.append(element);
          });
        },
      };
    }
    

    vdom

    • 我们都vue和react都是基于virtual dom进行的。所以我们需要实现一个创建vdom的函数h
    • 以及将vdom渲染成真实dom的函数mountElement

    关于Vue3响应式的理解
    /core/h.js

    // 作用是提供我们创建vdom所需要信息
    export function h(type, props, children) {
      return {
        type,
        props,
        children,
      };
    }
    

    /core/renderer.js -- 将虚拟节点vdom转成真实的dom

    //  虚拟节点转换为真实节点
    //  vnode: {type, props, children}
    
    //  children支持两种写法string与arrray
    //  h("div", null, [h("div", null, "hello")])
    //  h("div", null, "hi")
    export function mountElement(vnode, container) {
      const { type, props, children } = vnode;
    
      const el = createElement(type);
    
    // 有属性就给他挂上
      if (props) {
        for (const key in props) {
          const val = props[key];
          patchProps(el, key, null, val);
        }
      }
    
    // 如果子节点只是文本的话,就直接添加到尾部
      if (typeof children == "string" || typeof children == "number") {
        const text = document.createTextNode(children);
        el.append(text);
      } else if (Array.isArray(children)) {
        //  递归, 将其子节点挂在父节点上
        children.forEach((node) => {
          mountElement(node, el);
        });
      }
    
      container.append(el);
    }
    
    //  创建dom元素的类型
    function createElement(type) {
      return document.createElement(type);
    }
    
    //  给元素添加属性
    function patchProps(el, key, preValue, nextValue) {
      el.setAttribute(key, nextValue);
    }
    

    /core/index.js

    import { watchEffect } from "./reactivity/index.js";
    import { mountElement } from "./renderer.js";
    
    export function createApp(rootComponent) {
      return {
        //  rootContainer 根容器
        mount(rootContainer) {
          //  setupResult获取响应式数据
          const setupResult = rootComponent.setup();
    
          watchEffect(() => {
            // 获取render里关于虚拟节点的详细信息
            const subTree = rootComponent.render(setupResult);
            rootContainer.innerHTML = "";
            // 把subTree里虚拟节点的信息转换成真实的dom
            mountElement(subTree, rootContainer);
          });
        },
      };
    }
    

    app.js

    import { ref, reactive } from "./core/reactivity/index.js";
    import { h } from "./core/h.js";
    export default {
      render(context) {
        return h("div", { id: "test" }, [
          h("div", null, context.count.value),
          h("div", null, context.user.name),
        ]);
      },
    
      setup() {
        window.count = ref(0);
        window.user = reactive({ name: "hello", age: 18 });
    
        return { count, user };
      },
    };
    

    Result: 关于Vue3响应式的理解

    diff

    • 在没实现vdom之前,我们都是直接操作真实节点直接让innerHTML清空,这种做法太过暴力。而且无法判断节点是否更改或删除。
    • 我们实现vdom之后,就可以在renderer.js里的diff函数中进行判断。
    • diff就是可以找出哪些节点改变,哪些没有改变。
    • 在这次中实现的diff算法很简陋,大概就从type, props, children这几个方面去考虑。

    /core/renderer.js

    //  虚拟节点转换为真实节点
    //  vnode: {type, props, children}
    
    //  children支持两种写法string与arrray
    //  h("div", null, [h("div", null, "hello")])
    //  h("div", null, "hi")
    export function mountElement(vnode, container) {
      const { type, props, children } = vnode;
    
      const el = createElement(type);
    
      vnode.el = el;
    
      if (props) {
        for (const key in props) {
          const val = props[key];
          patchProps(el, key, null, val);
        }
      }
    
      if (typeof children == "string" || typeof children == "number") {
        const text = document.createTextNode(children);
        el.append(text);
      } else if (Array.isArray(children)) {
        //  递归, 将其子节点挂在父节点上
        children.forEach((node) => {
          mountElement(node, el);
        });
      }
    
      container.append(el);
    }
    
    //  创建dom元素的类型
    function createElement(type) {
      return document.createElement(type);
    }
    
    //  给元素添加属性
    function patchProps(el, key, preValue, nextValue) {
      if (nextValue) {
        el.setAttribute(key, nextValue);
      } else {
        //  删除属性
        el.removeAttribute(key);
      }
    }
    
    export function diff(prev, curr) {
      // 对比type, 不一样的话可以全部换掉
      if (curr.type != prev.type) {
        prev.el.replaceWith(createElement(curr.type));
      } else {
        // props改变
        // 主要是三种情况
        // 1. 值改变 prev {id: "old"} curr: {id:"new"}
        // 2. curr有新属性 prev {id: "old"} curr: {id:"old", test:"123"}
        // 3. curr少了属性 prev: {id:"old", test:"123"} curr: {test: "123"}
    
        const oldProps = prev.props || {};
        const newProps = curr.props || {};
    
        const el = (curr.el = prev.el);
    
        //  处理新的props
        Object.keys(newProps).forEach((key) => {
          //  情况一: 值更新
          //  情况二: 添加新值, 因为旧值中没有肯定是undefined
          if (newProps[key] !== oldProps[key]) {
            patchProps(el, key, oldProps[key], newProps[key]);
          }
        });
    
        //  处理旧的props
        //  情况三: 旧的有, 就需要删除
        Object.keys(oldProps).forEach((key) => {
          if (!newProps[key]) {
            patchProps(el, key, oldProps[key], null);
          }
        });
    
        //  处理children
        //  children -> string | array
        //  newChildren -> string | array
        //  两两组合, 就会出现四种情况
    
        const newChildren = curr.children || [];
        const oldChildren = prev.children || [];
    
        if (typeof newChildren == "string" || typeof newChildren == "number") {
          if (typeof oldChildren == "string" || typeof oldChildren == "number") {
            //  两个都是string, 对比一下是否不同再替换
    
            if (newChildren !== oldChildren) {
              el.textContent = newChildren;
            }
          } else if (Array.isArray(oldChildren)) {
            //  新children是string, 旧的string是节点数组, 直接替换即可
            el.textContent = newChildren;
          }
        } else if (Array.isArray(newChildren)) {
          if (typeof oldChildren == "string" || typeof oldChildren == "number") {
            //  有新的节点, 这时候要节点插入到旧节点的位置
            el.innerHTML = ``;
            newChildren.forEach((node) => {
              mountElement(node, el);
            });
          } else if (Array.isArray(oldChildren)) {
            //  考虑三种情况
            //  old的children与new的一样多
            //  old的比new的多
            //  old的比new的少
    
            //  暴力算法,先算出公共长度,公共长度内的直接对比替换
            //  多出来的直接删除
            //  少的就添加
    
            const commonLength = Math.min(newChildren.length, oldChildren.length);
            for (let i = 0; i < commonLength; i++) {
              const oldVnode = oldChildren[i];
              const newVnode = newChildren[i];
    
              diff(oldVnode, newVnode);
            }
    
            if (oldChildren.length > newChildren.length) {
              for (let i = commonLength; i < oldChildren.length; i++) {
                const vnode = oldChildren[i];
                el.removeChild(vnode.el);
              }
            }
    
            if (newChildren.length > oldChildren.length) {
              for (let i = commonLength; i < newChildren.length; i++) {
                const vnode = newChildren[i];
                mountElement(vnode, el);
              }
            }
          }
        }
      }
    }
    
    

    /core/index.js

    import { watchEffect } from "./reactivity/index.js";
    import { mountElement, diff } from "./renderer.js";
    //  一开始我们的用法是App.render(App.setup())
    //  简单点说就是:
    //  setup()获取响应式数据 -> 丢给render -> watchEffect-> 更新视图
    
    //  我们封装之后就变成了
    //  App -> createApp(App) -> mount -> setup获取响应式数据 ->
    //  watchEffect -> 执行render(更新视图)
    export function createApp(rootComponent) {
      return {
        //  rootContainer 根容器
        mount(rootContainer) {
          //  setupResult获取响应式数据
          const setupResult = rootComponent.setup();
    
          //  是否初始化
          let isMounted = false;
    
          let prevSubTree = null;
    
          watchEffect(() => {
            if (!isMounted) {
              isMounted = true;
              const subTree = rootComponent.render(setupResult);
              mountElement(subTree, rootContainer);
              prevSubTree = subTree;
            } else {
              //  初始化过了, 在这里实现diff
              const subTree = rootComponent.render(setupResult);
              diff(prevSubTree, subTree);
              prevSubTree = subTree;
            }
          });
        },
      };
    }
    
    

    app.js

    import { ref, reactive } from "./core/reactivity/index.js";
    import { h } from "./core/h.js";
    export default {
      render(context) {
        // const div = document.createElement("div");
        // div.innerHTML =
        //   context.count.value + " " + context.user.name + " " + context.user.age;
    
        return h("div", { id: "test" + context.count.value }, [
          h("div", null, context.count.value),
          h("div", null, [h("div", null, "我是干扰的")]),
          h("div", null, context.user.name),
        ]);
      },
    
      setup() {
        window.count = ref(0);
        window.user = reactive({ name: "hello", age: 18 });
    
        return { count, user };
      },
    };
    

    result: 关于Vue3响应式的理解


    起源地下载网 » 关于Vue3响应式的理解

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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