Giter VIP home page Giter VIP logo

zjn225.github.io's People

Contributors

zjn225 avatar

Watchers

 avatar

zjn225.github.io's Issues

Vue双向绑定原理和基本实现

关于原理

关键点:数据劫持、发布者订阅者模式(依赖收集) + Observer、Watcher、Dep

1、有一个监听器Observer,给data及data下的数组对象循环调用Object.defineProperty方法来设置getter和setter,以此来劫持监听data的赋值和取值。

2、有多个订阅者Watcher,在初始化的时候会将自己装进订阅器Dep中(强制get,这样Watcher就能和Dep关联起来)

3、关于监听器Observer的set和get:

① 当读取所需对象的值时,会触发getter函数进行依赖收集,目的是将相关Watcher对象存放到的Dep的subs中,形成的关系如下。

② 在修改对象的值的时候,会触发相应的setter,setter通知之前依赖收集得到的订阅器Dep中的每一个Watcher,这些watcher都依赖着这个值,然后会告诉他们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这中间还有一个 patch和re-render 的过程 (setter-->watcher-update)

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>Two-way-data-binding</title>
</head>

<body>

    <div id="app">
        <input type="text" v-model="text"> {{ text }}
    </div>

    <script>
        // 双向绑定第一步:view--->model

        // 监听器Observer,对vm进行数据劫持
        function observe(obj, vm) {
            Object.keys(obj).forEach(function (key) {
                defineReactive(vm, key, obj[key]);
            })
        }
        function defineReactive(obj, key, val) {
            var dep = new Dep();
            Object.defineProperty(obj, key, {
                get: function () {
                    // 进行依赖收集,会将相关Watcher对象存放到Dep中
                    // update函数里的get方法而来,执行完update方法之后target才为null
                    if (Dep.target) dep.addSub(Dep.target);
                    return val
                },
                set: function (newVal) {
                    if (newVal === val) return
                    val = newVal;
                    // 作为发布者发出通知,通知Dep的每一个Watcher
                    dep.notify();
                }
            });
        }

        // 订阅者Watcher
        function Watcher(vm, node, name, nodeType) {
            Dep.target = this; //指向自己
            this.name = name;
            this.node = node;
            this.vm = vm;
            this.nodeType = nodeType;
            // 执行监听器里的get函数,将自己装进订阅器Dep中,这样就能把Watcher和Dep关联起来
            this.update();
            Dep.target = null;// 当实例化之后,释放自己
        }
        Watcher.prototype = {
            update: function () {
                this.get();
                if (this.nodeType == 'text') {
                    this.node.nodeValue = this.value;
                }
                if (this.nodeType == 'input') {
                    this.node.value = this.value;
                }
            },
            // 获取 data 中的属性值
            get: function () {
                this.value = this.vm[this.name]; // 触发相应属性的 get
            }
        }

        // 订阅器Dep
        function Dep() {
            this.subs = []
        }
        Dep.prototype = {
            addSub: function (sub) {
                this.subs.push(sub);
            },
            notify: function () {
                this.subs.forEach(function (sub) {
                    sub.update();
                });
            }
        }

        // 双向绑定第二步:model-->view

        function nodeToFragment(node, vm) {
            var flag = document.createDocumentFragment();
            var child;
            // appendChild 方法有个隐蔽的地方,就是调用以后 child 会从原来 DOM 中移除
            // 所以,第二次循环时,node.firstChild 已经不再是之前的第一个子元素了
            while (child = node.firstChild) {
                compile(child, vm);//child分别是空白元素、input、{{text}}
                flag.appendChild(child); // 将子节点劫持到文档片段中
            }
            return flag
        }

        function compile(node, vm) {
            // 节点类型为元素
            if (node.nodeType === 1) {
                // 获取元素属性后循环解析
                var attr = node.attributes;
                for (var i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName == 'v-model') {
                        var name = attr[i].nodeValue; // 获取 v-model 绑定的属性名,text
                        node.addEventListener('input', function (e) {
                            // 给相应的 data 属性赋值,进而触发该属性的 set 方法(数据劫持里)
                            vm[name] = e.target.value;
                        });
                        node.value = vm[name]; // 将 data 的值赋给该 node
                        node.removeAttribute('v-model'); //把节点的v-model属性删除
                    }
                };
                new Watcher(vm, node, name, 'input');
            }
            // 节点类型为 text
            if (node.nodeType === 3) {
                var reg = /\{\{(.*)\}\}/;
                if (reg.test(node.nodeValue)) { //  {{text}}
                    var name = RegExp.$1; // 获取匹配到的字符串text
                    name = name.trim();  
                    new Watcher(vm, node, name, 'text');
                }
            }
        }

        function Vue(options) {
            debugger;
            this.data = options.data;
            //view--->model
            observe(this.data, this);//this就是vm
            // model-view
            var id = options.el;
            var dom = nodeToFragment(document.getElementById(id), this);//this就是vm
            // 编译完成后,将 dom 返回到 app 中
            document.getElementById(id).appendChild(dom);
        }

        var vm = new Vue({
            el: 'app',
            data: {
                text: 'hello world你好呀世界'
            }
        })
    </script>
</body>

</html>

思路

Vue 组件挂载时添加响应式的过程。在组件挂载时,会先对所有需要的属性调用 Object.defineProperty(),然后实例化 Watcher,传入组件更新的回调。在实例化过程中,会对模板中的属性进行求值,触发依赖收集。

解析一下上述代码

(一)先是编译流程,model->view

  1. new Vue,执行Vue构造函数,里面是双向绑定的入口函数observe和nodeToFragment

  2. 先执行observer,劫持了get和set方法,注意此时里面的方法还是没执行的

  3. 执行nodeToFragment,循环获取firstChild后依次编译

  4. 编译过程,如果是元素类型的节点,刚开始是input元素
    a、获取元素attributes后循环解析,找出nodeName为v-model的变量
    b、然后刚进入页面时没有input事件的,所以将v-model绑定变量的初始值赋给node.value
    c、执行Watcher方法,初始化的时候执行了update,update里强制执行了vm的get,这样Wacher把自己加入了Dep中,
    d、然后update方法里判断出是input,把input的node.value赋值为刚刚v-model的node.value,这样输入框就默认获取到了Vue的data里的参数了

    编译过程,其次是{{text}}的解析,和解析input一样的步骤,最终是把{{text}}字样也改为刚刚的value值

(二)手动改变输入框的值,view->model
1.执行数据劫持里的set方法,执行Dep的notify方法,遍历每个sub执行update方法。

手写call、apply、bind

一、call

1. 要求
  • 不传入第一个参数,那么上下文默认为 window
  • 改变了 this 指向,让新的对象(比如这里的foo)可以执行该函数,并能接受参数
2. 代码
Function.prototype.myCall = function (context) {
    if (typeof this !== 'function') {
      throw new TypeError('Error')
    }
    context = context || window
    const mySymbol = Symbol() // 利用Symbol而不是直接在context添加属性,保证了不会覆盖已有属性
    context[mySymbol] = this // 这里的this,是xx.Mycall的xx函数
    const args = [...arguments].slice(1)  // 获取第二个参数开始的参数,也就是除了上下文外的参数
    const result = context[mySymbol](...args) // call可以传入多个参数作为调用函数的参数
    delete context[mySymbol]
    return result
}

测试

window.value = "outer"
let foo = {
    value: "inner"
}
let bar = function () {
    console.log("this", this)
    console.log("value", this.value)
    console.log("------------")
}
bar.myCall(foo);  // value为inner
3. 注意点

context[mySymbol] = this,这一步非常关键,用一个变量直接代替 context[mySymbol]是不行的,因为xx.call(yy)是要用yy来执行xx的函数,context里才有yy的上下文,变量是没有的,否则会导致上下文的丢失,直接为window了

let that = context[mySymbol];
that(...args)  // 这样子虽然that和context[mySymbol]指针是一样的,但是此时的上下文是指向context[mySymbol]整体的,而不是context

二、apply

apply接收的第二个参数是数组形式,这是唯一的不同点

Function.prototype.myApply = function (context) {
    if (typeof this !== 'function') {
      throw new TypeError('Error')
    }
    let result;
    context = context || window
    const mySymbol = Symbol()
    context[mySymbol] = this
    if (arguments[1]) {
        result = context[mySymbol](...arguments[1])
    } else {
        result = context[mySymbol]()
    }
    delete context[mySymbol]
    return result
}

三、bind

bind是什么

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数

