Vue.js设计与实现学习

第一章 权衡的艺术

框架设计里到处体现了权衡的艺术。

声明式和命令式框架的区别:

声明式:关注结果,将实现的过程进行了封装,比如将大象放入冰箱那么他只会发一个命令将大象放入冰箱
命令式:关注过程,将实现的方式展现出来,比如将大象放入冰箱,他的实现可能是第一步将冰箱门打开,第二步放入大象,第三步关上冰箱门。

vue.js是一个编译运行的框架。

第二章 框架设计的核心要素

  1. 提升用户体验,错误给出提示
  2. 控制代码的体积,生产环境给用户警告,正式环境减少非必要的警告
  3. 特性开关,根据配置开启不懂的特性
  4. 统一的错误处理或者自定义处理
  5. 对typeScript的良好支持

第三章 vue.js3的设计思路

声明是描述UI

js对象

const title = {
	tag : 'h1',
	props: {
		onClick: handler
	},
	children: [
		{tag: 'span'}
	]
}

js对象对应在vue.js中就是如下所示

<h1 @click="handler"><span>sapn>h1>

初识渲染器

如下所示这是个虚拟DOM对象。

            const vnode = {
                tag: "div",
                props: {
                    onClick: () => alert('hello')
                },
                children: 'click me'
            }

设计一个函数将上面的虚拟DOM变成真实的DOM

            function renderer(vnode, container) {
                const el = document.createElement(vnode.tag)
                for(const key in vnode.props) {
                    if(/^on/.test(key)){
                        el.addEventListener(
                            key.substr(2).toLowerCase(),
                            vnode.props[key]
                        )
                    }
                }
                
                if(typeof vnode.children === 'string') {
                    el.appendChild(document.createTextNode(vnode.children))
                }else if(Array.isArray(vnode.children)){
                    vnode.children.forEach(child => renderer(child,el))
                }
                
                container.appendChild(el)
            }
            

这个函数将虚拟DOM解析为了真实的DOM元素

完整代码

DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>title>
        <style>
            div::before{
                width: 100px;
                height: 100px;
                content: "";
                background-color: red;
            }
        style>
    head>
    <body>
        <div>111div>
        <script>
            const vnode = {
                tag: "div",
                props: {
                    onClick: () => alert('hello')
                },
                children: 'click me'
            }
            
            function renderer(vnode, container) {
                const el = document.createElement(vnode.tag)
                for(const key in vnode.props) {
                    if(/^on/.test(key)){
                        el.addEventListener(
                            key.substr(2).toLowerCase(),
                            vnode.props[key]
                        )
                    }
                }
                
                if(typeof vnode.children === 'string') {
                    el.appendChild(document.createTextNode(vnode.children))
                }else if(Array.isArray(vnode.children)){
                    vnode.children.forEach(child => renderer(child,el))
                }
                
                container.appendChild(el)
            }
            
            renderer(vnode, document.body)
        script>
    body>
html>

组件的本质

组件的本质就是一组DOM元素的封装

DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>title>
    head>
    <body>
        <div>111div>
        <script>
            const MyComponent = function() {
                return {
                            tag: "div",
                            props: {
                                onClick: () => alert('hello')
                            },
                            children: 'click me'
                        }
            }
            // vnode 的tag元素可以是一个函数,字符串乃至对象
            const vnode = {
                tag: MyComponent
            }
            
            function renderer(vnode, container){
                if(typeof vnode.tag === 'string'){
                    mountElement(vnode, container)
                }else if(typeof vnode.tag === 'function'){
                    mountComponent(vnode, container)
                }
            }
            
            // 和原来的renderer函数一致
            function mountElement(vnode, container) {
                const el = document.createElement(vnode.tag)
                for(const key in vnode.props) {
                    if(/^on/.test(key)){
                        el.addEventListener(
                            key.substr(2).toLowerCase(),
                            vnode.props[key]
                        )
                    }
                }
                
                if(typeof vnode.children === 'string') {
                    el.appendChild(document.createTextNode(vnode.children))
                }else if(Array.isArray(vnode.children)){
                    vnode.children.forEach(child => renderer(child,el))
                }
                
                container.appendChild(el)
            }
            
            // 解析到tag再次调用renderer函数
            function mountComponent(vnode, container){
                const subtree = vnode.tag()
                renderer(subtree, container)
            }
            
            renderer(vnode, document.body)
        script>
    body>