this.value = 2
var foo = {
    value: 1
}
var bar = function() {
  console.log(this.value)
}
var result = bar.bind(foo)
bar() // 2
result() // 1,即this === foo
参数传递/函数柯里化
this.value = 2
var foo = {
    value: 1
};
var bar = function(name, age, school) {
  console.log(name) // 'An'
  console.log(age) // 22
  console.log(school) // '广东财经大学'
}
var result = bar.bind(foo, 'An') //预置了部分参数'An'
result(22, '广东财经大学') //返回的函数能继续传入参数
bind返回的函数能作为构造函数

(前提里面得有this.xxx才能赋值)

var value = 2;
var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'kevin';

var bindFoo = bar.bind(foo, 'daisy');
// 1
// daisy

var obj = new bindFoo('18'); 
obj.friend === 'kevin'  // 测试new后原型链是否继承父类
// obj == {habit: "shopping"}  obj.friend = 'kevin'
// 实例化的时候也执行了bar函数,此时this是指向bar本身的,所以this.value == undefined,name = "daisy",age=18
自己实现一个bind
// 初始版本,未考虑到原型链,在new一个bind生成的函数的时候,没有继承原函数的原型
Function.prototype.bind2 = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  const _this = this
  const args = [...arguments].slice(1)
  // 返回一个函数
  return function F() {
    // 因为返回了一个函数,我们可以 new F(),所以需要判断
    if (this instanceof F) {
      return _this.apply(this, args.concat(...arguments))
    }
    return _this.apply(context, args.concat(...arguments))
  }
}

// 完全版本
Function.prototype.bind2 = function (oThis) {
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    var aArgs = Array.prototype.slice.call(arguments, 1),
        that = this,  //保存this值,指的调用bind的函数也就是bar
        fNOP = function () { }, //原型式继承的第一步
        fBound = function () {  //要返回的函数等于后面的foo,再次调用时才执行这里面的代码
            //直接bind的话this=window,new了this=fBound,而fBound.__proto__ = FNOP
            return that.apply(this instanceof fNOP ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));
        };
    // 以下两步和上面初始化FNOP相当于原型式继承,等于fbound.prototype = Object.create(this.prototype);
    // 目的是为了在实例化的时候,能让 fBound 构造的实例能够继承绑定函数bar的原型中的值
    fNOP.prototype = that.prototype;
    fBound.prototype = new fNOP();  // fBound.__proto__ = FNOP
    return fBound;
};
var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'kevin';

var bindFoo = bar.bind2(foo, 'daisy');
// 1
// daisy
//当不实例化时,this instanceof FNOP = false,由于是直接执行foo(),相当于window.foo(),所以内部this是window,that是bar
//当实例化后bind出来的新函数后,this是指向本身的,this instanceof FNOP = true,内部this是fBound,而fBound.__proto__ = FNOP,that是bar
var obj = new bindFoo('18'); 
obj.friend === 'kevin'  // 测试new后原型链是否继承父类

Vue源码——nextTick

一、先看看Vue DOM的更新机制

<template>
  <div>
    <div ref="test">{{test}}</div>
    <button @click="handleClick">按钮</button>
  </div>
</template>
export default {
    data () {
        return {
            test: '1'
        };
    },
    methods () {
        handleClick () {
            this.test = '2';
            console.log(this.$refs.test.innerText);//打印“1”
        }
    }
}

以上代码中,当点击这个按钮触发handleClick修改dom来设置值的时候,如果是同步更新DOM的话,输出的1,但是输出的是2,就证明了Vue DOM的更新不是同步的。

Vue官方文档如是说:

可能你还没有注意到,Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MessageChannel,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。

例如,当你设置vm.someData = 'new value',该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数情况我们不需要关心这个过程,但是如果你想在 DOM 状态更新后做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

Vue的数据驱动视图更新是异步的,即修改数据时,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。

二、使用方式和场景

1、使用方式
  • Vue.nextTick( [callback, context] ) 全局方法,可以显示指定上下文
  • vm.$nextTick( [callback] ) 实例方法,自动绑定this到当前实例,原因如下

本质上都是调用了同一个方法,绑定时候的源码如下:

export function renderMixin (Vue: Class<Component>) {
  // 省略...
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
  // 省略...
}

然后$nextTick 方法是在renderMixin 函数中挂载到Vue原型上的,$nextTick只是对nextTick函数的简单包装。但是会传入一个this,所以在使用实例方法$nextTick时,可以不指定上下文

2、场景
  • 从服务端接口去获取数据的时候,数据做了修改,如果我们的某些方法去依赖了数据修改后的DOM变化,我们就必须在nextTick后执行。如果不这样就会获取不到更新后的dom
getData(res).then(()=>{
  this.xxx = res.data
  this.$nextTick(() => {
    // 这里我们可以获取变化后的 DOM
  })
})

三、然后再来看看源码(vue/src/core/util/next-tick.js)

去年Vue 2.5开始就单独抽出一个文件了,然后这个是2019.1.24最新更新的代码,2.6.7

/* @flow */                // 静态类型检测

import { noop } from 'shared/util'   // 空函数
import { handleError } from './error' 
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false  // 是否使用了microTask

const callbacks = []  // 存储所有需要执行的回调函数
let pending = false  // 用来标志是否正在执行回调函数

// 用来执行callbacks里存储的所有回调函数。
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0  // 保存回调函数后清空
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

let timerFunc // 宿主环境不同,可能会是以下四种的其中一种

// 检查宿主环境是否支持promise,如果支持,则利用promise来触发执行回调函数;
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)  // IOS兼容,虽然能触发微任务,但是这个微任务不会更新,需要浏览器做一些事情比如注册一个macrotask,即使这个macrotask什么都不做
  }
  isUsingMicroTask = true
  // 如果支持MutationObserver(HTML5中的新API,是个用来监视DOM变动的接口),则实例化一个观察者对象,观察文本节点发生变化时,触发执行所有回调函数。
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks) // 得到实例,里面的回调函数会在实例监听的dom发生改动时触发
  const textNode = document.createTextNode(String(counter))  // 创建一个观测对象
  observer.observe(textNode, {
    characterData: true  //说明监听的dom文本内容有修改
  })
  timerFunc = () => {
    // 修改counter,让实例监听到dom变化,进而执行回调
    // 归根到底就是nextTick想借用这个异步API来执行回调,也就是利用了MutationObserver的回调会被当作microTask来执行
    counter = (counter + 1) % 2  
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
// 检测是否支持setImmediate,此时isUsingMicroTask是false,表示此时所有的回调都注册为宏任务了
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
// 都不支持的话就调用setTimeout,此时isUsingMicroTask是false,表示此时所有的回调都注册为宏任务了
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
// 之所以是以上的顺序,是因为执行优先级、以及性能

 /*
    推送到队列中下一个tick时执行
    cb 回调函数
    ctx 上下文
  */
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 传入的cb会被push进callbacks中存放起来
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 先判断是否正在执行回调,如果不在执行,就将pengding设为true,然后执行timerFunc
  if (!pending) {
    pending = true
    timerFunc()
  }
  // 这是当 nextTick 不传 cb 参数的时候,提供一个 Promise 化的调用,比如:nextTick().then(() => {})
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

源码思路总结

  • cb 函数经处理压入 callbacks 数组,执行 timerFunc 函数,延迟调用 flushCallbacks 函数,遍历执行 callbacks 数组中的所有函数。
  • nextTick这个函数的作用是在异步任务macrotask或者microtask中推入一个timerFunc,目的利用异步任务的特性,让回调函数延迟到当前调用栈执行完以后执行
  • 至于这个timerFunc可能会有几种形式,微任务对应的promise、MutationObserver,宏任务对应的setImmediate、setTImeout

2.4、2.5、2.6差距

官方文档做出过如下解释
image
在Vue 2.5之前的版本中,nextTick几乎都是基于microTask实现的(大概是大部分宿主环境都是比较高级了,所以默认都是Promise了?)但是由于microTask的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,就会导致一些诡异的问题;但是如果全部都改成macroTask,对一些有重绘和动画的场景也会有性能的影响。

所以最终在2.5的时候nextTick采取的策略是默认走microTask,对于一些DOM的交互事件,如v-on绑定的事件回调处理函数的处理,会强制走macroTask。

具体做法是,在Vue绑定DOM事件时,默认会给回调函数调用withMacroTask方法做一层包装,从而保证整个回调函数的执行过程中,遇到数据改变导致的DOM更新相关的任务都会被推到macroTask。对于 macro task 的执行,Vue.js 优先检测是否支持原生 setImmediate,不支持的话就会降级为 setTimeout 0。代码如下,这段代码也是在同一个文件的。

然鹅最终在2.6.7的时候,又把这层包装删了,估计是用了其他方式处理吧
image

HOC、Render Props、Hooks 三种逻辑复用方案 - 栗子

目的:共享组件之间的状态和逻辑,提高可复用性

一、HOC

封装HOC

import React, { Component } from 'react'

export default (WrappedComponent, name) => {
  class NewComponent extends Component {
    constructor () {
      super()
      this.state = { data: null }
    }

    componentWillMount () {
      let data = localStorage.getItem(name)
      this.setState({ data })
    }

    render () {
      return <WrappedComponent data={this.state.data} />
    }
  }
  return NewComponent
}

初始化工作:现在 NewComponent 会根据第二个参数 name 在挂载阶段从 LocalStorage 加载数据,并且 setState 到自己的 state.data 中,而渲染的时候将 state.data 通过 props.data 传给 WrappedComponent(即子组件)。

使用该HOC

假设上面的代码是在 src/wrapWithLoadData.js 文件中的,我们可以在别的地方这么用它:

import wrapWithLoadData from './wrapWithLoadData'

class InputWithUserName extends Component {
  render () {
    return <input value={this.props.data} />
  }
}

InputWithUserName = wrapWithLoadData(InputWithUserName, 'username')
export default InputWithUserName

这里的this.props来自HOC即wrapWithLoadData,父子组件的关系!子组件wrapWithLoadData 接收了父组件的data参数

InputWithUserName组件作为参数传入wrapWithLoadData,渲染的出来的就是InputWithUserName里的render部分,即<input xxx,HOC的render部分相当于父组件一样,起到传递参数的作用

挂载的时候从 LocalStorage 里面加载 username 字段作为 的 value 值

二、Render Props

一种在 React 组件之间使用一个值为函数的 prop 共享代码技术,render prop 是一个用于告知组件需要渲染什么内容的函数类型的prop。(由父组件告知)

1、可变数据源(还不知道自己要渲染什么东西)

class List extends React.Component {
  constructor(props) {
    super(props);
    this.handleChange = this.handleChange.bind(this);
    this.state = {
      list: props.getList()
    };
  }

  handleChange() {
    // 当数据源更新时,更新组件状态
    this.setState({
      list: props.getList()
    });
  }

  render() {
    return (
      <div>
        {this.props.render(this.state)} // 调用父组件的render方法
      </div>
    );
  }
}

这个render就是我们前面说的“一个值为函数的属性”,叫啥名儿都行,叫render是凑巧了,子组件只负责把“可变数据源”即this.state作为参数给render函数,至于渲染的内容是什么?那就不是子组件能控制了,这个render是调用父组件的render方法

2、如何调用这货

class Page extends React.Component {
  //...
  render() {
    return (
        <List getList={DataSource.getItems}
              render={list=>(
                <div>{list.map(item=>(<Item item={item} key={item.id} />))} </div>
             )} /> 
    );
  }
}

List组件的this.props.render调用的也就是这个 list=>(<div>{list.map(item=>())} </div>),这个list参数的来源即List组件里的this.state

三、 Hook

不编写 class 的情况下使用 state 以及其他的 React 特性(之前的函数式组件无法拥有自己的状态),通过自定义 Hook,可以将组件逻辑提取到可重用的函数中。

1、自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook。

import React, { useState, useEffect } from 'react';

function useData(getData) {
  // useState 会返回一对值:当前状态和一个让你更新它的函数
  const [list, setList] = useState([]);
  useEffect(() => {
    function handleChange() {
      setList(getData());
    }

    DataSource.addChangeListener(this.handleChange);
    return () => {
      DataSource.removeChangeListener(this.handleChange);
    };
  });
  }
  return list;

2、使用Hook

function ItemList(props) {
  const list = useData(DataSource.getItems);
  return (
    <div>
    {list.map(item=>(<Item item={item} key={item.id}/>))}
    </div>
  )
}

执行上下文(栈)、执行栈、变量对象、堆栈内存等

一、执行上下文

分类

1、全局执行上下文

有且只有一个window

2、函数执行上下文

每次调用函数时创建

3、Eval函数执行上下文

运行在eval函数中的代码,不建议使用

执行上下文的创建

1、确定this的值

2、LexicalEnvironment(词法环境)组件被创建

  • 词法环境的组成部分
    • 环境记录:存储变量和函数声明
    • 对外部环境的引用
  • 词法环境的两种类型
    • 全局环境
    • 函数环境

3、VariableEnvironment(变量环境) 组件被创建

变量环境类似词法环境,属性和词法环境一致,在ES6中 的话,区别在于词法环境存储函数声明和变量(let const),而变量环境仅用于存储变量(var)

let a = 20;  
const b = 30;  
var c;

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}
c = multiply(20, 30);

对应的执行上下文的创建阶段

// 全局执行上下文
GlobalExectionContext = {  
  ThisBinding: <Global Object>,  // 1.确定this
  LexicalEnvironment: {          // 2.词法环境 
    EnvironmentRecord: {         //   2.1环境记录
      Type: "Object",  
      // 标识符绑定在这里  
      a: < uninitialized >,  
      b: < uninitialized >,  
      multiply: < func >  
    }  
    outer: <null>               //   2.2 对外部环境的引用
  },

  VariableEnvironment: {       //  3.变量环境
    EnvironmentRecord: {       //    3.1环境记录
      Type: "Object",  
      // 标识符绑定在这里  
      c: undefined,  
    }  
    outer: <null>            //      3.2对外部环境的引用
  }  
}

// 函数上下文,类似上
FunctionExectionContext = {  
   
  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      Arguments: {0: 20, 1: 30, length: 2},  
    },  
    outer: <GlobalLexicalEnvironment>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}

二、执行栈

执行栈,也叫调用栈,用于存储在代码执行期间创建的所以执行上下文,LIFO(后进先出)

运行JS代码时,会创建一个全局执行上下文,然后push到执行栈中,以后每次函数调用,引擎都会为这个函数创建一个函数执行上下文到当前执行栈的栈顶

当栈顶函数执行完后,其对应的函数执行上下文会从执行栈中pop出,上下文的控制权将转移到下一个执行上下文

三、函数上下文、AO、VO

在函数上下文中,用活动对象AO表示变量对象VO,变量对象不能被访问,当进入到一个执行上下文后,这个VO才会被激活,成为AO,这时各种属性函数才能被访问

四、堆、栈、队列

简介

  • 堆:类似于二叉树,是树状结构,它的存储方式类似key:value
  • 栈:LIFO后进先出,上下文基础
  • 队列:FIFO先进先出,是事件循环的基础结构

变量的存放

1、基本类型

保存在栈内存中(除Object外的6种)

2、引用类型

保存在堆内存中,因为引用类型地址大小固定,但是值大小不固定,所以栈存放引用类型的地址,然后根据地址从堆中读取值
image

手写Promise A+(逐行分析)

/**
 * Promise 实现 遵循promise/A+规范
 * Promise/A+规范译文:
 * https://malcolmyu.github.io/2015/06/12/Promises-A-Plus/#note-4
 */

// promise 三个状态
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";

function Promise(excutor) {
    let that = this; // 缓存当前promise实例对象
    that.status = PENDING; // 初始状态
    that.value = undefined; // fulfilled状态时 返回的信息
    that.reason = undefined; // rejected状态时 拒绝的原因
    that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数
    that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数

    // value为成功态时接收的终值
    function resolve(value) {
        // 如果value是Promise,则再次执行then 
        if (value instanceof Promise) {
            return value.then(resolve, reject);
        }

        // 规范规定要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
        setTimeout(() => {
            // 调用resolve 回调对应onFulfilled函数
            if (that.status === PENDING) {
                // 只能由pending状态 => fulfilled状态 (避免调用多次resolve reject)
                that.status = FULFILLED;
                that.value = value;
                that.onFulfilledCallbacks.forEach(cb => cb(that.value));
            }
        });
    }

    // reason失败态时接收的拒因
    function reject(reason) {
        setTimeout(() => {
            // 调用reject 回调对应onRejected函数
            if (that.status === PENDING) {
                // 只能由pending状态 => rejected状态 (避免调用多次resolve reject)
                that.status = REJECTED;
                that.reason = reason;
                that.onRejectedCallbacks.forEach(cb => cb(that.reason));
            }
        });
    }

    // 捕获在excutor执行器中抛出的异常,栗子如下
    // new Promise((resolve, reject) => {
    //     throw new Error('error in excutor')
    // })
    try {
        excutor(resolve, reject);
    } catch (e) {
        reject(e);
    }
}

/**
 * resolve中的值几种情况:
 * 1.普通值
 * 2.promise对象
 * 3.thenable对象/函数,有then方法的函数或对象
 * 2和3统一处理
 */

/**
 * 用来处理then方法返回结果包装成promise 方便链式调用
 * 对resolve 进行改造增强 针对resolve中不同值情况 进行处理
 * @param  {promise} promise2 promise1.then方法返回的新的promise对象
 * @param  {[type]} x         promise1中onFulfilled或onRejected的返回值
 * @param  {[type]} resolve   promise2的resolve方法,传进来方便更改promise最后的状态
 * @param  {[type]} reject    promise2的reject方法,传进来方便更改promise最后的状态
 */