html>

模板的工作原理

将vue的模板编译到script的内容里

		<template>
	       <div @click="handler">
	            click me
	        div>
        template>
        <script>
            export default {
                data() {},
                methods:{
                    handler: () => {
                        
                    }
                }
            }
        script>

如上所示是一个模板的基本构造,编译器会把html也就是template标签的内容编译到export里面,编译后的代码如下

        <script>
            export default {
                data() {},
                methods:{
                    handler: () => {
                        
                    }
                },
                render() {
                	return h('div', {onclick: handler}, 'click me')
                }
            }
        </script>

vue.js是各个模块组成的有机整体

组件的实现依赖于渲染器,模板的编译依赖于编译器。vue.js就是将各个模组组织到一起。

模板

<div id="foo" :class="cls">div>

编译器编译成渲染函数

render(){
	return {
		tag: 'div',
		props: {
			id: 'foo',
			class: cls
		}patchFlags: 1
	}
}

总上所示,class是一个变量,为了后续的方便更新,减少不必要的消耗,编译器会加上特殊的标记。方便后续更新视图。patchFlags便是提示那些可以更新的值。

响应系统的作用与实现

响应数据与副作用函数

const obj = { text : 'hello world'}
            
function effect() {
    document.body.innerText = obj.text
}
effect();
obj.text = 'hello vue3'

如上所示,obj 就是一个响应数据,执行了副作用函数之后,页面的内容变为hello world,但是之后修改obj的值,页面上的内容并没有变过来。

响应数据的基本实现

我们将给对象设置的时候,让其重新执行副作用函数,
监听对象的属性读取操作,vue2使用的是Object.defineProperty,vue3使用的是对象Proxy方法

const bucket = new Set()
const data = { text : 'hello world'}

const obj = new Proxy(data,{
    get(target, key){
        bucket.add(effect)
        return target[key]
    },
    
    set(target, key, newVal) {
        target[key] = newVal
        bucket.forEach(fn => fn())
        return true
    }
})
function effect() {
    document.body.innerText = obj.text
}
effect();
setTimeout(() =>{
    obj.text = 'hello vue3'
},1000)

在读取对象属性是,将副作用添加到桶中,设置的时候将桶中的方法再执行一次。

设置一个完善的响应系统

1.读取操作的时候将副作用函数放入桶中
2.当设置操作的时候在将副作用函数取出执行

	// 全局变量存储副作用函数
	let activeEffect;
	const bucket = new Set()
	
	// 接受一个函数作为参数 
	function effect(fn) {
		activeEffect = fn;
	    fn();
	}
	
	const data = { text : 'hello world'}
	
	const obj = new Proxy(data,{
	    get(target, key){
			if(activeEffect){
				bucket.add(activeEffect)
			}

	        return target[key]
	    },
	    
	    set(target, key, newVal) {
	        target[key] = newVal
	        bucket.forEach(fn => fn())
	        return true
	    }
	})
	// 匿名函数注册
	effect(fn => {
		console.log("执行了这个函数")
		document.body.innerText = obj.text
	});
	
	
	setTimeout(() =>{
	    obj.notExist = 'hello vue3'
	},1000)

如上代码所示,给obj添加notExist属性的时候,副作用函数也执行了,因为副作用函数是直接是挂在Object对象上的,那这个响应系统不是很完善,实际应该在对应的属性上比较好。

接下来,我们实现这个方法。