function resolvePromise(promise2, x, resolve, reject) {
    // 如果从onFulfilled中返回的x 就是promise2本身 就会导致循环引用报错,自己等待自己,一辈子等不完
    if (promise2 === x) {
        return reject(new TypeError('循环引用'));
    }
      // 用来记录promise2的状态改变,一旦发生改变了 就不允许 再改成其他状态  成功和失败只能只能调用一个  限制既调resolve,也调reject
      // 主要是调用then方法的时候用的,和递归没什么关系   
    let called = false;
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        // 如果x是一个对象或者函数 那么他就有可能是promise 需要注意 null typeof也是 object 所以需要排除掉
        // 先获得x中的then 如果这一步发生异常了,那么就直接把异常原因reject掉
        try {
            let then = x.then;
            if (typeof then === 'function') {
                // 如果then是个函数 那么就调用then 并且把成功回调和失败回调传进去;如果then是函数,就默认当作是promise了
                // 如果此时x是一个promise 并且最终状态时成功,那么就会执行成功的回调,如果失败就会执行失败的回调如果失败了,就把失败的原因reject出去,作为promise2的失败原因,直到成功了就取成功的value
                // 以x,也就是Promise作为上下文,value为成功的值,开始执行then方法,这个value有可能仍然是promise,所以需要递归调用resolvePromise这个方法 直达返回值不是一个promise
                // 至于为什么用call,因为then.xxx时,this是window而不是Promise本身了
                then.call(x, value => {
                    if (called) return;
                    called = true;
                    resolvePromise(promise2, value, resolve, reject)
                }, error => {
                    if (called) return
                    called = true;
                    reject(error)
                })
            } else {
                resolve(x)
            }
        } catch (error) {
            if (called) return
            called = true;
            reject(error)
        }
    } else {
        // 如果是一个普通值 那么就直接把x作为promise2的成功value resolve掉
        resolve(x)
    }
}
/**
 * [注册fulfilled状态/rejected状态对应的回调函数]
 * @param  {function} onFulfilled fulfilled状态时 执行的函数
 * @param  {function} onRejected  rejected状态时 执行的函数
 * @return {function} newPromsie  返回一个新的promise对象
 */