首先用WeakMap代替Set,修改拦截方法。

     // 全局变量存储副作用函数
     let activeEffect;
     const bucket = new WeakMap()
     
     // 接受一个函数作为参数 
     function effect(fn) {
         activeEffect = fn;
         fn();
     }
     
     const data = { text : 'hello world'}
     
     const obj = new Proxy(data,{
         get(target, key){
             if(!activeEffect){
                 return target[key]
             }
             
             let depsMap = bucket.get(target)
             if(!depsMap){
                 bucket.set(target, (depsMap = new Map()));                     
             }
             
             let deps = depsMap.get(key)
             if(!deps){
                 depsMap.set(key, (deps = new Set()))
             }
             deps.add(activeEffect);
             
             return target[key]
         },
         
         set(target, key, newVal) {
             target[key] = newVal
             let depsMap = bucket.get(target)
             if(!depsMap){
                 return
             }
             let effects = depsMap.get(key)
             
             effects && effects.forEach(fn => fn())
         }
     })
     // 匿名函数注册
     effect(fn => {
         console.log("执行了这个函数")
         document.body.innerText = obj.text
     });
     
     
     setTimeout(() =>{
         obj.notExist = 'hello vue3'
     },1000)

如上所示,对应的键就和副作用函数绑定在一起了。

将方法封装一下。如下:

DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>title>
    head>
    <body>
        <script>
            // 全局变量存储副作用函数
            let activeEffect;
            const bucket = new WeakMap()
            
            // 接受一个函数作为参数 
            function effect(fn) {
                activeEffect = fn;
                fn();
            }
            
            const data = { text : 'hello world'}
            
            const obj = new Proxy(data,{
                get(target, key){
                    track(target, key);
                    
                    return target[key]
                },
                
                set(target, key, newVal) {
                    target[key] = newVal
                   trigger(target, key)
                }
            })
            
            function track(target, key){
                if(!activeEffect){
                    return
                }
                
                let depsMap = bucket.get(target)
                if(!depsMap){
                    bucket.set(target, (depsMap = new Map()));                     
                }
                
                let deps = depsMap.get(key)
                if(!deps){
                    depsMap.set(key, (deps = new Set()))
                }
                deps.add(activeEffect);
            }
            
            function trigger(target, key){
                let depsMap = bucket.get(target)
                if(!depsMap){
                    return
                }
                let effects = depsMap.get(key)
                
                effects && effects.forEach(fn => fn())
            }
            // 匿名函数注册
            effect(fn => {
                console.log("执行了这个函数")
                document.body.innerText = obj.text
            });
            
            
            setTimeout(() =>{
                obj.notExist = 'hello vue3'
            },1000)
        script>
    body>
html>

分支切换与Clean

如下所示,ok为true的时候,页面上的值来自于text,否则就是一个固定值。此时ok和text两个键都绑定了副作用函数,如果我们将ok的值改为false,那么text对应的副作用函数应该是需要去除的。这就是分支。

 const data = { ok: true, text : 'hello world'}
 // 匿名函数注册
 effect(fn => {
     console.log("执行了这个函数")
     document.body.innerText = obj.ok ? obj.text : 'not';
 });
            

首先想到的就是执行副作用函数的时候重新绑定以下副作用函数。

  function effect(fn) {
       const effectFn = () => {
           cleanup(effectFn)
           activeEffect = effectFn
           fn();
       }
       effectFn.deps = []
       effectFn();
   }
   function cleanup(effectFn){
       for(let i = 0; i < effectFn.deps.length;i++){
           const deps = effectFn.deps[i]
           
           deps.delete(effectFn)
           
       }
       
       effectFn.deps.length = 0
   }

执行副作用函数的时候,将effectFn.deps的内容清除掉,然后执行副作用函数。这段我看的也不是很透彻。
修正对应的trigger方法

            function trigger(target, key){
                let depsMap = bucket.get(target)
                if(!depsMap){
                    return
                }
                let effects = depsMap.get(key)
                
                const effectsToRun= new Set(effects)
                effectsToRun.forEach(fn => fn())
            }

避免无限递归循环

effect(() => {
	obj.foo = obj.foo + 1
})

在一个方法中既有读取也有设置操作,就会导致无限循环

      function trigger(target, key){
          let depsMap = bucket.get(target)
          if(!depsMap){
              return
          }
          let effects = depsMap.get(key)
          
          const effectsToRun= new Set()
          effects && effects.forEach(effectFn =>{
          		// 设置同一个方法只添加一次,防止无限循环
              if(effectFn != activeEffect){
                  effectsToRun.add(effectFn)
              }
          })
          effectsToRun.forEach(fn => fn())
      }

计算属性Computed与lazy

effect下的lazy属性

你可能感兴趣的:(学习)