Promise.prototype.then = function (onFulfilled, onRejected) {
    const that = this;
    let newPromise;
    // 处理参数默认值 保证参数后续能够继续执行(处理值穿透)
    onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value;
    onRejected = typeof onRejected === "function" ? onRejected : reason => { throw reason; };

    // then里面的FULFILLED/REJECTED状态时 为什么要加setTimeout ?
    // 原因:
    // 其一 2.2.4规范 要确保 onFulfilled 和 onRejected 方法异步执行(且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行) 所以要在resolve里加上setTimeout
    // 其二 2.2.6规范 对于一个promise,它的then方法可以调用多次.(当在其他程序中多次调用同一个promise的then时 由于之前状态已经为FULFILLED/REJECTED状态,则会走的下面逻辑),所以要确保为FULFILLED/REJECTED状态后 也要异步执行onFulfilled/onRejected

    // 其二 2.2.6规范 也是resolve函数里加setTimeout的原因
    // 总之都是 让then方法异步执行 也就是确保onFulfilled/onRejected异步执行

    // 如下面这种情景 多次调用p1.then
    // p1.then((value) => { // 此时p1.status 由pending状态 => fulfilled状态
    //     console.log(value); // resolve
    //     // console.log(p1.status); // fulfilled
    //     p1.then(value => { // 再次p1.then 这时已经为fulfilled状态 走的是fulfilled状态判断里的逻辑 所以我们也要确保判断里面onFuilled异步执行
    //         console.log(value); // 'resolve'
    //     });
    //     console.log('当前执行栈中同步代码');
    // })
    // console.log('全局执行栈中同步代码');
    //

    if (that.status === FULFILLED) { // 成功态
        return newPromise = new Promise((resolve, reject) => {
            setTimeout(() => {
                try {
                    let x = onFulfilled(that.value);
                    resolvePromise(newPromise, x, resolve, reject); // 新的promise resolve 上一个onFulfilled的返回值
                } catch (e) {
                    reject(e);
                }
            });
        })
    }

    if (that.status === REJECTED) { // 失败态
        return newPromise = new Promise((resolve, reject) => {
            setTimeout(() => {
                try {
                    let x = onRejected(that.reason);
                    resolvePromise(newPromise, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
        });
    }

    if (that.status === PENDING) { // 等待态
        // 当异步调用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中
        return newPromise = new Promise((resolve, reject) => {
            that.onFulfilledCallbacks.push((value) => {
                try {
                    let x = onFulfilled(value);
                    resolvePromise(newPromise, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
            that.onRejectedCallbacks.push((reason) => {
                try {
                    let x = onRejected(reason);
                    resolvePromise(newPromise, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
        });
    }
};

/**
 * Promise.all Promise进行并行处理
 * 参数: promise对象组成的数组作为参数
 * 返回值: 返回一个Promise实例
 * 当这个数组里的所有promise对象全部变为resolve状态的时候,才会resolve。
 */
Promise.all = function (promises) {
    return new Promise((resolve, reject) => {
        let done = gen(promises.length, resolve);
        promises.forEach((promise, index) => {
            promise.then((value) => {
                done(index, value)
            }, reject)
        })
    })
}

function gen(length, resolve) {
    let count = 0;
    let values = [];
    return function (i, value) {
        values[i] = value;
        if (++count === length) {
            console.log(values);
            resolve(values);
        }
    }
}

/**
 * Promise.race
 * 参数: 接收 promise对象组成的数组作为参数
 * 返回值: 返回一个Promise实例
 * 只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理(取决于哪一个更快)
 */
Promise.race = function (promises) {
    return new Promise((resolve, reject) => {
        promises.forEach((promise, index) => {
            promise.then(resolve, reject);
        });
    });
}

// 用于promise方法链时 捕获前面onFulfilled/onRejected抛出的异常
Promise.prototype.catch = function (onRejected) {
    return this.then(null, onRejected);
}

Promise.resolve = function (value) {
    return new Promise(resolve => {
        resolve(value);
    });
}

Promise.reject = function (reason) {
    return new Promise((resolve, reject) => {
        reject(reason);
    });
}

/**
 * 基于Promise实现Deferred的
 * Deferred和Promise的关系
 * - Deferred 拥有 Promise
 * - Deferred 具备对 Promise的状态进行操作的特权方法(resolve reject)
 *
 *参考jQuery.Deferred
 *url: http://api.jquery.com/category/deferred-object/
 */
Promise.deferred = function () { // 延迟对象
    let defer = {};
    defer.promise = new Promise((resolve, reject) => {
        defer.resolve = resolve;
        defer.reject = reject;
    });
    return defer;
}

/**
 * Promise/A+规范测试
 * npm i -g promises-aplus-tests
 * promises-aplus-tests Promise.js
 */

try {
    module.exports = Promise
} catch (e) {
}

Promise测试

npm i -g promises-aplus-tests
promises-aplus-tests index.js

屏幕快照 2019-11-24 下午3.47.11.png

理一遍promise,加简单的实现(非Promise A+)

promise的理解

  • 异步编程方法之一,通过链式调用的方式来解决回调地狱,特别是在异步过程中,可以通过Promise可以保证代码的整洁性和可读性。
  • promise对象是一个构造函数,所以使用时需要new实例化
  • promise有三种状态,pending、fulfilled、reject,pending可以转化为fulfilled或rejected,一旦状态为fulfilled或reject,状态就不能再改变了。如果成功会提供一个value用来存储成功的值、失败会提供一个reason存储失败的原。
  • 一个promise常用的方法有then、catch、all等等

手撕promise最基本的要求

// 定义三种状态、resolve和reject方法、then方法(onFulfilled、onRejected)
function myPromise(constructor) {
    this.status = "pending" //定义初始状态
    this.value = undefined;//存储成功后的值
    this.reason = undefined;//存储失败的原因
    let resolve = value => {
        if (this.status === "pending") {
            this.status = "resolved";
            this.value = value;
        }
    }
    let reject = reason => {
        if (this.status === "pending") {
            this.status = "rejected";
            this.reason = reason;
        }
    }
    //捕获构造异常
    try {
        constructor(resolve, reject);
    } catch (e) {
        reject(e);
    }
}

myPromise.prototype.then = function (onFulfilled, onRejected) {
    // 状态为fulfilled,执行onFulfilled,传入成功的值
    if (this.status === 'resolved') {
        onFulfilled(this.value);
    };
    // 状态为rejected,执行onRejected,传入失败的原因
    if (this.status === 'rejected') {
        onRejected(this.reason);
    };
}

var p = new myPromise(function (resolve, reject) { resolve(1) });
console.log(p)  //结果是返回一个构造函数,myPromise {status: "resolved", value: 1, reason: undefined}reason: undefinedstatus: "resolved"value: 1__proto__: Object
p.then(function (x) { console.log(x) })

支持异步resolve

比如setTimeout里resolve,这里需要用2个数组onFullfilledArray和onRejectedArray来保存异步的方法。在状态发生改变时,一次性遍历执行数组中的方法。

// 定义三种状态、resolve和reject方法、then方法(onFulfilled、onRejected)
// 支持异步resolve.

function myPromise(constructor) {
    this.status = "pending" //定义初始状态
    this.value = undefined;//存储成功后的值
    this.reason = undefined;//存储失败的原因

    // 成功存放的数组
    this.onResolvedCallbacks = [];
    // 失败存放法数组
    this.onRejectedCallbacks = [];

    let resolve = value => {
        if (this.status === "pending") {
            console.log(1)
            this.status = "resolved";
            this.value = value;
            // 一旦resolve执行,调用成功数组的函数
            this.onResolvedCallbacks.forEach(fn => fn());
            console.log(2)
        }
    }
    let reject = reason => {
        if (this.status === "pending") {
            this.status = "rejected";
            this.reason = reason;
            // 一旦reject执行,调用失败数组的函数
            this.onRejectedCallbacks.forEach(fn => fn());
        }
    }
    //捕获构造异常
    try {
        constructor(resolve, reject);
    } catch (e) {
        reject(e);
    }
}

myPromise.prototype.then = function (onFulfilled, onRejected) {
    // 状态为fulfilled,执行onFulfilled,传入成功的值
    if (this.status === 'resolved') {
        onFulfilled(this.value);
    };
    // 状态为rejected,执行onRejected,传入失败的原因
    if (this.status === 'rejected') {
        onRejected(this.reason);
    };
    // 当状态status为pending时
    if (this.status === 'pending') {
        // onFulfilled传入到成功数组
        this.onResolvedCallbacks.push(() => {
            onFulfilled(this.value);
        })
        // onRejected传入到失败数组
        this.onRejectedCallbacks.push(() => {
            onRejected(this.reason);
        })
    }
}

// ------------------------------------------
// 如果按版本1,那么主线程上的同步任务也就是3和4是已经执行完了,最后只是执行了resolve方法,then方法未执行
// 所以用两个数组来临时存储下异步的方法,刚开始也是直接执行then,但是这次全给添加到数组里去了,当定时器时间到了执行resolve方法时,直接执行数组里的方法即可
var p2 = new myPromise(function (resolve, reject) { setTimeout(function () { resolve(22) }, 2000) });
p2.then(function (x) { console.log("x",x) })  //22
console.log("p2", p2)

canvas基础、图像处理

一、canvas基本操作

1. 同状态线条 (moveTo、lineTo)

let myCanvas = document.getElementById("myCanvas");
myCanvas.width = "500";
myCanvas.height = "300";

var ctx = myCanvas.getContext("2d");
//画三角形
ctx.moveTo(50, 50);
ctx.lineTo(250, 100);
ctx.lineTo(50, 200);
ctx.lineTo(50, 50);

//画直线
ctx.moveTo(350, 50);
ctx.lineTo(350, 200);

//定义画线样式
ctx.strokeStyle = "red";
ctx.lineWidth = "5";

ctx.lineCap = "round"; // 线条两端的形状
ctx.lineJoin = "miter" // 线条交界处形状

ctx.stroke();

2. 不同状态线条、封闭图形、填充 (beginPath、closePath、fill)

ctx.beginPath();  // 第一个状态的beginPath可省略
ctx.moveTo(50, 50); // 有beginPath可改成lineTo()
ctx.lineTo(250, 100);
ctx.lineTo(50, 200);
ctx.lineTo(50, 50); // 用了closePath可省此句 
ctx.closePath() // 绘制封闭图形时closePath可避免粗线条时显示问题
// ctx.fillStyle = "blue";
// ctx.fill();  // 填充
ctx.strokeStyle = "red";
ctx.stroke();

ctx.beginPath();
ctx.moveTo(350, 50);
ctx.lineTo(350, 200);
ctx.strokeStyle = "yellow";
ctx.stroke();

3. 规则图形

1)矩形

  • fillRect( x , y , width , height) //填充以(x,y)为起点宽高分别为width、height的矩形 默认为黑色
  • stokeRect( x , y , width , height) //绘制一个空心以(x,y)为起点宽高分别为width、height的矩形
  • clearRect( x, y , width , height ) // 清除以(x,y)为起点宽高分别为width、height的矩形 为透明

2)圆弧

  • arc( x , y , r , startAngle , endAngle , anticlosewise ) // 以(x,y)为圆心 r为半径的圆 绘制startAngle弧度 到endAngle弧度的圆弧 anticlosewise默认为false 即顺时针方向 true为逆时针方向
  • arcTo( x1 , y1 , x2 , y2 , radius ) //根据 两个控制点 (x1,y1) 和 (x2, y2)以及半径绘制弧线 同时连接两个控制点

二、canvas图像处理

1. 原图

  • drawImage(image,dx,dy)
  • drawImage(image,dx,dy,dw,dh)
  • drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight),s是source、d是destination,表示从这张图片的(sx, sy)坐标开始,取长为sWidth,sHeight的一部分图片,将图片渲染到画布的(dx,dy)坐标,在画布上渲染的实际长度为dWidth, 实际高度为dHeight。
let image = new Image();    
image.src = "img/img.png";
image.onload = function() {
  context.drawImage(image, 0, 0);
}

2. 图像缩放逻辑

function drawImageByScale(scale) {
    let imageWidth = 1152 * scale
    let imageHeight = 768 * scale
    // 计算缩放后图片的起始点,如果是放大的,那么<0,如果小到溢出画布的部分不会被显示
    x = canvas.width / 2 - imageWidth / 2
    y = canvas.height / 2 - imageHeight / 2
    // 先清除画布,然后进行绘制缩放后的图片
    context.clearRect(0, 0, canvas.width, canvas.height)
    context.drawImage(image, x, y, imageWidth, imageHeight)
}

3. 离屏canvas

将第二个canvas加载到第一个canvas,第二个通常不可见

1) 图片水印逻辑

function drawImage(image, scale) {
    imageWidth = 1152 * scale
    imageHeight = 768 * scale
    x = canvas.width / 2 - imageWidth / 2
    y = canvas.height / 2 - imageHeight / 2
    context.clearRect(0, 0, canvas.width, canvas.height)
    // 绘制原图
    context.drawImage(image, x, y, imageWidth, imageHeight)
    // 绘制水印
    context.drawImage(watermarkCanvas, canvas.width - watermarkCanvas.width,
        canvas.height - watermarkCanvas.height)
}

2)放大镜(和鼠标的交互)

  • 处理各种鼠标事件:滑入滑出、点击松开
  • 计算各种坐标
<canvas id="canvas" style="display:block;margin:0 auto;border:1px solid #aaa;">
    您的浏览器尚不支持canvas
</canvas>
<canvas id="offCanvas" style="display: none"></canvas>
    
// 主canvas
let canvas = document.getElementById("canvas")
let context = canvas.getContext("2d")
// 离屏canvas
let offCanvas = document.getElementById("offCanvas")
let offContext = offCanvas.getContext("2d")

let image = new Image()
let isMouseDown = false
let scale

window.onload = function () {
    canvas.width = 1152
    canvas.height = 768

    image.src = "img-lg.jpg"
    image.onload = function () {
        offCanvas.width = image.width
        offCanvas.height = image.height
        scale = offCanvas.width / canvas.width
        context.drawImage(image, 0, 0, canvas.width, canvas.height)
        offContext.drawImage(image, 0, 0)
    }
}

// 返回鼠标相对于canvas左上角的坐标
function windowToCanvas(x, y) {
    let bbox = canvas.getBoundingClientRect()
    return { x: x - bbox.left, y: y - bbox.top }
}

// 鼠标按下
canvas.onmousedown = function (e) {
    e.preventDefault()
    isMouseDown = true
    point = windowToCanvas(e.clientX, e.clientY)
    drawCanvasWithMagnifier(true, point)
}

// 鼠标松开
canvas.onmouseup = function (e) {
    e.preventDefault()
    isMouseDown = false
    drawCanvasWithMagnifier(false)
}

// 鼠标移出canvas区域
canvas.onmouseout = function (e) {
    e.preventDefault()
    isMouseDown = false
    drawCanvasWithMagnifier(false)
}

// 鼠标在canvas区域内移动
canvas.onmousemove = function (e) {
    e.preventDefault()
    if (isMouseDown == true) { // 即为按下同时滑动
        point = windowToCanvas(e.clientX, e.clientY)
        drawCanvasWithMagnifier(true, point)
    }
}

// 在离屏canvas上绘制放大镜
function drawCanvasWithMagnifier(isShowMagnifier, point) {
    // 先在主canvas上清除一下,然后绘制原图
    context.clearRect(0, 0, canvas.width, canvas.height)
    context.drawImage(image, 0, 0, canvas.width, canvas.height)
    if (isShowMagnifier === true) {
        drawMagnifier(point)
    }
}

function drawMagnifier(point) {
    let mr = 200  // 放大镜半径
    let imageLG_cx = point.x * scale
    let imageLG_cy = point.y * scale
    // 缩放后的x、y值减去半径,得到离屏canvas的开始坐标
    let sx = imageLG_cx - mr 
    let sy = imageLG_cy - mr
    // canvas的开始坐标
    let dx = point.x - mr
    let dy = point.y - mr

    context.save() // 保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作

    context.lineWidth = 10.0
    context.strokeStyle = "#069"

    context.beginPath()
    // 点击的当前坐标为圆心绘制圆形放大镜
    context.arc(point.x, point.y, mr, 0, Math.PI * 2, false)
    context.stroke()
    context.clip()// 裁剪区域,设定裁剪区域后,只有在区域内的图像才能显示,其余部分会被屏蔽掉
    // 将offCanvas上起点为(sx,sy)的区域----渲染到---->canvas上起点为(dx,dy)的区域  
   // 至于*2,是因为mr只是半径
   context.drawImage(offCanvas, sx, sy, 2 * mr, 2 * mr, dx, dy, 2 * mr, 2 * mr)

    context.restore()  // 用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响
}

4. 像素级处理

  • getImageData(sx, sy, sw, sh),起始点为(sx, sy)、宽为sw、高为sh的图像信息,返回值为ImageData对象
  • putImageData(imagedata, dx, dy, dirtyX, dirtyY, dirtyWidth, dirtyHeight); 将ImageData对象填写到对应canvas中;ImageData对象左上角坐标dx、dy;在画布上放置图像的坐标dirtyX、dirtyY;在画布上绘制图像的宽高;(最终图像起点坐标是x+dirtyX,y+dirtyY)

灰度滤镜(其他滤镜操作类似,图像算法不同)

function greyEffect() {
    var imageData = contexta.getImageData(0, 0, canvasa.width, canvasa.height)
    var pixelData = imageData.data
    for (var i = 0; i < canvasb.width * canvasb.height; i++) {
        var r = pixelData[i * 4 + 0]
        var g = pixelData[i * 4 + 1]
        var b = pixelData[i * 4 + 2]
        var grey = r * 0.3 + g * 0.59 + b * 0.11
        pixelData[i * 4 + 0] = grey
        pixelData[i * 4 + 1] = grey
        pixelData[i * 4 + 2] = grey
    }
    contextb.putImageData(imageData, 0, 0, 0, 0, canvasb.width, canvasb.height)
    context.putImageData()
}

事件循环 - 经典栗子🌰

栗子1

console.log(1)
setTimeout(function() {
  //settimeout1
  console.log(2)
}, 0);
const intervalId = setInterval(function() {
  //setinterval1
  console.log(3)
}, 0)
setTimeout(function() {
  //settimeout2
  console.log(10)
  new Promise(function(resolve) {
    //promise1
    console.log(11)
    resolve()
  })
  .then(function() {
    console.log(12)
  })
  .then(function() {
    console.log(13)
    clearInterval(intervalId)
  })
}, 0);

//promise2
Promise.resolve()
  .then(function() {
    console.log(7)
  })
  .then(function() {
    console.log(8)
  })
console.log(9)

1 - 9 - 7 - 8 - 2 - 3 - 10 - 11 - 12 -13

第一次事件循环:

  • console.log(1)被执行,输出1
  • settimeout1执行,加入macrotask队列
  • setinterval1执行,加入macrotask队列
  • settimeout2执行,加入macrotask队列
  • promise2执行,.then是同步方法, 它的两个then函数加入microtask队列
  • console.log(9)执行,输出9
  • 根据事件循环的定义,接下来会执行新增的microtask任务,按照进入队列的顺序,执行console.log(7)和console.log(8),输出7和8
    microtask队列为空,回到第一步,进入下一个事件循环,此时macrotask队列为: settimeout1,setinterval1,settimeout2

第二次事件循环:

  • 从macrotask队列里取位于队首的任务(settimeout1)并执行,输出2
    microtask队列为空,回到第一步,进入下一个事件循环,此时macrotask队列为: setinterval1,settimeout2

第三次事件循环:

  • 从macrotask队列里取位于队首的任务(setinterval1)并执行,输出3,然后又将新生成的setinterval1加入macrotask队列
    microtask队列为空,回到第一步,进入下一个事件循环,此时macrotask队列为: settimeout2,setinterval1

第四次事件循环:

  • 从macrotask队列里取位于队首的任务(settimeout2)并执行,输出10,并且执行new Promise内的函数(new Promise内的函数是同步操作,并不是异步操作),输出11,并且将它的两个then函数加入microtask队列
    从microtask队列中,取队首的任务执行,直到为空为止。因此,两个新增的microtask任务按顺序执行,输出12和13,并且将setinterval1清空。

栗子2

async function async1() {
    console.log('async1 start');
    await async2();
    console.log('async1 end');
}
async function async2() {
    console.log('async2');
}
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

还原该语法糖后

async function async1() {
    console.log('async1 start');
    Promise.resolve(async2()).then(()=>console.log('async1 end'))
}
async function async2() {
    console.log('async2');
}
async1();
new Promise(function(resolve) {
    console.log('promise1');
    resolve();
}).then(function() {
    console.log('promise2');
});
console.log('script end');

结果:

/** 
 * async1 start
 * async2
 * promise1
 * script end
 * async1 end (上一个tip被加入microTask)
 * promise2 (上一个tip被加入microTask)
 * */

await会产生一个微任务(Promise.then是微任务)

  • async1 start是由async1()执行的,输出async1 start,执行到await时,进入async2函数输出async2,把await后面的挂入下一个循环(因为底层就是后面那部分被加入then微任务了),进入下一个循环
  • 执行同步任务的new Promise,输出promise1,把then后面的挂入微任务
  • 输出script end
  • 执行微任务队列,async1 end, promise2

Redux的异步和中间件

一、背景

Action发出后,Reducer立刻计算出State,这是同步。

但如果需要在Action发出后(即store.dispatch())过一段时间再执行Reducer(即进入Store),这就是异步了。此时就需要用到中间件middleware

二、关于中间件

中间件就是一个函数,对store.dispatch方法进行了改造,在 发出 Action执行 Reducer 这两步之间,添加了其他功能。

  • 此前没有中间件时,Action只能是一个对象,dispatch处理后传给了Reducer
  • 有中间件后,Action可以是一个函数,因为中间件升级后的dispatch支持Action是函数,此时不会直接传给Reducer,而是会等函数执行完后再传过去

image

三、常见的中间件

  • redux-thunk:action可以是一个函数,用来发起异步请求。
  • redux-promise:action可以是一个promise对象,用来更优雅的进行异步操作。
  • redux-logger:action就是一个标准的plain object,用来记录action和nextState的。

四、redux-thunk中间件

如果有一个ajax异步请求

componentDidMount() {
    axios.get('/list.json').then((res)=>{
        const data = res.data; 
        const action = initListAction(data);   // initListAction来自actionCreators
        store.dispatch(action)
   })
}

但是不希望在组件内写,需要统一把复杂请求或异步请求放到action里去处理,那么就用到了redux-thunk中间件,重写的代码如下

componentDidMount(e) {
    const action = getTodoList();  // 返回的是函数
    store.dispatch(action);    // 由于有redux-thunk,会执行这个函数
}

//actionCreater.js
export const getTodoList = () => {
    return (dispatch) => {
       axios.get('/list.json').then((res) => {
          const data = res.data;
          const action = initListAction(data);
          dispatch(action);   // 也就是前面说的发出action后,到现在才执行Reducer,因为现在Action是对象了
      })
   }
}

Vue双向绑定原理和基本实现

双向绑定原理

1、监听器Observer,通过Proxy或Object.defineProperty来劫持并监听所有属性,如果有变动的,就通知订阅者

2、解析器Compile,对每个元素节点的指令进行扫描和解析,页面初始化时根据指令模板替换数据,以及绑定相应的更新函数

3、Dep,收集多个订阅者,它是在监听器Observer和订阅者Watcher之间进行统一管理的

4、订阅者Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图(在Compile里初始化)

① 当读取所需对象的值时,会触发依赖收集,目的是将相关订阅者也就是Watcher,存放到的订阅器Dep(存放Watcher观察者对象)的subs中,形成的关系如下。(触发的方式是初始化Watcher的时候默认触发了getter)

② 在修改对象的值的时候,会通知之前依赖收集得到的订阅器Dep中的每一个Watcher,这些watcher都依赖着这个值,然后会告诉他们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这中间还有一个 patch和re-render 的过程 (setter-->watcher-update)
(有多个订阅者Watcher,watcher在初始化的时候会将自己装进订阅器Dep中)

几个问题

Q1:Watcher,Dep,Observer这几个类之间的关系?

A1:Watcher是观察者观察经过Observer封装过的数据,Dep是Watcher和观察数据间的纽带,主要起到依赖收集和通知更新的作用。

Q2:Dep中的subs存储的是什么?

A2: subs存储的是观察者Watcher实例。

Q3:Dep.target是什么,该值是何处赋值的?

A4:Dep是一个类,Dep.target是类的属性,并不是dep实例的属性,在全局可访问到,所以Dep.target在全局能访问到,可以任意改变它的值,保存当前的watcher实例,在new Watcher()的时候进行赋值,赋值为当前Watcher实例,订阅完成后设置为null(get这个方法使用很平常,不可能每次使用获取数据值的时候都去触发依赖收集)

实现思路

实现mvvm主要包含两个方面

1、视图变化更新数据

事件监听,如input输入框的change、keyup事件

2、数据变化更新视图

重点是如何知道数据变了,就是通过Object.defineProperty( )对属性设置一个set函数,当数据改变了就会来触发这个函数,所以我们只要将一些需要更新的方法放在这里面就可以实现data更新view了。

3cw1qU.png

实现代码

https://github.com/zjn225/east-mvvm

在线版,https://codepen.io/zjn225/project/editor/ZgWxQP

Vue - diff

作用

减少更新量,找到最小差异部分DOM,只更新差异部分DOM就好了,没必要把其他没有涉及的没有变化的DOM也替换了

一、做法

  • 对新旧节点(VNode)中 父节点是相同节点 的 那一层子节点 进行比较
  • 最大的根节点一开始可以直接比较,这也叫做 同层级比较,并不需要递归

uSojJA.png
uSTAij.png

二、比较逻辑

Diff 比较的核心是节点复用,所以Diff比较就是为了在新旧节点中找到相同的节点,条件是同层 + 同父级
uS7JAg.png

具体比较逻辑

  • 先找到 不需要移动的相同节点,消耗最小 (能不移动就不移动)
  • 再找相同但是需要移动的节点,消耗第二小 (莫得办法,只好移动)
  • 最后找不到,才会去新建删除节点

栗子

  • 第一轮:find 相同 && 不移动的节点,页面DOM不做变化
    uSLUaT.png

  • 第二轮:find 相同 && 移动的节点
    uSOWmq.png
    uSOX0x.png

  • 第三轮:新增 && 删除
    uSXdgJ.png
    于是开始创建节点 6 和 9,并且删除节点 4 和 5

关于AST

一、一般的编译语言执行之前的三个步骤

  • 分词/词法分析/扫描scanner(分解成词法单元)
    • 定义:将代码分解成有意义的代码块,这些代码块称为词法单元
    • 过程:它会读取代码,然后按一定规则合并成一个个的标识tokens,同时会移除空白符,注释等,最后代码被分割进一个tokens数组

ejKBLQ.png

  • 解析/语法分析(生成AST树、抽象语法树)
    • 定义:将词法单元流转换成AST(代表程序语法结构的树)
    • 过程:词法分析出来的tokens数组转化成AST树,同时验证语法看有没报错,以及删除一些没必要的token,所以AST不是100%和源码匹配的,完全匹配的叫CST(具体语法树)

ejMSwd.png

  • 代码生成
    将代码转换为可执行代码

JavaScript引擎比起这些步骤要复杂,比如在语法分析和代码生成阶段有特定的步骤来对性能进行优化,包括对冗余代码进行优化

很多流行的语言都可以通过AST解析成一颗语法树,也可以认为是一个JSON,比如:CSS、HTML、JavaScript、PHP、Java、SQL等

二、常见的AST解析器

1、JavaScript

  • babylon,因为他是babel的解析器
  • acron: babylon就是从这个库fork来的
  • flow:facebook 出品的JavaScript 静态类型检查工具
  • typescript
  • recast
  • uglyfy-js:

2、HTML

  • htmlparser2:比较常用
  • parse5

3、CSS

  • less/sass
  • cssom、csstree

三、AST的使用场景

  • 代码编译:babel编译es6、JSX、typescript、sass/less
  • 代码压缩
  • 编辑器的代码高亮、格式化、自动补全、eslint检查
  • wepy、mpvue、taro、VX等可以转小程序的框架

关于转小程序其思路大致如下
ejQMHH.png

总体思路
eQjbCt.png

在线玩转AST玩转AST2

四、小实践

这里用到的解析器是babylon、

import * as babylon from "babylon"; // js的词法解析器
import traverse from "babel-traverse"; // 遍历ast
import generate from "babel-generator"; // 根据ast生成代码

const code = `const a = 5;`

// 解析生成ast
const ast = babylon.parse(code);

// 遍历ast做修改
traverse(ast,{
    enter(path){
        if(path.node.type === 'VariableDeclaration' ){
            path.node.kind = 'var'
        }
    }
})

// 生成代码
let res = generate(ast,{},code).code

console.log(res)

五、关于ASI

自动分号插入 (automatic semicolon insertion, ASI) ,在JS的语法分析 (parsing)阶段起作用,大部分语句如声明、表达式、return、break等都需要;,但块语句、if、try等语句是不需要;的

规则都基于以下两点:

  • 以换行为基础
  • 解析器会尽量将新行并入当前行,当且仅当符合ASI规则时才会将新行视为独立的语句

比如,新行并入当前行将构成非法语句,自动插入分号,在continue,return,break,throw后自动插入分号,++、--后缀表达式作为新行的开始,在行首自动插入分号

React组件化的演变——读《React.js小书》

通过点赞功能进行说明
image

版本一

  • 纯粹通过基本的dom来操作
  • 没有任何复用性可言
<body>
    <div class='wrapper'>
        <button class='like-btn'>
            <span class='like-text'>点赞</span>
            <span>👍</span>
        </button>
    </div>
</body>
// --------------------------------------------------
const button = document.querySelector('.like-btn')
const buttonText = button.querySelector('.like-text')
let isLiked = false
button.addEventListener('click', () => {
    isLiked = !isLiked
    if (isLiked) {
        buttonText.innerHTML = '取消'
    } else {
        buttonText.innerHTML = '点赞'
    }
}, false)

版本二

  • 初步复用,通过Class封装为一个类,之后可通过实例化构建点赞功能的实例
  • 粗鲁地通过innerHTML插入页面,而且也没有监听事件
class LikeButton {
    render() {
        return `
        <button id='like-btn'>
          <span class='like-text'>赞</span>
          <span>👍</span>
        </button>
      `
    }
}
// ----------------------------------------
const wrapper = document.querySelector('.wrapper')
const likeButton1 = new LikeButton()
wrapper.innerHTML = likeButton1.render()

const likeButton2 = new LikeButton()
wrapper.innerHTML += likeButton2.render()

版本三

  • 复用能力+1,现在render返回的不再是字符串了,而是对应的dom + 需要的事件
  • 但是如果状态不止一个isLiked,那么频繁操作DOM太影响性能、而且也不好维护
const createDOMFromString = (domString) => {
    const div = document.createElement('div')
    div.innerHTML = domString
    return div
}
class LikeButton {
    constructor() {
        this.state = { isLiked: false }
    }
    setState(state) {
        const oldEl = this.el
        this.state = state
        this.el = this.render()
        this.onStateChange && this.onStateChange(oldEl, this.el) // 实例化后被创建
    }
    changeLikeText() {
        this.setState({
            isLiked: !this.state.isLiked
        })
    }

    render() {
        this.el = createDOMFromString(`
        <button class='like-btn'>
          <span class='like-text'>${this.state.isLiked ? '取消' : '点赞'}</span>
          <span>👍</span>
        </button>
      `)
        // 如果不通过bind,那么真正执行的时候changeLikeText里面的this是window
        // 因为new的时候是直接执行changeLikeText
        this.el.addEventListener('click', this.changeLikeText.bind(this), false)
        return this.el
    }
}
// ------------------------------------------------------------------------------------
const likeButton = new LikeButton()
wrapper.appendChild(likeButton.render()) // 第一次插入 DOM 元素
likeButton.onStateChange = (oldEl, newEl) => {
    wrapper.insertBefore(newEl, oldEl) // 插入新的元素
    wrapper.removeChild(oldEl) // 删除旧的元素
}

版本四

优化DOM操作,一旦状态发生改变,就重新调用 render 方法,构建一个新的 DOM 元素。
具体来说当用户点击时,会调用setState,setState在改变状态的同时内部会调用render,render会根据state的不同构建不同的DOM元素(数据驱动),最后会通过 onStateChange 告知外部插入新的 DOM 元素,然后删除旧的元素,页面就更新了。

const createDOMFromString = (domString) => {
    const div = document.createElement('div')
    div.innerHTML = domString
    return div
}
class LikeButton {
    constructor() {
        this.state = { isLiked: false }
    }
    setState(state) {
        const oldEl = this.el   // 旧dom
        this.state = state
        this.el = this.render()  // 最新的dom
        this.onStateChange && this.onStateChange(oldEl, this.el) // 实例化后被创建
    }
    changeLikeText() {
        this.setState({
            isLiked: !this.state.isLiked
        })
    }
    render() {
        this.el = createDOMFromString(`
        <button class='like-btn'>
          <span class='like-text'>${this.state.isLiked ? '取消' : '点赞'}</span>
          <span>👍</span>
        </button>
      `)
        this.el.addEventListener('click', this.changeLikeText.bind(this), false)
        return this.el
    }
}
// ------------------------------------------------------------------------------------
const likeButton = new LikeButton()
wrapper.appendChild(likeButton.render()) // 第一次插入 DOM 元素
likeButton.onStateChange = (oldEl, newEl) => { 
    wrapper.insertBefore(newEl, oldEl) // 插入新的元素
    wrapper.removeChild(oldEl) // 删除旧的元素
}

版本五

为了能够写更多的类似组件,可以把这种模式抽象出来,放到一个Component中,让所有组件都能继承这个父类。子类继承后只需要实现一个返回HTML字符串的render方法

还有一个额外的 mount 的方法,其实就是把组件的 DOM 元素插入页面,并且在 setState 的时候更新页面:

class Component {
    constructor(props = {}) {
        this.props = props
    }
    setState(state) {
        const oldEl = this.el
        this.state = state
        this.el = this.renderDOM()
        if (this.onStateChange) this.onStateChange(oldEl, this.el)
    }
    renderDOM() {
        this.el = createDOMFromString(this.render())
        if (this.onClick) {
            this.el.addEventListener('click', this.onClick.bind(this), false)
        }
        return this.el
    }
}
const createDOMFromString = (domString) => {
    const div = document.createElement('div')
    div.innerHTML = domString
    return div
}
const mount = (component, wrapper) => {
    wrapper.appendChild(component.renderDOM())
    component.onStateChange = (oldEl, newEl) => {
        wrapper.insertBefore(newEl, oldEl)
        wrapper.removeChild(oldEl)
    }
}

之后再需要其他组件的时候,就能直接继承了

 class LikeButton extends Component {
     // 把 props 传给父类,相当于父类执行一遍构造函数,这样就可以通过 this.props获取到配置参数:
    constructor (props) { 
      super(props)
      this.state = { isLiked: false }
    }
    onClick () {
      this.setState({
        isLiked: !this.state.isLiked
      })
    }
    render () {
      return `
        <button class='like-btn' style=`background-color: ${this.props.bgColor}`>
          <span class='like-text'>
            ${this.state.isLiked ? '取消' : '点赞'}
          </span>
          <span>👍</span>
        </button>
      `
    }
  }
  mount(new LikeButton({ bgColor: 'red' }), wrapper)

JS性能优化之节流、防抖

节流

连续触发事件过程中有一定的时间间隔。

比如人的眨眼睛,就是一定时间内眨一次。

 function throttle(func, delay) {
            var prev = Date.now();
            return function () {
                var context = this;
                var args = arguments;
                var now = Date.now();
                if (now - prev >= delay) {
                    func.apply(context, args);
                    prev = Date.now();
                }
            }
        }

防抖

短时间内多次触发同一事件只执行最后一次

如果有人陆续刷卡上车,司机就不会开车。只有别人没刷卡了,司机才开车。一定要停下来才能开始计时

   function debounce(func, delay, immediate) {
            var timer = null;
            return function () {
                var context = this;
                var args = arguments;
                if (timer) clearTimeout(timer);
                // 立即执行
                if (immediate) {
                    var doNow = !timer;
                    //一开始的时候还没设置定时器,timer还是null,之后就是等于调用次数了
                    //  所有第一次doNow是true,之后设置一个定时器,delay之后重置为null,
                    // 在此之前,都不为null,因为已经设置了
                    if (doNow) {
                        func.apply(context, args);
                    }
                    timer = setTimeout(function () {
                        console.log("timergg", timer)
                        timer = null;
                    }, delay);
                    // 不立即执行
                } else {
                    timer = setTimeout(function () {
                        func.apply(context, args);
                    }, delay);
                }
            }
        }

浏览器渲染相关知识点总结

一、渲染过程

  • 处理HTML标记数据并生成DOM树。
  • 处理CSS标记数据并生成CSSOM树。
  • 将DOM树与CSSOM树合并在一起生成渲染树。
  • Layout(布局):计算每个 DOM 元素在最终屏幕上显示的大小和位置。
  • Paint(绘制):在多个层上绘制 DOM 元素的的文字、颜色、图像、边框和阴影等。

布局和绘制涉及到两个名词: reflow(重排/回流)和repain(重绘)。页面在首次加载时必然会经历reflow和repain

  • composite(渲染层合并):按照合理的顺序合并图层然后显示到屏幕上。

lG5jMR.png

lGIPiD.png

图一也叫做像素管道,这里就不多做阐述了

二、常见问题

1、DOM树是怎么构建的

  • 浏览器从磁盘或网络读取HTML的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串
  • 将字符串转换成Token,如:<html>、<body>等
  • 生成节点对象并构建DOM

2、token是如何拆分的

举个栗子

<p class="a">abc</p>
  • <p“标签开始”的开始
  • class=“a” 属性
  • > “标签开始”的结束
  • abc 文本
  • </p> 标签结束

lGIVsI.png

3、除了script标签,还有什么会阻塞DOM的构建

是CSSOM,造成CSSOM影响DOM构建的原因还是由于script标签的中途插入,当JS想修改样式时,必须拿到完整的CSSOM,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟JS执行和DOM的构建,直至完成CSSOM的下载和构建

4、渲染树包括了什么

render tree只包含了用于渲染页面的节点

为了形成渲染树,浏览器大致做的事情如下:

  • 从DOM树根节点开始,遍历每一个可见的节点和对应的样式
    一些节点是完全不可见的(比如 script标签,meta标签等),这些节点会被忽略,因为他们不会影响渲染的输出
  • 一些节点是通过CSS样式隐藏了,这些节点同样被忽略——例如上例中的span节点在render tree中被忽略,因为span样式是display:none;
  • 对每一个可见的节点,找到合适的匹配的CSSOM规则,并且应用样式
    显示可见节点(节点包括内容和被计算的样式)

5、浏览器包含哪些进程

  • 主进程(负责协调,主控),只有一个
  • 第三方插件进程:每种类型的插件对应一个进程
  • GPU进程:最多一个,用于3D绘制等。
  • 浏览器渲染进程

6、渲染进程包括了什么

浏览器是多进程的,浏览器的渲染进程是多线程的;

  • GUI渲染线程,用于布局绘制等
  • JS引擎线程
  • 事件触发线程
  • 定时触发器线程
  • 异步http请求线程

lGIuo8.png

三、关于图层

1、概念

上面提到了composite,浏览器渲染的图层大致分为2种:普通图层、复合图层

  • 普通文本流属于1个复合图层,无论里面多少元素都在这个复合图层里
  • absolute和fixed 虽然可以脱离普通文档流,但是无法脱离默认复合层,所以absolute中信息的改变,仍然会影响整个复合层的绘制
  • 通过硬件加速的方式可以声明一个新的复合图层,不会影响到默认复合图层

2、复合图层的创建条件有什么?(硬件加速)

  • 拥有具有3D变换的CSS属性,如translate3d,translatez
  • CSS3动画的节点
  • video、canvas、iframe、webgl元素
  • 拥有CSS加速属性的元素(will-change),一般配合opacity与translate使用
  • 元素有一个z-index较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
  • 如果有滚动条,滚动条也会单独生成一个图层
  • flash

3、复合图层作用

独立于普通文本流中,改动可以避免整个页面的reflow和repaint,提升性能,但是也要注意不要过度使用,否则资源也会消耗过度,适得其反

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.